LINUX0.11內核閱讀筆記

我是通過閱讀趙炯老師編的厚厚的linux內核完全剖析看完LINUX0.11的代碼,不得不發自內心的說Linus真的是個天才。雖然我覺得很多OS設計的思想他是從UNIX學來的,但是他自己很周全很漂亮很巧妙地實現瞭如此龐大一個系統的絕大多數代碼。這裏面有太多環節需要注意,很難得。。。

讀完之後覺得很有收穫,雖然版本很低,但是已經對OS有一個很具體的認識了,比理論上的要來得深刻、真實。下面是我自己學習過程的思考和總結,在看完細節之後主要從LINUX各個功能模塊其及相互之間和內部的層次關係去考慮的,本文圖片均取自該書。我覺得這篇總結性質的文章對還沒有接觸linux0.11內核的人來說肯定沒有什麼意義。應該只有讀過的代碼的人才會有同感吧。另外我看代碼的時候使用了VC版的內核源碼工程,代碼中的註釋與書中幾乎一樣。用VC可以更容易地在函數定義中跳轉查看,節約時間,我的方法是看書上代碼前給出的知識介紹,然後在電腦上看代碼實現,一共用了十天把這本書主要部分看完了。這裏給希望閱讀代碼的人分享一下:

http://www.mcuol.com/download/upfile/20071011080428_linux011VC.rar

一.源碼目錄

<!--[if !vml]--><!--[endif]-->

圖1

二.系統總體流程:

系統從boot開始動作,把內核從啓動盤裝到正確的位置,進行一些基本的初始化,如檢測內存,保護模式相關,建立頁目錄和內存頁表,GDT表,IDT表。然後進入main進行初始化設置,main完成系統各個模塊要用到的所有數據結構和外部設備的初始化。使得系統可以正常的工作。然後才進入用戶模式。執行第一個fork生成進程1執行init,運行shell,接受並執行用戶命令.

這裏整個系統建立起來了,OS就處於被動狀態,靠中斷和系統調用來完成每一項服務。

三.各個目錄的閱讀總結:

(一) boot

1.bootsect.s :

bootsect.s編譯結果生成一個512BYTE(一個扇區)鏡像。這個扇區的最後一個字是0xAA55,倒數第二個字是root_dev變量,值爲ROOT_DEV(306),即根文件系統所在的設備號。這段代碼必須寫入到啓動盤的啓動扇區,也就第一個物理扇區上。這樣機器啓動後,BIOS自動把它加載到7C00H處並跳到那裏開始執行。bootsect將自己移動到90000H(576K)處,並跳至那裏相應位置執行。然後利用BIOS中斷將setup直接加載到自己的後面(90200h)(576.5K處),並將system加載到地址10000h處。 跳到setup中執行。

2.setup.s:

利用BIOS中斷把系統參數如顯卡,硬盤參數保存到內存90000開始的位置,即覆蓋原bootsect所在的內存位置。再把整個system 模塊移動到00000 位置。加載GDTR和IDTR,這裏的GDT表是臨時的,保存了兩個描述符,即內核代碼、內核數據段描述符,其段基地址爲0。而加載IDT除了進入保護模式需要加載IDTR之外,沒有任何意義。開啓A20 地址線開啓擴展內存。重新設置8259中斷碼0x20~0x2f。進入保護模式(PE置1)跳轉到system模塊中的head.s中(0處)執行。Bootsect.s和setup.s執行時內存變化情況。

<!--[if !vml]--><!--[endif]-->

圖2

3.head.s:

前4KB的代碼將被頁目錄覆蓋掉。這些代碼執行的操作包括:設置系統堆棧爲_stack_start。重新設置GDTR和IDTR。gdt,idt表都定義在head.s的末端,長度均爲256項(2KBYTE)。第2個頁面到第5個頁面是系統的4張頁表。最後一個頁表後面的代碼執行分頁操作,即填充的4個頁目錄和4張頁表的內容,實現對等映射,即物理地址=線性地址。每個頁表項屬性爲存在並且用戶可讀寫。設置好後置CR3頁目錄地址即0。啓動分頁標誌,CR0的PG標誌置1。跳到main函數中執行。

跳到main之前,內存佈局如下:從0到16M

                    頁目錄4K(0x0開始)

                    頁表1 4K

                    頁表2 4K

                    頁表3 4K

                    頁表4 4K

                     軟盤緩衝區1K

                     head.s後半部分代碼

                     IDT表2K

                     GDT表2K

                     main.o代碼部分

                  內核其餘部分(大約到512K,end值爲結束地址)

                  setup保存的系統參數(90000H~900200)這個區間還保存着root_dev.

                  BIOS(640K-1M)

                     主內存區(1M-16M)

 

現在初始化好了內核工作依賴的主要的數據結構是GDT和IDT表,還有頁表。

(二)內核初始化init

main.c將進行進一步初始化工作。主要方面:分配主內存功能,IDT表各中斷描述符重新設定,對內核其它模塊如mm,fs進行初始化,然後移到用戶模式下生成進程1執行init,常駐進程0死循環執行pause。進程init加載根文件系統,設置終端標準IO,創建進程2以/etc/rc爲標準輸入文件執行shell.完成rc文件中的命令。

