天天看點

《Linux核心設計的藝術:圖解Linux作業系統架構設計與實作原理》——3.3 輪轉到程序1執行

在分析程序1如何開始執行之前,先回顧一下程序0建立程序1的過程。

在3.1.3節中講解調用copy_process函數時曾強調過,當時為程序1設定的tss.eip就是程序0調用fork( )建立程序1時int 0x80中斷導緻的cpu硬體自動壓棧的ss、esp、eflags、cs、eip中的eip值,這個值指向的是int 0x80的下一行代碼的位置,即if (__res >= 0)。

前面講述的ljmp 通過cpu的任務門機制自動将程序1的tss的值恢複給cpu,自然也将其中的tss.eip恢複給cpu。現在cpu中的eip指向的就是fork中的if (__res >= 0)這一行,是以,程序1就要從這一行開始執行。

執行代碼如下:

回顧前面3.1.3節中的介紹可知,此時的__res值,就是程序1的tss中eax的值,這個值在3.1.3節中被寫死為0,即p->tss.eax = 0,是以,當執行到return (type) __res這一行時,傳回值是0,如圖3-15所示。

《Linux核心設計的藝術:圖解Linux作業系統架構設計與實作原理》——3.3 輪轉到程式1執行

傳回後,執行到main()函數中if (!fork( ))這一行,! 0為“真”,調用init()函數!

進入init()函數後,先調用setup()函數,執行代碼如下:

本章後續的内容都是setup( )函數實作的。這個函數的調用與fork( )、pause( )函數的調用類似;略有差別的是setup( )函數不是通過_syscall0( )而是通過_syscall1( )實作的;具體的實作過程基本類似,也是通過int 0x80、_system_call、call _sys_call_table(,%eax,4)、sys_setup( )。

提醒:前面pause( )函數的那個int 0x80中斷還沒有傳回,現在setup( )又産生了一個中斷。

3.3.1 程序1為安裝硬碟檔案系統做準備

這一節的内容涉及sys_setup( )的大部分代碼,包括從函數開始到調用rd_load( )之前的所有代碼;技術路線比較長,代碼很多,難度比較大,hash_table的部分尤其如此。但這部分代碼的目的卻很單一:為第5章将要講述的安裝硬碟檔案系統做準備。

這個過程大概經過3個步驟;

1)根據機器系統資料設定硬碟參數;

2)讀取硬碟引導塊;

3)從引導塊中擷取資訊。

1.程序1設定硬碟的hd_info

根據機器系統資料中的drive_info,如硬碟的柱面數、磁頭數、扇區數,設定核心的hd_info,如圖3-16所示。

《Linux核心設計的藝術:圖解Linux作業系統架構設計與實作原理》——3.3 輪轉到程式1執行
《Linux核心設計的藝術:圖解Linux作業系統架構設計與實作原理》——3.3 輪轉到程式1執行

具體的執行代碼如下:

2.讀取硬碟的引導塊到緩沖區

在linux 0.11中,硬碟最基礎的資訊就是分區表,其他資訊都可以從這個資訊引導出來,這個資訊所在的塊就是引導塊。一塊硬碟隻有唯一的一個引導塊,即硬碟的0号邏輯塊。引導塊有兩個扇區,真正有用的是第一個扇區。我們設定計算機隻有一塊硬碟。下面把硬碟的引導塊讀入緩沖區,以便後續程式解讀引導塊中的資訊。這個工作通過調用bread( )函數實作,bread( )可以了解為block read。

進入bread( )函數後,先調用getblk( )函數,在緩沖區中申請一個空閑的緩沖塊。

申請空閑緩沖塊的主要步驟,在圖3-17中已有形象的說明。以下将結合代碼來深入分析這一過程。

《Linux核心設計的藝術:圖解Linux作業系統架構設計與實作原理》——3.3 輪轉到程式1執行