init等進程2退出,進入死循環:創建子進程,建立新會話,設置標準IO終端,以登錄方式執行shell.

至此係統動作起來了。

 

所以整個系統的建立起來後除了兩個死循環的進程idle和init,其它的動作都是由用戶在shell下執行命令,產生系統調用來工作的。

通過執行move_to_usermdoe(),idle和init進程都屬於用戶態下的進程。而內核則完全是中斷驅動的。也就是說只有通過中斷才能進入系統,如時鐘和系統調用等。

 

所以問題的重點就在於內核各部分數據結構的建立、初始化、操作是怎樣進行的。這些初始化流程涉及到內核各個模塊全部重要的數據結構。

現在從main執行的一系列初始化代碼來淺窺一下:

1.根據內存的大小,設置高速緩衝的末端。16M內存把高速緩衝末端設爲4M。緩衝末端到主存末端爲主內存區。

2.mem_init(main_memory_start,memory_end);主內存區初始化

 設置高端內存HIGH_MEMORY=memory_end,

 設置內存映射字節圖mem_map [ PAGING_PAGES ],將不可用的全部置爲USED,可用的置爲0。mem_map數組是系統mm模塊核心數據結構,記載了每個內存頁使用計數。

3.trap_init().硬件中斷向量表設置。

  向IDT中填充各個中斷描述符,使其指向對應的中斷處理程序。對於錯誤,基本是結束當前進程。其他如外設中斷都是各個模塊初始化的時候向IDT表中相應項進行設置。

4.blk_dev_init();     // 塊設備初始化。

       初始化請求數組request[],將所有請求項置爲空閒項(dev = -1)。

5.chr_dev_init();    // 字符設備初始化。尚爲空操作。

6.tty_init();            // tty 初始化。                    

       /// tty 終端初始化函數。                                                 

       // 初始化串口終端和控制檯終端。                                          

       void tty_init (void)                                                     

       { 

              rs_init ();                   // 初始化串行中斷程序和串行接口1 和2。(serial.c, 37)   

              con_init ();                   // 初始化控制檯終端。(console.c, 617)                

       }

       rs_init 初始化兩個串口,安裝串口中斷處理IDT項。

       con_init 初始化顯示器和鍵盤。安裝鍵盤中斷處理IDT項。                                                                 

7.time_init().取CMOS 時鐘,並設置開機時間 startup_time(爲從1970-1-1-0 時起到開機時的秒數)

8.sched_init(); // 調度程序初始化(加載了任務0 的tr, ldtr)

 這裏初始化與進程調度有關的數據結構。

 手工設置了任務0的TSS和LDT到GDT表中。

 清GDT表和task[NR_TASKS]數組其餘部分。

 ltr (0);                 // 將任務0 的TSS 加載到任務寄存器tr。

lldt (0);                // 將局部描述符表加載到局部描述符表寄存器。

設置內核的工作心跳--8253定時器,安裝定時器中斷。

設置系統調用中斷門:set_system_gate (0x80, &system_call);

9.buffer_init(buffer_memory_end);// 緩衝管理初始化,建內存鏈表等。

  在內核的結束地址end(由連接程序生成)到buffer_memory_end之間(除掉640kb-1M的BIOS範圍)區域中,建立緩衝區鏈表(表頭start_buffer)並分配緩衝塊(1KB)。

  初始化空閒表free_list,HASH表hash_table。

10.hd_init();// 硬盤初始化。

 blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST; //設置硬盤的設備請求函數爲do_hd_request.

 設置硬盤中斷處理IDT項。

11.floppy_init();//軟盤初始化。

       blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST;

       設置軟盤中斷處理IDT項。

12.sti()開中斷。

13.move_to_user_mode();移動到用戶態執行。

這是個宏。由嵌入彙編代碼組成。設置內核堆棧中的CS爲任務0代碼段(用戶態),通過中斷返回iret, 自動加載了LDT0的代碼段到CS,數據段到SS,DS等,完成了從特權級從0跳到3。其實執行的代碼在內存中的位置完全相同。只是完成執行權跳到用戶態而已。這樣,內核執行變成了任務0的執行。

14.fork();生成進程1,執行init();

這裏的fork()是內聯函數。爲了不使用用戶棧。

進程0從此死循環執行pause();

15 init();

進程1執行init()函數。

調用setup取硬盤分區信息hd.加載虛擬盤,進程init加載根文件系統,設置終端標準IO,創建進程2以/etc/rc爲標準輸入文件執行shell.完成rc文件中的命令。加載完根文件系統之後,整個OS就已經完整地運行起來了。

init等進程2退出,進入死循環:創建子進程,建立新會話,設置標準IO終端,以登錄方式執行shell.,剩下的動作由用戶來決定了。

 

(三)kernel:

<!--[if !vml]-->
<!--[endif]-->

圖3

個人認爲最主要的是中斷代碼,然後是中斷代碼會調用的通用代碼。爲什麼這麼說呢,無論是調度schedule,還是fork,都只有在用戶進程執行int 0x80 中斷進行系統調用或者是硬件中斷才能進入內核代碼,執行內核函數。當內核初始化結束後所有進程都是用戶態進程,只有通過IDT表中定義的那些中斷函數去執行內核代碼。所以中斷是OS的主線,只是在功能上分成了多個模塊。

在traps.c中,設置了絕大多數中斷向量,通過set_trap_gate()或者set_intr_gate設置對應IDT描述符。set_trap_gate()不會屏蔽中斷,而set_intr_gate屏蔽外部中斷,中斷處理程序都是用匯編定義的,大部分在asm.s中定義,其餘在system_call.s,keyboard.s,rs_io.s中定義。 彙編程序中再調用C語言程序做具體的處理。

比較重要的中斷時鐘中斷int 0x20,系統調用中斷int 0x80,頁故障中斷int14,還有一些外部設備如鍵盤,硬盤等也很重要,不過屬於fs模塊的內容。大多數異常只是簡單調用sys_exit()結束當前進程,並重新調度其他進程schedule()。

從幾個重要中斷去中斷執行流程去弄清OS怎麼工作的:

1)int 0x20 時鐘中斷。

時鐘是整個OS工作的心跳。8253每10ms產生一箇中斷。中斷服務執行do_timer(),然後do_signal();

do_timer主要判斷當前進程時間片是否用完,如果用完且處於用戶態則執行schedule()重新調度。如果中斷時當前進程正在內核態執行,則不能進行切換。也是說linux在內核態不支持任務搶佔。這樣使得內核的設計大大的簡化了,因爲除了進程自己放棄執行(如sleep,wait類)不用擔心臨界區資源競爭的問題。

如果當前進程是用戶進程,判斷當前信號位圖中是否有未處理的信號,取最小信號然後調用do_signal()。這個函數想要執行用戶定義的信號處理函數。

do_signal()把信號對應的處理函數插入到內核堆棧eip處,並修改用戶堆棧使中斷返回後用戶進程執行信號處理函數,

信號處理函數返回後執行一個sa_restorer,恢復用戶堆棧爲正常中斷退出之後的狀態。(這是一個技巧,它實現了內核空間調用用戶空間的函數!!!)

另外內核空間與用戶空間數據交換默認通過fs來完成。

用戶進程總是通過中斷進入內核態,信號判斷總是發生在時鐘中斷和系統調用中斷處理之後。所以實時性也很強,因此稱爲軟中斷。

關於信號,在schedule()中,對當前系統中的所有進程alarm信號定時判斷,可睡眠打斷進程如果有未屏蔽信號置位喚醒(狀態改爲就緒)。

因此時鐘,系統調用,以及最頻繁調用的schedule()裏面都會處理信號。因此信號總是可以及時地得到"觸發"。

2)int 0x80 系統調用

系統調用的架構就是一個統一的中斷入口和出口_system_call,保護現場,準備參數(最多三個),取調用號,調用系統調用函數列表中對應處理函數,多是名爲sys_XXX()的C函數。C處理函數返回後的後期流程:

如果進程系統調用後狀態不爲就緒態或者時間片用完,執行調度schedule();判斷中斷前是用戶進程且有未處理的信號?執行do_signal(),中斷返回。

 

最重要的系統調用莫過於fork()和execve;

首先進程的重要組成部分:

任務數組task[],每個任務佔一項(假定序號爲nr),每個虛擬地址空間都是64M, 範圍從nr*64M到(nr+1)*64M-1 ,在頁目錄表中最多佔16項,每項對應一個頁表即4M空間。進程任務數據結構task和內核棧共用一頁空間,內核棧頂在頁空間末端,task數據在頁起始端。

進程的頁表佔用的頁需要通過內存管理提供的接口get_free_page()來申請。 每個進程在GDT表中佔用兩個描述符項,LDT(nr)和TSS(nr)。

 

fork()流程:

調用find_empty_process()找一個空閒的進程任務task[]下標即任務號nr,並取得一個不重複的pid.
調用copy_process(...),裏面一堆參數都是系統棧中保存的全部內容。(彙編和C混合編程技巧!)。copy_process 向mm申請一頁內存保存task數據和設置內核堆棧。把父進程也就是當前進程的task數據全部拷貝,然後修改。
設置tss內容,(需要修改的主要是ss0=內核數據段,esp0=申請的頁底部,eax=0)。

copy_mem拷貝進程空間。注意任務號nr意義在於進程的虛擬地址空間在nr*64M~(nr+1)*64M範圍內。copy_mem先計算子進程虛擬地址空間基址和父進程空間大小,設置子進程LDT中的代碼段和數據段描述符基址和段限。調用copy_page_tables複製進程空間。copy_page_tables就是把父進程佔用的頁目錄項和全部頁表中指定的有效的物理頁面全部拷貝到子進程的頁目錄項和頁表中去。同時把父子進程的頁表設爲共享的也就是隻讀的,一旦任意一個進程執行寫內存操作,將發生頁錯誤中斷。這個中斷將導致系統爲進程重新分配可寫的內存頁。copy_page_tables先計算父子進程虛擬地址空間佔用的目錄項(16個最多)開始地址,對每個有效的目錄項先爲子進程分配一個頁面作爲頁表,然後對該目錄項下所有有效的頁表項進行復制。同時把r/w位都置0。把對應物理頁的mem_map[]加1。這樣做非常高效而且非常巧妙。最大限度地共享了本身就只讀或者不需要再寫的頁面。每當進程和內核之間要交換數據時尤其是內核向進程空間寫數據時總是要先驗證進程給的線性地址是否有效。如verify_area,write_verify..這兩個函數最終會調用un_wp_page,取消頁面的寫保護。對mem_map[]=1的直接置r/w爲1,mem_map[]>1表明頁面共享了,內存頁映射表mem_map[]-1然後申請空閒物理頁,設置到頁表項中,並複製頁面copy_page。可見,父子進程先寫進程者將申請空閒頁並拷貝頁面內容,另一個則可以直接使用原來的頁面,因爲這時mem_map[]=1了。