在getblk( )函數中,先調用get_hash_table( )函數查找哈希表,檢索此前是否有程式把現在要讀的硬碟邏輯塊(相同的裝置号和塊号)已經讀到緩沖區。如果已經讀到緩沖區,那就不用再費勁從硬碟上讀取,直接用現成的,如圖3-17中的第一步所示。使用哈希表進行查詢的目的是提高查詢速度。

進入get_hash_table( )函數後,調用find_buffer( )函數查找緩沖區中是否有指定裝置号、塊号的緩沖塊。如果能找到指定緩沖塊,就直接用。

現在是第一次使用緩沖區,緩沖區中不可能存在已讀入的緩沖塊,也就是說hash_table中沒有挂接任何節點,find_buffer( )傳回的一定是null。

從find_buffer( )、get_hash_table( )函數退出後,傳回getblk( )函數,在空閑表中申請一個新的空閑緩沖塊。現在所有緩沖塊都是綁定在空閑表中的,是以要在空閑表中申請新的緩沖塊,如圖3-17中的第二步所示。

申請到緩沖塊後,對它進行初始化設定,并将這個空閑塊挂接到hash_table上。

《Linux核心設計的藝術:圖解Linux作業系統架構設計與實作原理》——3.3 輪轉到程式1執行
《Linux核心設計的藝術:圖解Linux作業系統架構設計與實作原理》——3.3 輪轉到程式1執行

挂接hash_table的執行代碼如下,分步示意圖如圖3-19所示。

《Linux核心設計的藝術:圖解Linux作業系統架構設計與實作原理》——3.3 輪轉到程式1執行
《Linux核心設計的藝術:圖解Linux作業系統架構設計與實作原理》——3.3 輪轉到程式1執行

執行完getblk( )函數後,傳回bread( )函數。

3.将找到的緩沖塊與請求項挂接

傳回bread( )函數後,調用ll_rw_block( )這個函數,将緩沖塊與請求項結構挂接,如圖3-20所示。

《Linux核心設計的藝術:圖解Linux作業系統架構設計與實作原理》——3.3 輪轉到程式1執行

進入ll_rw_block( )函數後,先判斷緩沖塊對應的裝置是否存在或這個裝置的請求項函數是否挂接正常。如果存在且正常,說明可以操作這個緩沖塊,調用make_request( )函數,準備将緩沖塊與請求項建立關系,執行代碼如下:

程序1繼續執行,進入make_request( )函數後,先要将這個緩沖塊加鎖,目的是保護這個緩沖塊在解鎖之前将不再被任何程序操作,這是因為這個緩沖塊現在已經被使用,如果此後再被挪作他用,裡面的資料就會發生混亂。如圖3-20右邊的緩沖塊buffer_head所示,其中選中的那個緩沖塊對應的buffer_head已加鎖。

之後,在請求項結構中,申請一個空閑請求項,準備與這個緩沖塊相挂接。值得注意的是,如果是讀請求,則從整個請求項結構的最末端開始尋找空閑請求項;如果是寫請求,則從整個結構的2/3處,申請空閑請求項。這是因為從使用者使用系統的心理角度講,使用者更希望讀取的資料能更快地顯現出來,是以給讀取操作以更大的空間。這時候,請求項結構是第一次被使用,而且是讀請求,是以在請求項結構的末端找到一個空閑的請求項,如圖3-20中的request[32]結構所示,其中的最後一項已被選中。之後,緩沖塊與請求項正式挂接,并對這個請求項各個成員進行初始化。

調用add_request( )函數,向請求項隊列中加載該請求項,進入add_request( )後,先對目前硬碟的工作情況進行分析,然後設定該請求項為目前請求項,并調用硬碟請求項處理函數(dev->request_fn)( ),即 do_hd_request( )函數去給硬碟發送讀盤指令。圖3-21中給出了請求項管理結構與do_hd_request( )函數的對應關系。

《Linux核心設計的藝術:圖解Linux作業系統架構設計與實作原理》——3.3 輪轉到程式1執行

4.讀硬碟

進入do_hd_request( )函數去執行,為讀盤做最後準備工作。具體的準備過程如圖3-22所示。

《Linux核心設計的藝術:圖解Linux作業系統架構設計與實作原理》——3.3 輪轉到程式1執行

先通過對目前請求項資料成員的分析,解析出需要操作的磁頭、扇區、柱面、操作多少個扇區……之後,建立硬碟讀盤必要的參數,将磁頭移動到0柱面,如圖3-22中第二步所示;之後,針對指令的性質(讀/寫)給硬碟發送操作指令。現在是讀操作(讀硬碟的引導塊),是以接下來要調用hd_out( )函數來下達最後的硬碟操作指令。注意看最後兩個實參,win_read表示接下來要進行讀操作,read_intr( )是讀盤操作對應的中斷服務程式,是以要提取它的函數位址,準備挂接,這一動作反映在圖3-22中的第三步。請注意,這是通過hd_out( )函數實作的,讀盤請求就挂接read_intr( );如果是寫盤,那就不是read_intr( ),而是write_intr( )了。

進入hd_out( )函數中去執行讀盤的最後一步:下達讀盤指令,如圖3-23中第一步所示。

《Linux核心設計的藝術:圖解Linux作業系統架構設計與實作原理》——3.3 輪轉到程式1執行

其中,do_hd = intr_addr;這一行是把讀盤服務程式與硬碟中斷操作程式相挂接,這裡面的do_hd是system_call.s中_hd_interrupt下面xchgl _do_hd,%edx這一行所描述的内容。

現在要做讀盤操作,是以挂接的就是實參read_intr,如果是寫盤,挂接的就應該是write_intr( )函數。

下達讀盤指令!

硬碟開始将引導塊中的資料不斷讀入它的緩存中,同時,程式也傳回了,将會沿着前面調用的反方向,即hd_out( )函數、do_hd_request( )函數、add_request( )函數、make_request( )函數、ll_rw_block( )函數,一直傳回bread( )函數中。

現在,硬碟正在繼續讀引導塊。如果程式繼續執行,則需要對引導塊中的資料進行操作。但這些資料還沒有從硬碟中讀完,是以調用wait_on_buffer( )函數,挂起等待!

進入wait_on_buffer( )函數後,判斷剛才申請到的緩沖塊是否被加鎖。現在,緩沖塊确實加鎖了,調用sleep_on( )函數。如圖3-24中的第二步所示。執行代碼如下:

《Linux核心設計的藝術:圖解Linux作業系統架構設計與實作原理》——3.3 輪轉到程式1執行

進入sleep_on( )函數後,将程序1設定為不可中斷等待狀态,如圖3-24中第三步所示,程序1挂起;然後調用schedule( )函數,準備程序切換,執行代碼如下:

5.等待硬碟讀資料時,程序排程切換到程序0執行

進入schedule( )函數後,切換到程序0去執行。圖3-25給出了切換過程的主要步驟。

《Linux核心設計的藝術:圖解Linux作業系統架構設計與實作原理》——3.3 輪轉到程式1執行

具體執行步驟在3.2節中已經說明。但第二次周遊task[64]的時候,與3.2節中執行的結果不一樣。此時隻有兩個程序,程序0的狀态是可中斷等待狀态,程序1的狀态也已經剛剛被設定成了不可中斷等待狀态。正常的程序切換條件是,剩餘時間片最多且必須是就緒态,即代碼“if ((p)->state == task_running && (p)->counter > c)”給出的條件。現在兩個程序都不是就緒态,按照正常的條件無法切換程序,沒有程序可以執行。

這是一個非常尴尬的狀态。

作業系統的設計者對這種狀态的解決方案是:強行切換到程序0!

注意:c的值将仍然是-1,是以next 仍然是0,這個next就是要切換到程序的程序号。可以看出,如果沒有合适的程序,next的數值将永遠是0,就會切換到程序0去執行!

調用switch_to(0)執行代碼如下:

switch_to (0)執行完後,已經切換到程序0去執行。前面3.2節中已經說明,當時程序0切換到程序1時,是從switch_to (1)的"ljmp %0nt"這一行切換走的,tss中儲存當時的cpu所有寄存器的值,其中cs、eip指向的就是它的下一行,是以,現在程序0要從"cmpl %%ecx,_last_task_used_mathnt” 這行代碼開始執行,如圖3-25中的第三步所示。

将要執行的代碼如下:

回顧3.2節,當時程序0切換到程序1是從pause( )、sys_pause( )、schedule( )、switch_to(1)這個調用路線執行過來的。現在,switch_to(1)後半部分執行完畢後,就應該傳回sys_pause( )、for(;;)pause( )中執行了。

pause這個函數将在for(;;)這個循環裡面被反複調用,是以,會繼續調用schedule函數進行程序切換。而再次切換的時候,由于兩個程序還都不是就緒态,按照前面講述過的理由,當所有程序都挂起的時候,核心會執行switch_to強行切換到程序0。

現在,switch_to中情況有些變化,"cmpl %%ecx,_currentnt” “je 1fnt”的意思是:如果切換到的程序就是目前程序,就跳轉到下面的“1:”處直接傳回。此時目前程序正是程序0,要切換到的程序也是程序0,正好符合這個條件。

是以,又回到程序0(注意:不是切換到程序0)。

循環執行這個動作,如圖3-26所示。

從這裡可以看出作業系統的設計者為程序0設計的特殊職能:當所有程序都挂起或沒有任何程序執行的時候,程序0就會出來維持作業系統的基本運轉,等待挂起的程序具備可執行的條件。業内人士也稱程序0為怠速程序,很像維持汽車等待駕駛員踩油門的怠速狀态那樣維護計算機的怠速狀态。

注意:硬碟的讀寫速度遠低于cpu執行指令的速度(2~3個量級)。現在,硬碟仍在忙着把指定的資料讀到它的緩存中……

《Linux核心設計的藝術:圖解Linux作業系統架構設計與實作原理》——3.3 輪轉到程式1執行

6.程序0執行過程中發生硬碟中斷

循環執行了一段時間後,硬碟在某一時刻把一個扇區的資料讀出來了,産生硬碟中斷。cpu接到中斷指令後,終止正在執行的程式,終止的位置肯定是在pause( )、sys_pause( )、schedule( )、switch_to (n)循環裡面的某行指令處,如圖3-27中的第一步所示。

《Linux核心設計的藝術:圖解Linux作業系統架構設計與實作原理》——3.3 輪轉到程式1執行

然後轉去執行硬碟中斷服務程式。執行代碼如下:

别忘了中斷會自動壓棧ss、esp、eflags、cs、eip,硬碟中斷服務程式的代碼接着将一些寄存器的資料壓棧以儲存程式的中斷處的現場。之後,執行_do_hd處的讀盤中斷處理程式,對應的代碼應該是call *%edx這一行。這個edx裡面是讀盤中斷處理程式read_intr的位址,參看hd_out( )函數的講解及代碼注釋。

read_intr( )函數會将已經讀到硬碟緩存中的資料複制到剛才被鎖定的那個緩沖塊中(注意:鎖定是阻止程序方面的操作,而不是阻止外設方面的操作),這時1個扇區256字(512位元組)的資料讀入前面申請到的緩沖塊,如圖3-27中的第二步所示。執行代碼如下:

但是,引導塊的資料是1024位元組,請求項要求的也是1024位元組,現在僅讀出了一半,硬碟會繼續讀盤。與此同時,在得知請求項對應的緩沖塊資料沒有讀完的情況下,核心将再次把read_intr( )綁定在硬碟中斷服務程式上,以待下次使用,之後中斷服務程式傳回。

程序1仍處在被挂起狀态,pause( )、sys_pause( )、schedule( )、switch_to(0)循環從剛才硬碟中斷打斷的地方繼續循環,硬碟繼續讀盤……

整個過程如圖3-28所示。