進程空間拷貝完畢之後,再設置一些task結構數據。給GDT表填加兩項LDT(nr),TSS(nr).進程狀態設爲就緒,等待被調度就OK了。。

這就是所謂的寫時複製,太神奇了。。

 

execve提供了需求加載的機制。

它加載一個程序文件到進程中執行,因此把原來進程擁有的頁表項和頁表全部釋放掉。同時分配一頁內存存放參數。

根據可執行文件頭把進程任務數據結構task[nr]所有數據都設置好,但是並不加載一頁代碼數據。所以整個進程就是一副空架子。

從進程空間的第一條語句開始執行就會產生中斷,然後根據PC的值從外設中加載所在頁到內存中。這個中斷將執行do_no_page.

這個函數在fs模塊中定義,在fs模塊中再仔細分析。

 

3)缺頁中斷int14

這個中斷是十分有用的,它實現寫時複製。fork和execve沒做完的事情都會由這個中斷提供的功能來了結。

中斷錯誤號爲出錯頁表項的最後3位。根據P位0或1判斷是缺頁中斷或寫保護中斷。

缺頁中斷調用do_no_page,寫保護調用do_wp_page.

do_wp_page提供寫時複製機制,

取消頁面保護(對於主內存區而言),頁面不是共享狀態,即mem_map[]=1,則置r/w=1返回。

如果頁面是共享狀態(mem_map[]>1),mem_map[]--,申請一頁內存,並拷貝,映射到進程空間。

do_no_page提供的需求加載機制。

CR2提供發生錯誤時的線性地址。如果當前進程沒有可執行文件且該地址比數據段末地址大,這可能是因爲堆棧伸長引起的,直接申請一頁內存映射到該線性地址所在頁。

否則嘗試頁面共享,即如果有執行文件而且其inode使用計數>1,表明系統中可能有進程也在執行這個程序,這樣可以查找到這個進程並把其對應地址處的頁面共享到自己空間,也就是修改這兩個進程對應的頁表項。而且是共享方式,所以只能讀不能寫,如果有一個進程要執行寫,則會引起寫保護中斷,系統再給寫的進程另外再分配內存頁並拷貝一頁內容。如果嘗試頁面共享失敗,沒辦法只得從外設中加載,找到可執行文件的inode.計算要讀的邏輯塊號(注意第一塊是文件頭),讀一頁(4塊)到內存。分配頁面,複製緩衝中的4塊數據,把頁面映射到進程空間引起中斷的線性地址處。

 

(四)mm內存管理

linux的mm雖然只有兩個文件memory.c和page.s,但是內容卻很不簡單。必須對分頁機制有很好的理解才能讀明白。
這個版本的內核每個進程虛擬空間64M,共支持4G/64M=64的任務數。所有進程共用一個頁目錄,但是卻有自己的頁表。
對虛擬地址的劃分使得在頁目錄中也存在劃分。每個進程虛擬空間最大佔用16個目錄項,每個目錄項指向一個頁表(1024個內存頁),對應4M空間。
線性地址分三段,每段都是一個索引index或者叫偏移(offset),第一段索引是在頁目錄(基址在CR3)中找到頁目錄項,頁目錄項裏保存的是一張頁表的基地址。以線性地址的第二段爲索引加上這個基地址,得到的頁表項保存的是實際內存頁的起始地址。再加上線性地址第三段爲偏移,得到線性地址映射的實際物理地址。

 

內存管理提供的功能主要有管理頁面,操作進程空間,缺頁中斷處理(寫時複製,需求加載),共享內存頁。其中大多數函數都會訪問頁目錄和頁表,都使用上述的計算的原理。

 

內存管理mm和內核kernel兩部分代碼聯繫十分密切

內存管理提供的主要的功能函數可以分爲

1管理頁面    :取一個空閒頁get_free_page,釋放一頁free_page.
2操作進程空間:free_page_tables釋放進程頁目錄和頁表
                            copy_page_tables在進程空間之間複製頁目錄和頁表。主要提供給fork()使用,實現寫時分配。
                            put_page 把一頁內存映射到進程空間中去。
                            write_verify進程空間有效性驗證,當內核向用戶空間寫數據之前必須進行驗證。爲可能爲無效的地址區域分配頁面
3頁面共享&頁故障中斷:
                            try_to_share 嘗試在打開文件表中找當前執行程序inode,已經存在的話就查找所有進程中executalbe與當前進程相同的任務。有則嘗試共享對應地址的映射的物理頁面。即添加到自己相應位置的頁表項中去。(share_page)
                            do_no_page 缺頁中斷。判斷地址是否超出end_data,是則可能是堆棧伸長,分配頁面到相應位置(get_free_page,put_page),否則表示地址在可執行文件內部,先嚐試共享,不成功則從線性地址計算需加載部分在文件上內部的塊號,通過bmap把文件內部塊號映射的設備邏輯塊號計算出來。申請空閒頁並通過bread_page讀取一頁,最近由put_page把這頁映射到發生中斷的進程頁面上。