《Linux核心設計的藝術:圖解Linux作業系統架構設計與實作原理》——3.3 輪轉到程式1執行

又過了一段時間後,硬碟剩下的那一半資料也讀完了,硬碟産生中斷,讀盤中斷服務程式再次響應這個中斷,進入read_intr( )函數後,仍然會判斷請求項對應的緩沖塊的資料是否讀完了,對應代碼如下:

這次已經将請求項要求的資料量全部讀完了,經檢驗确認完成後,不執行if裡面的内容了,跳到end_request( )函數去執行,如圖3-29中read_intr( )這個函數所示。

《Linux核心設計的藝術:圖解Linux作業系統架構設計與實作原理》——3.3 輪轉到程式1執行

進入end_request( )後,由于此時緩沖塊的内容已經全部讀進來了,将這個緩沖塊的更新标志b_uptodate置1,說明它可用了,執行代碼如下:

之後,調用unlock_buffer( )函數為緩沖塊解鎖。在unlock_buffer( )函數中調用wake_up( )函數,将等待這個緩沖塊解鎖的程序(程序1)喚醒(設定為就緒态),并對剛剛使用過的請求項進行處理,如将它對應的請求項設定為空閑……執行代碼如下:

硬碟中斷處理結束,也就是載入硬碟引導塊的工作結束後,計算機在pause( )、sys_pause( )、schedule( )、switch_to(0)循環中繼續執行,如圖3-29中第三步所示。

7.讀盤操作完成後,程序排程切換到程序1執行

現在,引導塊的兩個扇區已經載入核心的緩沖塊,程序1已經處于就緒态。注意:雖然程序0一直參與循環運作,但它是非就緒态。現在隻有程序0和程序1,當循環執行到schedule函數時就會切換程序1去執行。該過程如圖3-30所示。

《Linux核心設計的藝術:圖解Linux作業系統架構設計與實作原理》——3.3 輪轉到程式1執行

切換到程序1後,程序1從下面的代碼繼續執行:

可以看出,所有程序間的切換都是這個模式。

程序1是從"ljmp %0nt" 切換走的,是以現在執行它的下一行。現在,傳回切換的發起者sleep_on( )函數中,并最終傳回bread( )函數中。在bread( )函數中判斷緩沖塊的b_uptodate 标志已被設定為1,直接傳回,bread( )函數執行完畢。執行代碼如下:

回到sys_setup函數繼續執行,處理硬碟引導塊載入緩沖區後的事務。緩沖塊裡面裝載着硬碟的引導塊的内容,先來判斷硬碟資訊有效标志'55aa'。如果第一個扇區的最後2位元組不是'55aa',就說明這個扇區中的資料是無效的(我們假設引導塊的資料沒有問題)。執行代碼如下:

之後,利用從引導塊中采集到的分區表資訊來設定hd[],如圖3-31所示。

《Linux核心設計的藝術:圖解Linux作業系統架構設計與實作原理》——3.3 輪轉到程式1執行

讀引導塊的緩沖塊已經完成使命,調用brelse( )函數釋放,以便以後繼續程式使用。

根據硬碟分區資訊設定hd[],為第5章安裝硬碟檔案系統做準備的工作都已完成。下面,我們将介紹程序1用虛拟盤替代軟碟使之成為根裝置,為加載根檔案系統做準備。

3.3.2 程序1格式化虛拟盤并更換根裝置為虛拟盤

第2章的2.3節設定了虛拟盤空間并初始化。那時的虛拟盤隻是一塊“白盤”,尚未經過類似“格式化”的處理,還不能當做一個塊裝置使用。格式化所用的資訊就在boot作業系統的軟碟上。第1章講解過,第一個扇區是bootsect,後面4個扇區是setup,接下來的240個扇區是包含head的system子產品,一共有245個扇區。“格式化”虛拟盤的資訊從256扇區開始。

下面,程序1調用rd_load( )函數,用軟碟上256以後扇區中的資訊“格式化”虛拟盤,使之成為一個塊裝置。

進入rd_load( )函數後,調用breada( )函數從軟碟預讀一些資料塊,也就是“格式化”虛拟盤需要的引導塊、超級塊。

注意:現在根裝置是軟碟。

breada( )和bread( )函數類似,不同點在于可以把一些連續的資料塊都讀進來,一共三塊,分别是257、256和258,其中引導塊在256(盡管引導塊并未實際使用)、超級塊在257中。從軟碟上讀取資料塊與bread讀硬碟上的資料塊原理基本一緻,具體情況參看3.3.1節的講解。讀取完成後的狀态如圖3-32所示。可以看出3個連續的資料塊被讀入了高速緩沖區的緩沖塊中,其中,超級塊用紅色框标注。

《Linux核心設計的藝術:圖解Linux作業系統架構設計與實作原理》——3.3 輪轉到程式1執行

之後,分析超級塊資訊,包括判斷檔案系統是不是minix檔案系統、接下來要載入的根檔案系統的資料塊數會不會比整個虛拟盤區都大……這些條件都通過,才能繼續加載根檔案系統。分析完畢,釋放緩沖塊。

整個過程如圖3-33所示。

《Linux核心設計的藝術:圖解Linux作業系統架構設計與實作原理》——3.3 輪轉到程式1執行

接下來調用breada( )函數,把與檔案系統相關的内容,從軟碟上拷貝到虛拟盤中,然後及時釋放緩沖塊,最終完成“格式化”這個過程,如圖3-34所示。

複制結束後,将虛拟盤設定為根裝置。

《Linux核心設計的藝術:圖解Linux作業系統架構設計與實作原理》——3.3 輪轉到程式1執行

下面将要介紹在虛拟盤這個根裝置上加載根檔案系統。

3.3.3 程序1在根裝置上加載根檔案系統

作業系統中加載根檔案系統涉及檔案、檔案系統、根檔案系統、加載檔案系統、加載根檔案系統這幾個概念。為了更容易了解,這裡我們隻讨論塊裝置,也就是軟碟、硬碟、虛拟盤(有關塊裝置的詳細讨論請閱讀第5、7章)。

作業系統中的檔案系統可以大緻分成兩部分;一部分在作業系統核心中,另一部分在硬碟、軟碟、虛拟盤中。

檔案系統是用來管理檔案的。檔案系統用i節點來管理檔案,一個i節點管理一個檔案,i節點和檔案一一對應。檔案的路徑在作業系統中由目錄檔案中的目錄項管理,一個目錄項對應一級路徑,目錄檔案也是檔案,也由i節點管理。一個檔案挂在一個目錄檔案的目錄項上,這個目錄檔案根據實際路徑的不同,又可能挂在另一個目錄檔案的目錄項上。一個目錄檔案有多個目錄項,可以形成不同的路徑。效果如圖3-35所示。

《Linux核心設計的藝術:圖解Linux作業系統架構設計與實作原理》——3.3 輪轉到程式1執行

所有的檔案(包括目錄檔案)的i節點最終挂接成一個樹形結構,樹根i節點就叫這個檔案系統的根i節點。一個邏輯裝置(一個實體裝置可以分成多個邏輯裝置,比如實體硬碟可以分成多個邏輯硬碟)隻有一個檔案系統,一個檔案系統隻能包含一個這樣的樹形結構,也就是說,一個邏輯裝置隻能有一個根i節點。

加載檔案系統最重要的标志,就是把一個邏輯裝置上的檔案系統的根i節點,關聯到另一個檔案系統的i節點上。具體是哪一個i節點,由作業系統的使用者通過mount指令決定。

邏輯效果如圖3-36所示。

《Linux核心設計的藝術:圖解Linux作業系統架構設計與實作原理》——3.3 輪轉到程式1執行

另外,一個檔案系統必須挂接在另一個檔案系統上,按照這個設計,一定存在一個隻被其他檔案系統挂接的檔案系統,這個檔案系統就叫根檔案系統,根檔案系統所在的裝置就叫根裝置。