un_wp_page在寫保護中斷中調用,取消頁表保護,實現寫時複製。

(五)文件系統模塊fs:

1.總體結構:

Linux把所有設備都做爲文件來看待。提供統一的打開,關閉,讀寫系統調用接口。          下面是文件系統層次關係:

<!--[if !vml]--><!--[endif]-->

圖4

總體來說,文件系統提供兩類外部接口(系統調用),文件讀寫和文件管理控制。

上圖中Read_write代表的是文件讀寫系統調用接口read,wirte。它根據操作文件的類型分別調用了四種讀寫函數:

字符型文件tty_read,tty_write,在kernel/chr_drv驅動模塊中定義;
FIFO文件  pipe_read,pipe_write 都是內存操作。Fs/pipe.c中定義
block_dev塊設備文件 :block_read,block_wirte,間接調用bread。

File_dev 常規文件。File_read,file_write,   涉及的內容是fs主要的內容。

圖中Open stat fcntl 則是文件系統的系統管理控制接口如創建打開關閉,狀態訪問修改功能。這主要針對常規文件,如根文件系統下的全部文件。這些都需要底層文件系統函數支持,主要包括文件系統超級塊,i結點位圖和邏輯塊位圖,i結點,文件名解析等操作。而這些底層文件系統函數則建立於buffer提供的緩衝管理機制之上。這些是對上圖的大體歸納吧!

在上面總結kernel的時候,沒有提及blk_drv和chr_drv,因爲我覺得把它們放在文件系統裏面來更合適。

Blk_drv目錄是塊設備驅動代碼。實現了HD(硬盤),FD(軟盤),RD(Ramdisk)三種塊設備的底層驅動,並提供一個外部調用的接口ll_rw_block(dev,nr)。就是上圖中右下虛框示意的層次上。

圖5

 

同樣的,char_drv實現了字符設備(串行終端)的驅動,包括控制檯(鍵盤屏幕),兩個串口。實現供上層調用的讀寫接口read_tty , write_tty。下面是源碼關係圖:

<!--[if !vml]--><!--[endif]-->

圖6

2下面分別從從底層向高層總結一下各個層次中源碼實現的主要細節:

2.1 塊設備驅動部分 kernel/blk_drv

塊設備工作流程(粗略):

1)文件設備接口調用底層塊設備讀寫函數ll_rw_block(int rw,buffer_head *bh).這裏bh要讀的設備號,塊號,已經寫入bh, rw是讀或者寫指令

2)ll_rw_block(int rw,buffer_head *bh)取主設備號major,調用make_request(major,rw,bh);

3)make_request(major,rw,bh)申請一個請求項,根據rw和bh相應設置填充req各字段值,     並調用add_request (major + blk_dev, req)插入到設備major的請求隊列。

4)add_request (major + blk_dev, req)檢查設備等待隊列是否爲空,爲空則把req添加到隊列中並馬上調用設備的請求執行函數。
       對於硬盤,這個函數就是do_hd_request,它將根據請求項的各個字段設置向硬盤發出相應的命令. 如果請求隊列不爲空,則按照電梯算法把req加到隊列中。
       ll_rw_block函數返回。

整個ll_rw_block()返回到上層調用(緩衝管理部分buffer.c)。然後調用進程將執行等待wait_on_buffer(bh);進程切換。

硬盤接受命令後,完成req要求的讀/寫一個扇區後將發出中斷。hd_interrupt(定義於kernel/system_call.s)被執行。調用_do_hd。do_hd是設備當前要調用的中斷處理函數的指針。根據當前請求,do_hd_request在調用hd_out向硬盤控制器發命令(如讀寫或復位等)時根據命令類型指定do_hd爲read_intr, write_intr或其它。如果爲讀,do_hd=read_intr。寫則do_hd=write_intr.

read_intr 將判斷當前請求項請求讀的扇區數是否已經全部讀完,如果沒有,再次設置do_hd=read_intr,然後返回。如果全部完成,則調用end_request(1)去喚醒等待的進程。然後調用do_hd_request去處理其餘請求項。

 

write_intr 將判斷當前請求項請求寫的扇區數是否已經寫完,如果沒有,把一扇區數據複製到硬盤緩衝區內,然後再次設置do_hd=write_intr並返回。如果寫完,則調用end_request(1),更新並有效緩存塊,然後調用do_hd_request去處理其餘請求項。

整個硬盤讀寫流程如下 :


圖7

 

對於軟盤,大體的流程差不多。只是軟盤有啓動馬達等延時寫時操作,比較瑣碎一些。

對於ramdisk,速度很快所以不需要中斷機制,當然請求隊列也最多隻有當前一個。像上面的過程一樣,make_request會調用add_request,而由於前面的請求隊列一定爲空,所以會馬上執行do_rd_request. 在do_rd_request中直接讀、寫數據。然後就end_request(1).

2.2 字符設備驅動 kernel/chr_drv