别的檔案系統可以挂在根檔案系統上,根檔案系統挂在哪呢?

挂在super_block[8]上。

linux 0.11作業系統中隻有一個super_block[8],每個數組元素是一個超級塊,一個超級塊管理一個邏輯裝置,也就是說作業系統最多隻能管理8個邏輯裝置,其中隻有一個根裝置。加載根檔案系統最重要的标志就是把根檔案系統的根i節點挂在super_block[8]中根裝置對應的超級塊上。

可以說,加載根檔案系統有三個主要步驟:

1)複制根裝置的超級塊到super_block[8]中,将根裝置中的根i節點挂在super_block[8]中對應根裝置的超級塊上。

2)将駐留緩沖區中16個緩沖塊的根裝置邏輯塊位圖、i節點位圖分别挂接在super_block[8]中根裝置超級塊的s_zmap[8]、 s_imap[8]上。

3)将目前程序的pwd、root指針指向根裝置的根i節點。

加載根檔案系統和安裝硬碟檔案系統完成後的總體效果如圖3-37所示。

《Linux核心設計的藝術:圖解Linux作業系統架構設計與實作原理》——3.3 輪轉到程式1執行

程序1通過調用mount_root( )函數實作在根裝置虛拟盤上加載根檔案系統。執行代碼如下:

1.複制根裝置的超級塊到super_block[8]中

進入mount_root( )函數後,初始化記憶體中的超級塊super_block[8],将每一項所對應的裝置号加鎖标志和等待它解鎖的程序全部設定為0。系統隻要想和任何一個裝置以檔案的形式進行資料互動,就要将這個裝置的超級塊存儲在super_block[8]中,這樣可以通過super_block[8]擷取這個裝置中檔案系統的最基本資訊,根裝置中的超級塊也不例外,如圖3-38

所示。

《Linux核心設計的藝術:圖解Linux作業系統架構設計與實作原理》——3.3 輪轉到程式1執行

前面的rd_load( )函數已經“格式化”好虛拟盤,并設定為根裝置。接下來調用read_super( )函數,從虛拟盤中讀取根裝置的超級塊,複制到super_block[8]中。

在read_super( )函數中,先檢測這個超級塊是不是已經被讀進super_block[8]中了。如果已經被讀進來了,則直接使用,不需要再加載一次了。這與本章3.3.1節中先通過哈希表來檢測緩沖塊是否已經存在的道理是一樣的。

因為此前沒有加載過根檔案系統,是以要在super_block[8]中申請一項。從圖3-39中可以看出,此時找到的是super_block[8]結構中的第一項。然後進行初始化并加鎖,準備把根裝置的超級塊讀出。

《Linux核心設計的藝術:圖解Linux作業系統架構設計與實作原理》——3.3 輪轉到程式1執行

對應的代碼如下:

調用bread( )函數,把超級塊從虛拟盤上讀進緩沖區,并從緩沖區複制到super_block[8]的第一項。 bread( )函數在3.3.1節中已經說明。這裡有一點差別,在3.3.1節中提到,如果給硬碟發送操作指令,則調用do_hd_request( )函數,而此時操作的是虛拟盤,是以要調用do_rd_request( )函數。值得注意的是,虛拟盤雖然被視為外設,但它畢竟是記憶體裡面一段空間,并不是實際的外設,是以,調用do_rd_request( )函數從虛拟盤上讀取超級塊,不會發生類似硬碟中斷的情況。

超級塊複制進緩沖塊以後,将緩沖塊中的超級塊資料複制到super_block[8]的第一項。從現在起,虛拟盤這個根裝置就由super_block[8]的第一項來管理,之後調用brelse( )函數釋放這個緩沖塊,如圖3-40所示。

《Linux核心設計的藝術:圖解Linux作業系統架構設計與實作原理》——3.3 輪轉到程式1執行

初始化super_block[8]中的虛拟盤超級塊中的i節點位圖s_imap、邏輯塊位圖s_zmap,并把虛拟盤上i節點位圖、邏輯塊位圖所占用的所有邏輯塊讀到緩沖區,将這些緩沖塊分别挂接到s_imap[8]和s_zmap[8]上。由于對它們的操作會比較頻繁,是以這些占用的緩沖塊并不被釋放,它們将常駐在緩沖區内。

如圖3-41所示,超級塊通過指針與s_imap和s_zmap實作挂接。

《Linux核心設計的藝術:圖解Linux作業系統架構設計與實作原理》——3.3 輪轉到程式1執行

2.将根裝置中的根i節點挂在super_block[8]中根裝置超級塊上

回到mount_root( )函數中,調用iget( )函數,從虛拟盤上讀取根i節點。根i節點的意義在于,通過它可以到檔案系統中任何指定的i節點,也就是能找到任何指定的檔案。

進入iget( )函數後,作業系統從i節點表inode_table[32]中申請一個空閑的i節點位置(inode_table[32]是作業系統用來控制同時打開不同檔案的最大數)。此時應該是首個i節點。對這個i節點進行初始化設定,其中包括該i節點對應的裝置号、該i節點的節點号……

圖3-42中給出了根目錄i節點在核心i節點表中的位置。

《Linux核心設計的藝術:圖解Linux作業系統架構設計與實作原理》——3.3 輪轉到程式1執行

對應代碼如下:

在read_inode( )函數中,先給inode_table[32]中的這個i節點加鎖。在解鎖之前,這個i節點就不會被别的程式占用。之後,通過該i節點所在的超級塊,間接地計算出i節點所在的邏輯塊号,并将i節點所在的邏輯塊整體讀出,從中提取這個i節點的資訊,載入剛才加鎖的i節點位置上,如圖3-43所示,注意inode_table[32]中的變化。最後,釋放緩沖塊并将鎖定的i節點解鎖。

《Linux核心設計的藝術:圖解Linux作業系統架構設計與實作原理》——3.3 輪轉到程式1執行

執行的代碼如下:

回到iget( )函數,将inode指針傳回給mount_root( )函數,并賦給mi指針。

下面是加載根檔案系統的标志性動作:

将inode_table[32]中代表虛拟盤根i節點的項挂接到super_block[8]中代表根裝置虛拟盤的項中的s_isup 、s_imount指針上。這樣,作業系統在根裝置上可以通過這裡建立的關系,一步步地把檔案找到。

3.将根檔案系統與程序1關聯

對程序1的tast_struct中與檔案系統i節點有關的字段進行設定,将根i節點與目前程序(現在就是程序1)關聯起來,如圖3-44所示。

《Linux核心設計的藝術:圖解Linux作業系統架構設計與實作原理》——3.3 輪轉到程式1執行

得到了根檔案系統的超級塊,就可以根據超級塊中“邏輯塊位圖”裡記載的資訊,計算出虛拟盤上資料塊的占用與空閑情況,并将此資訊記錄在3.3.3節中提到的駐留在緩沖區中“裝載邏輯塊位圖資訊的緩沖塊中”。執行代碼如下:

到此為止,sys_setup( )函數就全都執行完畢了。因為這個函數也是由于産生軟中斷才被調用的,是以傳回system_call中執行,之後會執行ret_from_sys_call。這時候的目前程序是程序1,是以下面将調用do_signal( )函數(隻要目前程序不是程序0,就要執行到這裡),對目前程序的信号位圖進行檢測,執行代碼如下:

現在,目前程序(程序1)并沒有接收到信号,調用do_signal( )函數并沒有實際的意義。

至此,sys_setup( )的系統調用結束,程序1将傳回3.3節中講到的代碼的調用點,準備下面代碼的執行。

至此,程序0建立程序1,程序1為安裝硬碟檔案系統做準備、“格式化”虛拟盤并用虛拟盤取代軟碟為根裝置、在虛拟盤上加載根檔案系統的内容講解完畢。