串行/字符設備在linux下叫TTY,每個TTY對就一個tty_struct結構。0.11版本一共三個,一個控制檯兩個串口。每個tty_struct有三個緩衝區,read_q,  write_q,  secondary 。
read_q保存原始的輸入字符隊列,write_q保存的是輸出的字符隊列,secondary裏面是輸入字符序列通過行規則處理後的字符序列。
tty_struct中的termios保存的是終端IO屬性等。這個結構通過tty_ioctl.c中tty_ioctl()來對tty進行相應的控制或設置。
避開非常瑣碎的行規則,從char_dev.c中函數rw_tty調用來看整個過程的粗略脈絡。 rw_tty(int rw,unsigned minor,char * buf,int count, off_t * pos)。檢測進程有無終端,有則根據rw調用tty_read 或者tty_write.
先看tty_read(minor,buf,count),對於要求讀取的字節數nr,在定時時間內,循環讀取secondary中的字符,直到讀到nr個爲止。如果secondary空了,  進程等待於secondary的等待隊列上。如果超時,則返回。

再看tty_write(minor,buf,count),如是tty寫緩衝write_q已經滿了,睡眠sleep_if_full (&tty->write_q); 對於要求寫字節數nr,循環拷貝到write_q中去。如果拷貝過程中write_q滿了或者已經拷貝完調用寫函數。沒拷貝完則切換進程。剩下的工作交給中斷處理程序去完成。

 

對於讀操作,當tty收到一個字符,比如串口收到一個字符或者是用戶按下鍵盤,系統將進入相應中斷程序。中斷程序對收到的字符進行處理,然後把字符放入對應tty的read_q中,調用do_tty_interrupt,do_tty_interrupt直接調用copy_to_cooked(tty).  copy_to_cooked(tty)把read_q的全部字符通過行規則處理。然後放到secondary隊列中去。如果tty回顯標誌置位,則把轉換後的字符也放到寫隊列write_q中,並調用tty->write (tty); 如果中ISIG 標誌置位,收到INTR、QUIT、SUSP 或DSUSP 字符時,需要爲進程產生相應的信號。最後喚醒等待該輔助緩衝隊列的進程(如果有的話)。wake_up (&tty->secondary.proc_list); 中斷返回。

對於寫操作,如果tty是控制檯,其tty寫操作爲con_write (tty),這個函數直接把write_q中所有字符按照格式寫到顯存中去或者調整光標。如果是串口,tty寫操作爲rs_write(tty);這個函數更簡單,打開串口發送緩衝空閒允許中斷位就返回。這樣,CPU會馬上收到一箇中斷,在中斷程序中,寫操作纔會真正進行。串口寫緩衝空中斷執行時先判斷寫隊列是否爲空,如果爲空,喚醒可能的等待隊列,並且禁止發送緩衝空中斷允許並中斷返回。如果不空,但是寫隊列字符數小於256,也喚醒可能的寫等待隊列,然後從寫隊列中取一個字符寫入串口發送寄存器。中斷返回。

2.3文件系統之緩衝管理fs/buffer.c

緩衝管理部分兩個作用,利用cache機制提供更高效的使用外部塊設備,給使用塊設備的其它程序提供簡單的接口如bread。使得上層程序對設備操作全部變成對緩衝塊的操作。給塊設備如軟硬盤提供一種cache機制。每個緩衝塊buffer_head都對應一個設備dev和邏輯塊號block,引用計數count,修改標誌dirt,有效標誌uptodate。 類比CPU,修改標誌與有效標誌是cache機制必需的,而dev和block號則相當於地址。緩衝管理負責設備數據塊與緩衝映射塊數據一致性。緩衝管理具體去調用設備驅動程序ll_rw_block().

Buffer.c主要提供的函數有申請、釋放緩存,同步(buffer與設備內容一致),讀取。而寫操作則總是在緩衝不足的情況下利用同步進行的。讀取的時候總是根據給定的dev和block先查找當前緩存中是否存在有效的對應塊,如果存在就不再訪問設備。否則取一個空閒緩衝,調用設備驅動ll_rw_block。

緩衝塊鏈表實現了hash鏈表和LRU算法,所有的緩衝塊都連接於鏈表中,鏈表頭總是空閒度最高的緩衝塊,鏈表尾則是最近剛申請的塊。查找空閒緩衝從頭開始比較空閒度,找最大的。這就實現了LRU算法。 所有具有相同hash值的緩衝塊連接於同一個hash_table[nr]項上。nr值由設備號和塊號經過一個hash算法得到。這樣查找速度會快好多倍。

所有對外設邏輯塊的讀寫都在這裏被轉化爲對緩衝塊的讀寫。每次讀寫前總是先根據設備號和邏輯塊號到hash_table[]表中查找hash鏈表,若已經存在且有效,則直接對緩衝讀寫。寫後要置修改標誌dirt。這樣當執行同步操作,或者getblk()找不到乾淨的空閒塊的時候會把所有dirt爲1的未被佔用(count=0)的緩衝塊寫入磁盤。

2.4 文件系統之文件系統底層操作。

文件底層操作。(bitmap.c,inode.c,namei.c,super.c,truncate.c,)這部分按照文件系統對硬盤等塊設備的使用規則,實現了相應規則的操作函數。文件系統把塊設備按邏輯塊管理,功能劃分:

引導塊

超級塊

i結點位圖塊區

邏輯塊位圖塊區

i結點塊區

數據塊區

    超級塊指明瞭整個文件系統各區段的信息。兩個位圖區分別指示i結點區和數據塊區的佔用和空閒狀況,i結點區中每個i結點都代表一個文件或者目錄。整個文件系統的根目錄在第1個i結點處。通過它可以找出任何路徑下的文件的i結點。

i結點指示一個文件或者目錄,一個i結點的內容主要是文件佔用的數據塊號。直接塊號,一、二次間接塊號提供了靈活的機制來線性地查找文件中一個數據塊對應在哪一個具體的物理塊上。目錄文件的數據塊內容是目錄項,它包含的所在目錄的全部文件和子目錄的信息。每個目錄項保存一個文件或者子目錄的inode號和文件名。

相應地,bitmap.c提供了i結點位圖,數據塊位圖的佔用和釋放操作。super.c實現對超級塊的讀定安裝卸載操作。inode.c實現是獲取指定nr的inode(iget(dev,nr)),寫回inode ( iput(inode) )等操作。

namei.c則實現了按照文件名來獲取inode的操作。從而提供了通過文件名來管理文件(inode)的方法。

這些操作之間的層次並不十分清晰,相互調用很多。注意塊設備是按塊爲最小單位訪問的,這些操作不過是按照文件系統對設備塊使用的定義對各個塊以及塊中的數據做解析和操作罷了。文件底層操作都貌似在訪問塊設備,但是卻僅僅調用了緩衝管理提供的接口。它們操作了內存。緩衝管理去實現設備的讀寫。比如在系統安裝根文件系統的時候,超級塊已經讀如緩衝,根據超級塊的信息,將i結點位圖塊,邏輯位圖塊讀入到內存緩衝中了。

下面對各個源文件的實現進行小結:

bitmap.c: 位圖操作,主要提供文件系統中i結點位圖,邏輯塊位圖相關操作,包括申請和釋放inode,申請和釋放block.首先文件系統的位圖塊在mount_root中已經緩存到buffer中,緩衝塊指針由超級塊s_zmap[],s_imap[]指向。所以申請釋放操作主要的一部分------對位圖相應位置位或者復位就變成對緩衝塊置位復位了,然後修改標誌dirt=1就行了。
new_block除了要對找到的空閒位置位外,還要申請一塊空閒緩衝(清0)並填申請塊的dev和block。置有效和修改標誌。這麼做其實就等於一個寫操作,即把申請的設備塊清0。(當然,可能申請後馬上就要寫這一塊,所以這麼做最高效了。)

truncate.c: 對文件(inode)長度清0。主要調用free_block對位圖進行操作。直接塊直接釋放,對一次間接塊上所有有效塊號釋放,然後再釋放一次間接塊。二次間接塊同理。

inode.c:   主要提供三個函數,iget,iput,bmap.  iget是獲取指定設備和i結點號的內存i結點。使用計數加1。主要調用read_inode(調用buffer管理部分)  ;iput是把一個內存i節點寫回到設備中去。使用計數減1。主要調用write_inode(調用buffer管理部分)
bmap是把文件塊號對應到設備塊號(邏輯塊號)中去。文件塊號是按直接塊,一直間接,二次間接順序計算的索引。邏輯塊號則是保存在它們裏面的塊號。有點像頁表,頁的線性地址對應文件塊號,頁的物理地址對應邏輯塊號。頁表項中的保存的地址就是頁物理地址。bmap有創建和不創建兩種方式。創建時會根據文件塊號給文件(inode)申請邏輯塊存放可能需要的一、二次間接塊和數據塊。

super.c:對文件系統超級塊的相關操作。如get_super,put_super,read_super,sys_mount,sys_umount;超級塊對應一個文件系統。
get_super(dev)在系統超級塊數組中查找並返回匹配的超級塊。
put_super(dev)釋放超級塊數組中超級塊信息,並釋放超級塊i結點,邏輯塊位圖佔用的緩衝區。
read_super(dev)先在超級塊數組中查找,有直接返回,沒有則先在超級塊數組找一空閒項。讀dev 1號塊,取得超級塊信息,如位圖佔多少塊,再讀位圖塊(i位圖,邏輯塊位圖)到緩衝中。設置完畢返回。
sys_mount(devname,dirname,rw_flag) 在目錄dirname上安裝devname設備的文件系統。取dirname和devname的i結點判斷二者都有效?然後讀dev超級塊read_super(dev),置超級塊安裝結點爲direname的i結點 sb->s_imount=dir_i. 置目錄i結點安裝標誌1。所以i結點的安裝標誌表明該目錄是否安裝了一個文件系統。而要知道安裝的文件系統的具體信息則要查找超級塊數組,看看哪一個超級塊的s_imount等於該i結點。。。

namei.c:提供文件路徑名到i結點的操作。大部函數參數都直接給出文件路徑名,所以它實現了通過文件名來管理文件系統的接口。如打開創建刪除文件、目錄,創建刪除文件硬連接等。
大部分函數的原理都差不多:調用get_dir取得文件最後一層目錄的i結點dir。如果是查找就調用find_entry從dir的數據塊中查找匹配文件名字符串的目錄項。這樣通過目錄項就取得了文件的i結點。如果是創建(sys_mknod)就申請一個空的inode,在dir的數據塊中找一個空閒的目錄項,把文件名和inode號填入目錄項。創建目錄的時候要特殊一些,要爲目錄申請一個數據塊,至少填兩個目錄項,.和..  (sys_mkdir)。
刪除文件和目錄的時候把要釋放i結點並刪除所在目錄的數據塊中佔用的目錄項。
打開函數open_namei()基本上實現了open()的絕大部分功能。它先按照上述過程通過文件路徑名查找 最後一層目錄i結點,在目錄數據塊中查找目錄頂。如果沒找到且有創建標誌,則創建新文件,申請一個空閒的inode和目錄項進行設置。 對於得到的文件inode,根據打開選項進行相應處理。成功則通過調用參數返回inode指針。
這個文件用得最多的功能函數莫過於namei();根據文件名返回i節點。
這裏任何對inode的操作都是通過iget,iput這類更底層的函數去實現,iget和iput所在的層次基於buffer管理和內存inode表的操作之上。

 

2.5 文件系統之文件數據訪問操作  這提供讀寫系統調用接口read,write。

主要包括文件:
block_dev.c:定義了塊設備文件的讀寫函數,block_write,block_read.
file_dev.c :定義正規文件讀寫函數。file_read,  file_write
pipe.c   :定義FIFO文件讀寫及管道系統調用。read_pipe, write_pipe, sys_pipe

char_dev.c :定義字符型設備訪問讀寫,rw_ttyx, rw_tty, rw_char. 最終都調用tty_read,tty_write
read_write.c:實現文件系統讀寫接口 read,write,lseek。
read,write的參數是文件句柄fd,這需要通過系統調用sys_open來獲取。函數根據進程task的filp[fd]指向的系統打開文件表項獲取inode、讀寫權限、當前讀寫位置等。由inode的類型(上面四種之一)調用相應讀寫函數(參看圖4)。對於正規文件,過程如下:由inode指向的內存inode項獲取文件在設備上的位置大小等信息。通過inode和bmap計算要讀取的文件數據在設備的邏輯塊號。通過bread讀數據塊,然後對緩衝塊進行讀寫。到此就不用管了。緩衝管理的作用真的是太神奇了。

2.6 文件系統高層操作&管理部分:

包括文件open.c,exec.c,stat.c,fcntl.c,ioctl.c 實現文件系統中對文件的管理和操作,如創建,打開,設置/讀取文件信息,執行一個文件程序。這個層次位於文件底層操作之上,調用底層函數去實現。

open.c: 定義了系統調用sys_ustat,sys_access,sys_chmod,sys_chdir,sys_chroot,sys_open,sys_close.參數基本上是文件名或者文件句柄。
可以分爲三類:修改inode屬性類(前3個),修改進程根/當前目錄,打開關閉文件。    
第一類通過namei找到i結點,修改相關屬性域,iput寫回設備。
第二類通過namei找到i結點,把進程task數據相應域設爲對應inode.
第三類打開時主要調用open_namei返回一個文件名對應的i結點,並設置系統打開文件表和進程打開文件表。返回文件句柄。關閉則清進程打開文件表項,處理對應的系統打開文件表項(引用減1),寫回i結點。

execv.c:主要一個函數do_execve.往往在系統執行完fork之後會調用execve簇函數去執行一個全新的程序。必須重新對進程空間進行初始化。
主要流程:找到執行程序inode,進行相應判斷(如如權限等),讀文件頭(第1個數據塊)信息。
如果是腳本文件,則取shell文件名和參數,以shell爲執行程序去執行該腳本文件,這時重新以shell爲文件名,以本腳本文件爲參數,執行上述過程。
根據文件頭信息得到文件各段長度,entry位置,修改進程任務結構中相應的數據。然後拷貝參數到進程空間末端,設置堆棧指針。
清空原進程空間佔用的頁目錄和頁表。 修改系統調用返回地址爲進程空間的起始地址。
該系統調用返回後,新進程執行第一條語句,會引起一個缺頁中斷。根據要求的線性地址和executable,在中斷中執行do_no_page進行共享或者需求加載。

stat.c : 系統調用sys_stat,sys_fstat.取文件狀態信息。

fcntl.c: 實現sys_dup,sys_dup2,sys_fcntl.
dup複製到從0開始找最小的空閒句柄。
dup2指定開始搜索的最小句柄。
fcntl主要根據flag參數不同。可以實現四方面的操作:複製文件句柄同dup,設/取close_on_exec標誌。設/取文件狀態和訪問模式。給文件上/解鎖。

ioctl.c:主要實現系統調用sys_ioctl,間接調用tty_ioctl.主要對終端設備進行命令控制、參數設置、狀態讀取等。

 

結束.

看這本書的剖析的linux代碼之前覺得LINUX很神祕,現在覺得親切多了。心裏面對內核的動作已經有了比較清晰的概念。但是還遠遠不足以運用到嵌入式中去,最新的內核與0.11相比,我覺得好像自己還是啥也不知道一樣,差得太多,顯得很陌生。下一步必須看看2.6版本的內核分析一類的書籍,瞭解最新的內核。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章