欧美性猛交XXXX免费看蜜桃,成人网18免费韩国,亚洲国产成人精品区综合,欧美日韩一区二区三区高清不卡,亚洲综合一区二区精品久久

打開(kāi)APP
userphoto
未登錄

開(kāi)通VIP,暢享免費電子書(shū)等14項超值服

開(kāi)通VIP
內核代碼學(xué)習==>深入介紹Linux內核(九)

深入介紹Linux內核(九)

第五章


5.5 Linux的系統呼叫

5.5.1系統呼叫介面

系統呼叫(通常稱(chēng)為syscalls)是Linux內核與上層應用程式進(jìn)行交互通信的唯一介面,參見(jiàn)圖5-4所示。從對中斷機制的說(shuō)明可知,用戶(hù)程式透過(guò)直接或間接(透過(guò)程式庫函數)呼叫中斷int 0x80,並在eax寄存器中指定系統呼叫功能號,即可使用內核資源,包括系統硬件資源。 不過(guò)通常應用程式都是使用具有標批介面定義的 C 函數庫中的函數間接地使用內核的系統呼叫,見(jiàn)圖5-19所示。






通常系琉呼叫使用函數形式進(jìn)行呼叫,因此可帶有一個(gè)或多個(gè)參數。對於系統呼叫執行的結果,它會(huì )在返回值中表示出來(lái)。通常負值表示錯誤,而0則表示成功,在出錯的情況下,錯誤的類(lèi)型碼被存放在全域變數errno中。透過(guò)呼叫程式庫函數perror( ),我們可以列印出該錯誤碼對應應的出錯字串資訊。

在linux內核中,每個(gè)系統呼叫都具有唯一的一個(gè)系統呼叫功能號。這些功能號定義在當include/unistd.h中第62行開(kāi)始處。例如,write系統呼叫的功能號是4,定義為符號--NR_write這些系統。這些系統呼叫功能號實(shí)際上對應於include/linux/sys.h中定義的系統呼叫處理程式指標陣列表sys_call_table[ ]中項的索引值。因此,write( )系統呼叫的處理程式指標就位于該陣列的項4處。

當我們想在自己的程式中使用這些系統呼叫符,需要像下面所示在包括進(jìn)檔“”之前定義符號“__LIBRARY__”。

#define__LIBRARY__
#include

另外,我們從sys_call_table[ ]中可以看出,內核中所有系統呼叫處理函數的名稱(chēng)基本上都是以符號‘sys_’開(kāi)始的。例如系統呼叫read()在內核原始碼中的實(shí)現函數就是sys_read( )。


5.5.2系統呼叫處理過(guò)程

當應用程式經(jīng)過(guò)程式庫函數向內核發(fā)出一個(gè)中斷呼叫int 0x80時(shí),就開(kāi)始執行一個(gè)系統呼叫。其中寄存器eax中存放著(zhù)系統呼叫號,而攜帶的參數可依次存放在寄存器ebx、ecx和edx中。因此Linux 0.12內核中用戶(hù)程式能夠向內核最多直接傳遞三個(gè)參數,當然也可以不帶參數。處理系統呼叫中斷int 0x80的過(guò)程是程式kernel/system_call.s中的system_call。


為了方便執行系統呼叫,內核原始碼在include/unistd.h檔(150-200行)中定義了巨集函數_syscalln( ) ,其中n代表攜帶的參數個(gè)數,可以分別0至3。因此最多可以直接傳遞3個(gè)參數。若需睪傳遞大塊資料給內核,則可以傳遞這塊資料的指標值。例如對於read()系統呼叫,其定義是:

int read(int fd,char *buf, int n );

若我們在用戶(hù)程式中直接執行對應的系統呼叫,那麼該系統呼叫的巨集的形式為:

#define__LIBRARY__
#include
_syscall3(int, read, int, fd, char *, buf, int, n)

因此我們可以在用戶(hù)程式直接使用上面的_syscall3( )來(lái)執行一個(gè)系統呼叫read( ),而不用透過(guò)C函數庫作仲介。實(shí)際上C函數庫中函數最終呼叫系統呼叫的形式和這裡給出的完全一樣。

對于include/unistd.h中給出的每個(gè)系統呼叫巨集,都有2+2*n個(gè)參數。其中第1個(gè)參數對應系統呼叫返回值的類(lèi)型;第2個(gè)參數是系統呼叫的名稱(chēng);隨后是系統呼叫所攜帶參數的類(lèi)型 名稱(chēng)。這個(gè)巨集會(huì )被擴展成包含內嵌組合語(yǔ)句的C函數,見(jiàn)如下所示。

int read(int fd,char *buf, int n)
{
long__res;
__asm__volatile (
“int$0x80”
:“=a” ( __res)
: “”(__NR_read),“b”((long) (fd)),“c”((1ong) (buf)),“d”((1ong) (n)));
if ( __res>=0)
return int __res;
errno=- __res;
return -1;
}

可以看出,這個(gè)巨集經(jīng)過(guò)展開(kāi)就是一個(gè)讀取作業(yè)系統呼叫的具體實(shí)現。其中使用了嵌入組合語(yǔ)句以功能號_ _NR_read (3)執行了Linux的系統中斷呼叫0x80。該中斷呼叫在eax(_ _res )寄存器中返回了實(shí)際讀取的位元組數。若返回的值小于0,則表示此次讀取操作出錯,于是將出錯號反轉后存入全域變數errno中,并向呼叫程式返回-1值。

如果有某個(gè)系統呼叫需要多於3個(gè)參數,那么內核通常採用的方法是直接把這些參數作為一個(gè)參數緩沖區塊,並把這個(gè)緩沖區塊的指標作為一個(gè)參數傳遞給內核。因此對於多於3個(gè)參數的系統呼叫,我們只需要使用帶一個(gè)參數的巨集_syscalll( ),把第一個(gè)參數的指標傳遞給內核即可。例如,select( )函數系統呼叫具有5個(gè)參數,但我們只需傳遞其第l個(gè)參熟的指標,參見(jiàn)對fs/select.c程式的說(shuō)明。

當進(jìn)入內核中的系統呼叫處理程式kernel/sys_call.s后,system_call的代碼會(huì )寫(xiě)先檢查eax中的系統呼叫功能號是否在有效系統呼叫號范圍內,然后根據sys_call_table[ ]函數指標表呼叫執行相應的系統呼叫處理程式。

call_sys_call_table(, %eax, 4) //kernel/sys_call.s第99行。

這句組合語(yǔ)句運算元的含義是間接呼叫地址在_sys_call_table + %eax * 4處的函數。由於sys_call_table[ ]指標每項4 立元組,因此這里需要給系統呼叫功能號乘上4。然后用所得到的值從表中獲取被呼叫處理函數的位址。

5.5.3Linux系統呼叫的參數傳遞方式

關(guān)于Linux用戶(hù)行程向系統中斷呼叫過(guò)程傳遞參數方面,Linux系統使用了通用寄存器傳遞方法,例如寄存器ebx、ecx和edx。這種使用寄存器傳遞參數方法的一個(gè)明顯優(yōu)點(diǎn)就是:當進(jìn)入系統中斷服務(wù)程式而保存寄存器值時(shí),這些傳遞參數的寄存器也被自動(dòng)地放在了內核態(tài)堆棧上,因此用不著(zhù)再專(zhuān)門(mén)對傳遞參數的寄存器進(jìn)行特殊處理。這種方法是Linus 當時(shí)所知的最簡(jiǎn)單最快速的參數傳遞方法。另外還有一種使用Intel CPU提供的系統呼叫門(mén)(System Call gate)的參數專(zhuān)遞方法,它在行程用戶(hù)態(tài)堆棧和內核態(tài)堆棧自動(dòng)復制傳遞的參數。但這種
方法吏用起來(lái)步驟比較復雜。

另外,在每個(gè)系統呼叫處理函數中應該傳遞的參數進(jìn)行驗證,以保證所有參數都合法有效。尤其是用戶(hù)提供的指標,應該進(jìn)行嚴格地審查。以保證指標所指的記憶體區域范圍有效,並且具有相應的讀寫(xiě)許可權。

5.6系統時(shí)間和定時(shí)

5.6.1系統時(shí)間

為了讓作業(yè)系統能自動(dòng)地準確提供當前時(shí)間和日期資訊,PC/AT微機系統中提供了用電池供電的真實(shí)時(shí)鐘RT(Real Time)電路支援。通常這部分電路與保存系統資訊的少量CMOS RAM集成在一個(gè)晶片上,因此這部分電路被稱(chēng)為RT/CMOS RAM電路。PC/AT微機或其相容機中使用了Motorola公司的MC146818晶片。

有初始化時(shí),Linux 0.12內核透過(guò)init/main.c程式中的time_init( )函數讀取這塊晶片中保存的當前時(shí)間和日期資訊,并透過(guò)kernel/mktime.c程式中的kernel mktime( )函數轉換成從1970年1月1日午夜0時(shí)開(kāi)始計起到當前的以秒為單位的時(shí)間,我們稱(chēng)之為UNIX 日歷時(shí)間。該時(shí)間確定了系統開(kāi)始執行的日歷時(shí)間,被保存在全域變數startup_time中供內核所有代碼使用。用戶(hù)程式可以使用系統呼叫stime( )來(lái)讀取startup_time的值,而超級用戶(hù)則可以透過(guò)系統呼叫stime()來(lái)修改這個(gè)系統時(shí)間值。


另外,再透過(guò)下面介紹的從系統啟動(dòng)開(kāi)始計數的系統滴答值jiffies,程式就可以唯一地確定執行時(shí)刻的當前時(shí)間值。由于每個(gè)滴答定時(shí)值是10毫秒,因此內核代碼中定義了一個(gè)巨集來(lái)方便代碼對當前時(shí)間的存取。這個(gè)巨集定義在include/linux/sched.h檔第192行上,其形主 下:

# define CURRENT_TIME(startup_time + jiffiles/HZ)

其中,HZ = 100,是內核系統時(shí)鐘頻率。當前時(shí)間巨集CURRENT_TIME被定義為系統開(kāi)機時(shí)間startup_time加上開(kāi)機系統執行的時(shí)間jiffies/100 。在修改一個(gè)檔被存取時(shí)間或其i節點(diǎn)被修改時(shí)間均使用了這個(gè)巨集。


5.6.2 系統定時(shí)

在Linux 0.12內核的初始化過(guò)程中,PC 機的可程式化定時(shí)晶片Intel 8253(8254)的計數器通道0被設置成執行在方式3下(方波發(fā)生器方式),並且初始計數值LATCH被設置成每隔10毫秒在通道0輸出端OUT發(fā)出一個(gè)方波上升沿。由于8254晶片的時(shí)鐘輸入頻率為1.193180MHz,因此初始計數值LATCH=1193180/100,約為11931。由於OUT接腳被連接到可程式化控制晶片的0級上,因此系統每隔10毫秒就會(huì )發(fā)出一個(gè)時(shí)鐘中斷請求(IRQ0)信號。這個(gè)時(shí)間節拍就是作業(yè)系統執行的脈搏,我們稱(chēng)之為l個(gè)系統滴答或一個(gè)系統時(shí)鐘週期。因此每經(jīng)過(guò)1個(gè)滴答時(shí)問(wèn),系統就會(huì )呼叫一次時(shí)鐘中斷處理程式(timer_interrupt)。

時(shí)鐘中斷處理程式timer_interrupt主要用來(lái)透過(guò)jiffies變數來(lái)累計自系統啟動(dòng)以來(lái)經(jīng)過(guò)的時(shí)鐘滴答數。每當發(fā)生一次時(shí)鐘中斷jiflies值就增加1。然后呼叫C語(yǔ)言函數do_timer( )作進(jìn)一步的處理。呼叫時(shí)所帶的參數CPL是從被中斷程式的段選擇符(保存在堆棧中的CS段寄存器值)中取得當前代碼特權級CPL。

do_timer( )函數則根據特權級對當前行程執行時(shí)間作累計。如果CPL=0,則表示行程執行在內核態(tài)時(shí)被中斷,因此內核就會(huì )把行程的內核態(tài)執行時(shí)間統計值stime增1,否則把行程用戶(hù)態(tài)執行時(shí)間統計值增1。如果軟碟處理程式floppy.c在操作過(guò)程中添加過(guò)計時(shí)器,則對計時(shí)器鏈表進(jìn)行處理。若某個(gè)計時(shí)器時(shí)間到(遞減后等於0),則呼叫該計時(shí)器的處理函數。然后對當前行程執行時(shí)間進(jìn)行處理,把當前行程執行時(shí)間片減1。時(shí)間片是一個(gè)行程在被切換掉之前所能持續執行的CPU時(shí)間,其單位是上面定義的滴答數。如果行程時(shí)間片值遞減后還大於0,表示其時(shí)間片還沒(méi)有用完,于是就退出do_timer( )繼續執行當前行程。如果此時(shí)行程時(shí)間片已經(jīng)遞減為0,表示該行程已經(jīng)用完了此次使用CPU的時(shí)間片,於是程式就會(huì )根據被中斷程式的級別來(lái)確定進(jìn)一步處理的方法。若被中斷的當前行程是工作在用戶(hù)態(tài)的(特權級別大於0),則do_timer()會(huì )呼叫調度程式schedule( )切換到其飽行程去執行。如果被中斷的當前行程工作在內核態(tài),也即在內核程式中執行時(shí)被中斷,則do_timer( )會(huì )立刻退出。因此這樣的處理方式?jīng)Q定了Linux系統的行程在內核態(tài)執行時(shí)不會(huì )被調度程式切換。即行程在內核態(tài)程式中執行時(shí)是不可搶占的(nonpreemptive) ¹,但當處於用戶(hù)程式中執行時(shí)則是可以被搶佔的(preemptive)。

¹從Linux2.4內核起,Robert Love開(kāi)發(fā)出了可搶占式的內核升級套件。這使得在內核空間低優(yōu)先順序的行程也能被高優(yōu)先順序行程搶占,從而能使系統回應效能最大提高200%。參見(jiàn)Robert Love編著(zhù)的《Linux內核開(kāi)發(fā)》一書(shū)。

注意 上述計時(shí)器專(zhuān)門(mén)用於軟碟馬達開(kāi)啟和關(guān)閉定時(shí)操作。這種計時(shí)器類(lèi)似現代Linux系統中的動(dòng)態(tài)計時(shí)器(Dynamic Timer),僅供內核使用。這種計時(shí)器可以在非要時(shí)動(dòng)態(tài)地建立,而在定時(shí)到期時(shí)動(dòng)態(tài)地撤銷(xiāo)。在Linux 0.12內核中計時(shí)器同時(shí)最多可以有64個(gè)。計時(shí)器的處理代碼在sched.c程式283- -368行。


5.7 Linux行程控制

程式是一個(gè)可執行的檔案,而行程(process)是一個(gè)執行中的程式實(shí)例。利用分時(shí)技術(shù),在Linux作業(yè)系統上同時(shí)可以執行多個(gè)行程。分時(shí)技術(shù)的基本原理是把CPU的執行時(shí)間劃分成一個(gè)個(gè)規定長(cháng)度的時(shí)間片(time slice),讓每個(gè)行程在一個(gè)時(shí)間片內執行。當行程的時(shí)間片用完時(shí)系統就利用調度程式切換到另一個(gè)行程去執行。因此實(shí)際上對於具有單個(gè)CPU的機器來(lái)說(shuō)某一時(shí)刻只能執行一個(gè)行程。但由於每個(gè)行程執行的時(shí)間片很短(例如15個(gè)系統滴答=150毫秒) ,所以表面看來(lái)好象所有行程在同時(shí)執行著(zhù)。

對於Linux 0.12內核來(lái)講,系統最多可有64個(gè)行程同時(shí)存在,除了第一個(gè)行程用“手工”建立以外,其余的都是現有行程使用系統呼叫fork建立的新行程,被建立的行程稱(chēng)為子行程(child process),建立者,則稱(chēng)為父行程(parent process)。內核程式使用行程標識號(process ID,pid)來(lái)標識每個(gè)行程。行程由可執行的指令代碼、資料和堆棧區。行程中的代碼和資料部分分別對應一個(gè)執行檔中的代碼段、資料段。每個(gè)行程只能執行自己的代碼和存取自己的資料及堆棧區。行程之間的通信需要透過(guò)系統呼叫來(lái)進(jìn)行。對於只有一個(gè)CPU的系統,在某一時(shí)刻只能有一個(gè)行程正在執行。內核透過(guò)調度程式分時(shí)調度各個(gè)行程執行。

我們已經(jīng)知道,Linux系統中一個(gè)行程可以在內核態(tài)(kernel mode)或用戶(hù)態(tài)(user mode)下執行,並且分別使用各自獨立的內核態(tài)堆棧和用戶(hù)態(tài)堆棧。用戶(hù)堆疊用於行程在用戶(hù)態(tài)下臨時(shí)保存 呼叫函數的參數、區域變數等資料;內核堆棧則含有內核程式執行函數呼叫時(shí)的信息。

另外在Linux內核中,行程通常被稱(chēng)作任務(wù)(task) ,而把執行在用戶(hù)空間的程式稱(chēng)作行程。本文將在盡量遵守這個(gè)預設規則的同時(shí)混用這兩個(gè)術(shù)語(yǔ)。

5.7.1 任務(wù)資料結構

內核程式透過(guò)行程表對行程進(jìn)行程管理,每個(gè)行程在行程表中佔有一項。在Linux系統中,行程表項是一個(gè)task_struct任務(wù)結構指標。任務(wù)資料結構定義在標頭檔include/linux/sched.h中。有寫(xiě)書(shū)上稱(chēng)其為行程控制塊PCB(Process Control Block)或行程描述符PD (Processor Descriptor) 。其中保存著(zhù)用于控制和管理行程的所有信息。主要包括當前執行的狀態(tài)信息、信號、行程號、父行程號、執行時(shí)間累計值、正在使用的檔案和本任務(wù)的區域描述符以及任務(wù)狀態(tài)段信息。該結構每個(gè)欄位的具體含義如下所示。


};


■ long state欄位含有行程的當前狀態(tài)代號。如果行程正在等待使用CPU或者行程正被執行,那麼state的值是TASK_RUNNING。如果行程正在等待某一事件的發(fā)生因而處於空閒狀態(tài),那麼state的值就是TASK_INTERRUPTIBLE或者TASK_UNINTERRUPTIBLE。這兩個(gè)值含義區別在于處于TASK_INTERRUPTIBLE狀態(tài)的行程能夠被信號喚醒並啟動(dòng),而處於TASK_UNINTERRUPTIBLE狀態(tài)的行程則通常是在直接或間接地等待硬件條件的滿(mǎn)足因而不會(huì )接受任何信號。TASK_STOPPED狀態(tài)用於說(shuō)明一個(gè)行程正處於停止狀態(tài)。例如行程在收到一個(gè)相關(guān)信號時(shí)(例如SIGSTOP、SIGTTIN或SIGTTOU等)或者當行程被另一個(gè)行程使用ptrace系統呼叫監控並且控制權在監控行程中時(shí)。TASK_ZOMBIE狀態(tài)用於描述一個(gè)行程已經(jīng)被終止,但其任務(wù)資料結構項仍然存在於任務(wù)結構表中。一個(gè)行程在這些狀態(tài)之間的轉換過(guò)程見(jiàn)下面說(shuō)明。

■ long counter欄位保存著(zhù)行程在被暫時(shí)停止本次執行之前還能執行的時(shí)間滴答數,即在正常情況下還需要經(jīng)過(guò)幾個(gè)系統時(shí)鐘周期才切換到另一個(gè)行程。調度程式會(huì )使用行程的counter值來(lái)選擇下一個(gè)要執行的行程,因此counter可以看作是一個(gè)行程的動(dòng)態(tài)特性。在一個(gè)行程剛被建立時(shí)counter的初值等於priority。

■ long priority用於給counter代入初始值。在Linux0.12中這個(gè)初值為15個(gè)系統時(shí)鐘週期時(shí)間(15個(gè)滴答)。當需要時(shí)調度程式會(huì )使用priority的值為counter代入一個(gè)初值,參見(jiàn)sched.c程式和fork.c程式。當然,priority的單位也是時(shí)間滴答數。

■ long signal欄位是行程當前所收到信號的點(diǎn)陣圖,共32個(gè)Bit位,每個(gè)Bit位元代表一種信號,信號值二位元偏移值 +l。因此Linux內核最多有32個(gè)信號。在每個(gè)系統呼叫處理過(guò)程的最后,系統會(huì )使用該信號點(diǎn)陣圖對信號進(jìn)行預處理。

■ struct sigaction sigaction [32]結構陣列用來(lái)保存處理各信號所使用的操作和屬性。陣列的每一項對應一個(gè)信號。

■ long blocked欄位是行程當前不想處理的信號阻塞點(diǎn)陣圖。與signal欄位類(lèi)似,其每一Bit位代表一種被阻塞的信號。

■ int exit欄位是用來(lái)保存程式終止時(shí)的退出碼。在子行程結束后父行程可以查詢(xún)它的這個(gè)退出碼。

■ unsigned long start_code欄位是行程代碼在線(xiàn)性空間中的開(kāi)始位址。

■ unsigned long end_code欄位保存著(zhù)行程代碼的位元組長(cháng)度值。

■ unsigned long end_data欄位保存著(zhù)行程的代碼長(cháng)度 + 資料長(cháng)度的總位元組長(cháng)度值。

■ unsigned long brk欄位也是行程代碼和資料的總位元組長(cháng)度值(指標值) ,但是還包括未初始化的的資料區bss,參見(jiàn)圖13-6。這是brk在一個(gè)行程開(kāi)始執行時(shí)內初值。透過(guò)修改這個(gè)指標,內核可以為行程添加和釋放動(dòng)態(tài)分配的記憶體。這通常是透過(guò)呼叫malloc( )函數並透過(guò)brk系統呼叫由內核進(jìn)行操作。

■ unsigned long start_stack欄位值指向行程邏位址空間中堆棧的起始處。同樣請參尋圖13-6中的堆棧指標位置。

■ long pid是行程標識號,即行程號。它被用來(lái)唯一地標識行程。

■ long pgrp是指行程所屬行程群組號。

■ long session是行程的會(huì )話(huà)號,即所屬會(huì )話(huà)的行程好。

■ long leader是會(huì )話(huà)首行程號。有關(guān)行程群組和會(huì )話(huà)的概念請參見(jiàn)第7章程序列表后的說(shuō)明。

■ int groups[NGROUPS]是行程所屬各個(gè)組的群組號陣列。一個(gè)行程可屬於多個(gè)組。

■ task_struct *p_pptr是指向父行程任務(wù)結構的指標。

■ task_struct *p_cptr是指向最新子行程任務(wù)結構 旨標o

■ task_struct *p_ysptr是指向比自己后建立的相鄰行程的指標。

■ task_struct *p_osptr是指向比自己早建立的相鄰行程的指標。以上4個(gè)指標的關(guān)系參見(jiàn)圖5-20所示。在Linux 0.11內核的任務(wù)數據結構中專(zhuān)門(mén)有一個(gè)父行程號欄位徹father,但是0.12內核中已經(jīng)不用。此時(shí)我們可以使用行程的pptr->pid來(lái)取得父行程的行程號。






■ unsigned short uid是擁有該行程的用戶(hù)標識號(用戶(hù)id)。

■ unsigned short euid是有效用戶(hù)標識號,用于指明存取檔的權力。

■ unsigned short suid是保存的用戶(hù)標識號。當執行檔的設置用戶(hù)ID標志。
(set-user-ID)置位元時(shí),suid中保存著(zhù)執行檔的uido。否則suid等於行程的euid。

■ unsigned short gid是用戶(hù)所屬組標識號(組id)。指明了擁有該行程的用戶(hù)群組。

■ unsigned short egid是有效群組標識號,用于指明該群組用戶(hù)存取檔的許可權。

■ unsigned short sgid是保存的用戶(hù)組標識號。當執行檔的設置組ID旗標(set-group-ID)置位元時(shí),sgid中保存著(zhù)執行檔的gid。否則sgid等於行程的egid。有關(guān)這些用戶(hù)號和群組號的描述請參第5章sys.c程式前的概述。

■ long timeout內核定時(shí)超時(shí)值。

■ long alarm是行程的報警定時(shí)值(滴答數) 主系統定時(shí)中斷中會(huì )遞減該值。當使用系統呼叫alarm( ) (sched.c第338行) 設置了該值后(參數是以秒為單位,但在保存到alarm欄位中之前內核會(huì )把它轉換為系統滴答數),那麼在經(jīng)過(guò)了指定的秒數后,該值遞減為0,此時(shí)系統就會(huì )向該行程發(fā)送一個(gè)SIGALRM信號,預設時(shí)該信號會(huì )終止程式的執行。當然也可以使用信號捕捉函數(signal( )或signal ())來(lái)捕捉該信號進(jìn)行指定的操作。

■ long utime是累計行程在用戶(hù)態(tài)執行的時(shí)間(滴答數)。

■ long stime是累計行程在系統態(tài)(內核態(tài)) 執行的時(shí)間 (滴答數)。

■ long cutime是累計行程的子行程在用戶(hù)態(tài)執行的時(shí)間 (滴答數)。

■ long cstime是累計行程的子行程內核態(tài)執行的時(shí)間 (滴答數)。

■ struct start_time是行程生成並開(kāi)始執行的時(shí)刻。

■ struct rlimit rlim[RLIM NLIMITS] 行程資源使用統計陣列。

■ unsigned int flags各行程的標志,0.12內核還未使用。

■ unsigned short used_math是一個(gè)標志,指明本行程是否使用了輔助運算器。

■ int tty是行程使用tty終端的子裝置號。-1 表示沒(méi)有使用。

■ unsigned short umask是行程建立新檔時(shí)所用的屬性遮罩位元,即新建檔所設置的存取屬性。

■ struct m_inode * pwd是行程的當前工作目錄 i節點(diǎn)結構。每個(gè)行程都有一個(gè)當前工作目錄,用於解析相對路徑名,並且可以使用系統呼叫chdir來(lái)改變之。

■ struct m_inode * root是行程自己的根目錄 i點(diǎn)節結構。每個(gè)行程都可有自己指定的根目錄,用於解析絕對路徑名。只有超級用戶(hù)能透過(guò)系統呼叫chroot來(lái)修改這個(gè)根目錄。

■ struct m_inode * executable是行程執行的執行檔在記憶體中i節點(diǎn)結構指標。系統可根據該欄位來(lái)判斷系統中是否還有另一個(gè)行程在執行同一個(gè)執行檔。如果有的話(huà)那麼這個(gè)記憶體中i節點(diǎn)參照計數值executable ->i_count會(huì )大於1在行程被建立時(shí)該欄位被賦予和父行程同一欄位相同的值,即表示正在與父行程執行同一個(gè)程式。當在行程中呼叫cxec( )類(lèi)函數而去執行一個(gè)指定的執行檔時(shí),該欄位值就會(huì )被替換成exec( ) 函數所執行程式的記憶體i節點(diǎn)指標。當行程呼叫exit( )函數而執行退出處理時(shí)該欄位所指記憶體i節點(diǎn)的參照引用計數會(huì )被減l,並且該欄位將被置空。該欄位的主要作用體現存memory.c程式的share_page()函數中。該函數代碼根據行程的executable所指節點(diǎn)的引用計數可判斷系統中當前執行的程式是否有多個(gè)拷貝存在(起碼2個(gè))。若是的話(huà)則在他們之間嘗試頁(yè)面共用操作。

■ 在系統初始化時(shí),在第1次呼叫執行execve()牧之前,系統建立的所有任務(wù)的executable都是0。這些任務(wù)包括任務(wù)0、任務(wù)1以及任務(wù)1直接建立的沒(méi)有執行過(guò)execve( )的所有任務(wù),即代碼直接包含在內核碼中的所有任務(wù)的executable都是0。因為任務(wù)0的代碼包含在內核代碼中,它不是由系統從檔案系統上載入執行的執行檔,因此內核代碼中固定設置它的executable值為0。另外,建立新行程時(shí),fork( )會(huì )復制父行程的任務(wù)資料結構,因此任務(wù)1的executable也是0。但在執行了exccve( )之后,executable就被賦予了被執行檔的記憶體i節點(diǎn)的指標,此后所有任務(wù)的該值就均不會(huì )為0 了。

■ unsigned long close_on_exec是一個(gè)行程檔案描述符(檔案控制碼)點(diǎn)陣圖標志。每個(gè)Bit位代表一個(gè)檔案描述符,用於確定在系統呼叫execvc( )時(shí)需要關(guān)閉的檔案描述符(參見(jiàn)include/fcntl.h)。當一個(gè)程式使用fork( )函數建立了一個(gè)子行程時(shí),通常會(huì )在該子行程中呼叫execve( )函數戴入執行另一個(gè)新程式。此時(shí)子行程將完全被新程式替換掉,並在子行程中開(kāi)始執行新程式,若一個(gè)檔案描述符在close_on_exec中的對應Bit位元是置位元狀態(tài),那麼在子行程執行execve( )呼叫時(shí)對應打開(kāi)著(zhù)的檔案描述符將被關(guān)閉,即在新行程中該檔案描述符被關(guān)閉。否則該檔案描述符將始終處於打開(kāi)狀態(tài)。

■ struct file * filp[NR_OPEN]是行程使用的所有打開(kāi)檔的檔案結構指標表,最多32項。檔案描述符的值即是該結構中的索引值。其中每一項用於檔案描述符定位檔指標和存取檔。

■ struct desc_struct ldt[3]是該行程區域描述符表結構。定義了該任務(wù)在虛擬位址空間中的代碼段和資料段。其中陣列項0是空項,項l是代碼段描述符,項2是資料段(包含數據和堆棧)描述符。

■ struct tss_struct tss是行程的任務(wù)狀態(tài)段TSS(Task State Segment)資訊結構。在任務(wù)從執行中被切換出時(shí)tss_struct結構保存了當前處理器的所有寄存器值。當任務(wù)又被CPU重新執行時(shí),CPU就會(huì )利用這些值恢復到任務(wù)被切換出時(shí)的狀態(tài),並開(kāi)始執行。

當一個(gè)行程在執行時(shí),CPU的所有寄存器中的值、行程的狀態(tài)以及堆棧中的內容被稱(chēng)為該行程的上下文。當內核需要切換( switch)至另一個(gè)行程時(shí),它就需要保存當前行程的所有狀態(tài),也即保存當前行程的上下文,以便在再次執行該行程時(shí),能夠恢復到切換時(shí)的狀態(tài)執行下去。在Linux中,當前行程上下文均保存在行程的任務(wù)資料結構中。在發(fā)生中斷時(shí),內核就在被中斷行程的上下文中,在內核態(tài)下執行中斷服務(wù)常式。但同時(shí)會(huì )保留所有要用到的資源,以便中斷服務(wù)結束時(shí)能夠恢復被中斷行程的執行。


5.7.2 行程執行狀態(tài)

一個(gè)程在其生存期內,可處於一組不同的狀態(tài)下,稱(chēng)為行程狀態(tài)。見(jiàn)圖5-21所示。行程狀態(tài)保存在行程任務(wù)結構的state欄位中。當行程正在等待系統中的資源而處于等待狀態(tài)時(shí),則稱(chēng)其處於睡眠等待狀態(tài),在Linux系統中,睡眠等待狀態(tài)被分為可中斷的和不可中斷的等待狀態(tài)。






執行狀態(tài) (TASK_RUNNING)
當行程正在被CPU執行,或已經(jīng)準備就緒隨時(shí)可由調度程式執行,則稱(chēng)該行程為處于執行狀態(tài)(running)。若此時(shí)行程沒(méi)有被CPU執行,則稱(chēng)其處於就緒執行狀態(tài)。見(jiàn)圖5-21中三個(gè)標號為0的狀態(tài),行程可以在內核態(tài)執行,也可以在用戶(hù)態(tài)執行。當一個(gè)行程在內核代碼中執行時(shí),我們稱(chēng)其處於內核執行態(tài),或簡(jiǎn)稱(chēng)為內核態(tài);當一個(gè)行程正在執行用戶(hù)自己的代碼時(shí),我們稱(chēng)其為處於用戶(hù)執行態(tài)(用戶(hù)態(tài))。當系統資源已經(jīng)可用時(shí),行程就被喚醒而進(jìn)入準備執行狀態(tài),該狀態(tài)稱(chēng)為就緒態(tài)。這些狀態(tài)(圖中中間一列)在內核中表示方法相同,都被成
為處於TASK_RUNNING狀態(tài)。當一個(gè)新行程剛被建立出后就處於本狀態(tài)中(最下一個(gè)0處)。

可中斷睡眠狀態(tài) (TASK_INTERRUPTIBLE)

當行程處於可中斷等待(睡眠)狀態(tài)時(shí),系統不會(huì )調度該行程執行。當系統產(chǎn)生一個(gè)中斷或者釋放了行程正在等待的資源,或者行程收到一個(gè)信號,都可以喚醒行程轉換到就緒狀態(tài)(執行狀態(tài))。

不可中斷睡眠狀態(tài) (TASK_UNINTERRUPTIBLE)

除了不會(huì )因為收到信號而被喚醒,該狀態(tài)與可中斷睡眠狀態(tài)類(lèi)似。但處於該狀態(tài)的行程只有被使用wake_up( )函數明確喚醒時(shí)才能轉換到可執行的就緒狀態(tài),該狀態(tài)通常在行程需要不受干擾地等待或者所等待事件會(huì )很快發(fā)生時(shí)使用。

暫停狀態(tài) (TASK_STOPPED)
當行程收到信號SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU時(shí)就會(huì )進(jìn)入暫停狀態(tài)??上蚱浒l(fā)送SIGCONT信號讓行程轉換到可執行狀態(tài)。行程在除錯期間接收到任何信號均會(huì )進(jìn)入該狀態(tài)。在Linux 0.12中,還未實(shí)現對該狀態(tài)的轉換處理。處於該狀態(tài)的行程將被作為行程終止來(lái)處理。

僵死狀態(tài)(TASK ZOMBIE)
當行程已停止執行,但其父行程還沒(méi)有呼叫wait ( )詢(xún)問(wèn)其狀態(tài)時(shí),則稱(chēng)該行程處於僵死狀態(tài)。為了了讓父行程能夠獲取其停止其執行的資訊,此時(shí)子行程的任務(wù)資料結構資訊還需要保留著(zhù)。一旦父行程呼叫wait ( )取得了子行程的資訊,則處於該狀態(tài)行程的任務(wù)資料結構就會(huì )被釋放掉。

當一個(gè)行程的執行時(shí)間片用完,系統就會(huì )使用調度程式強制切換到其他的行程去執行。另外,如果行程在內核態(tài)執行時(shí)需要等待系統的某個(gè)資源,此時(shí)該行程就會(huì )呼叫sleep_on( )或interruptible_sleep_on 自愿地放棄CPU的使用權,而讓調度程式去執行其他行程。行程則進(jìn)入睡眠狀態(tài)(TASK_UNINTERRUPTIBLE或TASK_INTERRUPTIBLE)。

只有當行程從“內核執行態(tài)”轉移到“睡眠狀態(tài)”時(shí),內核才會(huì )進(jìn)行行程切換操作。在內核態(tài)下執行的行程不能被其他行程搶占,而且一個(gè)行程不能改變另一個(gè)行程門(mén)狀態(tài)。為了避免行程切換時(shí)造成內核數據錯誤,內核在執行臨界區代碼時(shí)會(huì )禁止一切中斷。


5.7.3行程初始化

在boot/目錄中,開(kāi)機程式把內核從磁碟上載入到記憶體中,並讓系統進(jìn)入保護模式下執行后,就開(kāi)始執行系統初始化程式init/main.c。該程式首先確定如何分配使用系統實(shí)體記憶體,然后呼叫內核各部分的初始化函數分別對記憶體管理、中斷處理、區塊裝置和字元裝置、行程管理以及硬盤(pán)和軟碟硬體進(jìn)行初始化處理。在完成了這些操作之后,系統各部分已經(jīng)處于可執行狀態(tài)。此后程式把自己“手工”移動(dòng)到任務(wù)0(行程0)中執行,並使用fork( )呼叫首次建立出行程l。在行程1中程式將繼續進(jìn)行應用環(huán)境的初始化並執行shell登錄程式。而原行程0則會(huì )在系統空閒時(shí)被調度執行,此時(shí)任務(wù)0僅執行pause( )系統呼叫,並又會(huì )呼叫調度函數。

“移動(dòng)到任務(wù)0中執行”這個(gè)過(guò)程由巨集move_to_user_mode (include/asm/system.h)完成。它把main.c程式執行流從內核態(tài)(特權級0)移動(dòng)到了用戶(hù)態(tài)(特權級3)的任務(wù)0中繼續執行。在移動(dòng)之前,系統在對調度程式的初始化過(guò)程(sched_init ( ))中,首先對任務(wù)0的執行環(huán)境進(jìn)行了設置。這包括人工預先設置好任務(wù)0資料結構各欄位的值(include/linux/sched.h) 、在全域描述符表中添入任務(wù)0的任務(wù)狀態(tài)段(TSS) 描述符和區域描述符表(LDT)的段描述符,並把它們分別載入到任務(wù)寄存器tr和區域描述符表寄存器ldtr中。

這裡需要強調的是,內核初始化是一個(gè)特殊過(guò)程,內核初始化代碼也即是任務(wù)0的代碼。從任務(wù)0資料結構中設置的初始數據可知,任務(wù)0的代碼段和資料段的基址是0、段限長(cháng)是640KB。而內核代碼段和資料段的基址是0、段限長(cháng)是16MB,因此任務(wù)0的代碼段和資料段分別包含在內核代碼段和資料段中。內核初始化程式main.c也即是任務(wù)0中的代碼,只是在移動(dòng)到任務(wù)0之前系統正以?xún)群藨B(tài)特權級。執行著(zhù)main.c程式。巨集move_ to_user_mode的功能就是把執行特權級從內核態(tài)的0級變換到用戶(hù)態(tài)的3級,但是仍然繼續執行原來(lái)的代碼指今流。

在移動(dòng)到任務(wù)0的過(guò)程中,巨集move_to_user_mode使用了中斷返回指令造成特權級改變的方法。使用這種方法進(jìn)行控制權轉移是由CPU保護機制造成的。CPU允許低級別(如特權級3)代碼透過(guò)呼叫門(mén)或中斷、陷阱門(mén)來(lái)呼叫或轉移到高級別代碼中執行,但反之則不行。因此內核採用了這種模擬IRET返回低級別代碼的方法。該方法的主要思想是在堆棧中構筑中斷返回指令需要的內容,把返回位址的段選擇符設置成任務(wù)0代碼段選擇符,其特權級為3。此后執行中斷返回指令iret時(shí)將導致系統CPU從特權級0跳轉到外層的特權級3上執行。
參見(jiàn)圖5-22所示的特權級發(fā)生變化時(shí)中斷返回堆棧結構示意圖。






巨集move_to_user_mode首先往內核堆棧中壓入任務(wù)。資料段選擇符和內核堆棧指標。然后壓入標志寄存器內容。最后壓入任務(wù)0代碼段選擇符和執行中斷返回后需要執行的下一條指令的偏移位置。該偏移多位置是iret后的一條指令處。

當執行iret指令時(shí),CPU把返回位址送入CS: EIP中,同時(shí)彈出堆棧中標志寄存器內容。由於CPU判斷出目的代碼段的特權級是3,與當前內核態(tài)的0級不同。于是CPU會(huì )把堆棧中的堆棧段選擇符和指標彈出到SS : ESP中。由於特權級發(fā)生了變化,段寄存器DS、ES、FS和GS的值變得無(wú)效,此時(shí)CPU會(huì )把這些段寄存器清零。因此在執行了iret指令后需要重新載入這些段寄存器。此后,系統就開(kāi)始以特權級3執行在任務(wù)0的代碼上。所使用的用戶(hù)態(tài)堆棧還是原來(lái)在移動(dòng)之前使用的堆棧。而其內核態(tài)堆棧則被指定為其任務(wù)資料結構所在頁(yè)面
的頂端開(kāi)始(PAGE_SIZE + (1ong) &init_task)由於以后在建立新行程時(shí),需要復制任務(wù)0的任務(wù)資料結構,包括其用戶(hù)幻 ”指標,因此要求任務(wù)。的用戶(hù)
態(tài)堆棧在建立任務(wù)l(shuí) (行程1)之前保持“干淨”狀態(tài)。


5.7.4 建立新行程

Linux系統中建立新行程使用fork( )系統呼叫。所有行程都是透過(guò)復制行程0而得至的,都是行程0的子行程。

在建立新行程的過(guò)程中,系統首先在任務(wù)陣列中找出一個(gè)還沒(méi)有被任何行程使用的空項(空槽) 。如果系統已經(jīng)有64個(gè)行程在執行,則fork ( )系統呼叫會(huì )因為任務(wù)陣列表中沒(méi)有可用空項而出錯返回。然后系統為新建行程在主記憶體區中申請一頁(yè)記憶體來(lái)存放其任務(wù)資料結構資訊,并復制當前行程任務(wù)資料結構中的所有內容作為新行程任務(wù)資料結構的范本。為了防止這個(gè)還未處理完成的新建行程被調度函數執行,此時(shí)應該立刻將新行程狀態(tài)置為不可斷的等待狀態(tài) (TASK_UNINTERRUPTIBLE)。

隨后對復制的任務(wù)資料結構進(jìn)行修改。把當前行程設置為新行程的父行程,清除信號點(diǎn)陣圖並重定新行程各統計值,並設置初始執行時(shí)間片值為15個(gè)系統滴答數(150毫秒) 。接著(zhù)根據當前行程設置任務(wù)狀態(tài)段(TSS)中各寄存器的值。由于建立行程時(shí)新行程返回值應為0,所.以需要設置tss.eax = 0。新建行程內核態(tài)堆棧指標tss.esp0被設置成新行程任務(wù)資料結構所在記憶體頁(yè)面的頂端,而堆棧段 tss.ss0被設置成內核資料段選擇符。tss.1dt被設置為區域表描述符在GDT中的索引值。如果當前行程使用了輔助運算器,則還需要把輔助運算器的完整狀態(tài)保存到新行程的tss.i387結構中。


此后系統設置新任務(wù)的代碼和資料段基址、限長(cháng),並復制當前行程記憶體分頁(yè)管理的頁(yè)表。注意,此時(shí)系統並不為新的行程分配實(shí)際的實(shí)體記憶體頁(yè)面,而是讓它共用其父行程的記憶體頁(yè)面。只有當父行程或新行程中任意一個(gè)有寫(xiě)記憶體操作時(shí),系統才會(huì )為執行寫(xiě)操作的行程分配相關(guān)的獨自使用的記憶體頁(yè)面。這種處理方式稱(chēng)為寫(xiě)時(shí)復制(Copy On Write)技術(shù)。

隨后,如果父行程中有檔案是打的,則應將對應檔案的打開(kāi)次數增加1。接著(zhù)在GDT中設置新任務(wù)的TSS和LDT描述符項,其中基底位址資訊指向新行程任務(wù)結構中的tss和ldt。最后再將新任務(wù)設置成可執行狀態(tài)並返回新行程號。

另外請注意,建立一個(gè)新的子行程和載入執行一個(gè)執行程式檔是兩個(gè)不同的概念。當建立子行程時(shí),它完全復制了父行程代碼和資料區,並會(huì )在其中執行子行程部分的代碼。而執行區塊裝置上的一個(gè)程式時(shí),一般是在子行程中執行exec( )系統呼叫來(lái)操作的。在進(jìn)入exec( )后,子行程原來(lái)的代碼和資料區就會(huì )被清掉(釋放) 。待該子行程開(kāi)始執行新程式時(shí),由於此時(shí)內核還沒(méi)有從區塊裝置上載入該程式的代碼,CPU就會(huì )立刻產(chǎn)生內碼表面不存在的異常(Fault) ,此時(shí)記憶體管理程式就會(huì )從區塊裝置上載入相應的內碼表面,然后CPU又重新執
行引起異常的指令,到此時(shí)新程式的代碼才真正開(kāi)始室執行。


5.7.5行程調度

內核中的調度程式用於選擇系統中下一個(gè)要執行的行程。這種選擇執行機制是多工作業(yè)系統的基礎。調度程式可以看作為在所有處於執行狀態(tài)的行程之間分配CPU執行時(shí)間的管理代碼。由前面描述可知,Linux行程是搶佔式的,但被搶佔的行程仍然處於TASK_RUNNING狀態(tài),只是暫時(shí)沒(méi)有被CPU執行。行程的搶佔發(fā)生在行程處於用戶(hù)態(tài)執行階段,在內核態(tài)執行時(shí)是不能被搶佔的。

為了能讓行程有效地使用系統資源,又能使行程有較快的回應時(shí)問(wèn),就需要對行程的切換調度採用一定的調度策略。在Linux 0.12中採用了基於優(yōu)先順序排隊的調度策略。

調度程式
schedule ( )函數首先掃描任務(wù)陣列。透過(guò)比較每個(gè)就緒態(tài)(TASK_RUNNING)任務(wù)的執行時(shí)間遞減滴答計數counter的值來(lái)確定當前哪個(gè)行程執行的時(shí)間最少。哪一個(gè)的值大,就表示執行時(shí)間還不長(cháng),于是就選中該行程,並使用任務(wù)切換巨集函數切換到該行程執行。

如果此時(shí)所有處于TASK_RUNNING狀態(tài)行程的時(shí)間片都已經(jīng)用完,系統就會(huì )根據每個(gè)行程的優(yōu)先權值priority,對系統中所有行程(包括正在睡眠的行程)重新計算每個(gè)任務(wù)需要執行的時(shí)間片值counter。計算的公式是:



這樣對于正在睡眠的行程當它們被喚醒時(shí)就具有較高的時(shí)間片counter值。然后schedule ( )函數重新掃描任務(wù)陣列中所有處于TASK_RUNNING狀態(tài),重復上述過(guò)程,直到選擇出一個(gè)行程為止。最后呼叫switch_to( )執行實(shí)際的行程切換操作。

如果此時(shí)沒(méi)有其他行程可執行,系統就會(huì )選擇行程0執行,對於Linux 0.12來(lái)說(shuō),行程0會(huì )呼叫pause( )把自己置為可中斷的睡眠狀態(tài)並再次呼叫schedule( )。不過(guò)在調度行程執行時(shí),schedule( )並>不在意行程0處於什麼狀態(tài)。只要系統空閑就調度行程0執行。


行程切換
每當選擇出一個(gè)新的可執行行程時(shí),schedule( )函數就會(huì )呼叫定義在include/asm/system.h中的switch_to ( )巨集執行實(shí)際行程切換操作。該巨集會(huì )把CPU的當前行程狀態(tài)(上下文)替換成新行程的狀態(tài)。在進(jìn)行切換之前,switch_to ( )首先檢查要切換到的行程是否就是當前行程,如果是則什麼也不做,直接退出。否則就首先把內核全域變數current置為新任務(wù)的指標,然后長(cháng)跳轉到新任務(wù)的任務(wù)狀態(tài)段TSS組成的位址處,造成CPU執行任務(wù)切換操作。此時(shí)CPU會(huì )把其所有寄存器的狀態(tài)保存到當前任務(wù)寄存器TR中TSS段選擇符所指向的當前行
程任務(wù)資料結構的tss結構中,然后把新任務(wù)狀態(tài)段選擇符所指向的新任務(wù)資料結構中tss結構中的寄存器資訊恢復到CPU中,統就正式開(kāi)始執行新切換的任務(wù)了。這個(gè)過(guò)程可參見(jiàn)圖5-23所示。






5.7.6終止行程

當一個(gè)行程結束了執行或在半途中終止了執行,那么內核就需要釋放該行程所佔用的系統資源。這包括行程執行時(shí)打開(kāi)的檔案、申請的記憶體等。

當一個(gè)用戶(hù)程式呼叫exit ( )系統呼叫時(shí),就會(huì )執行內核函數do_exit ( )。該函數會(huì )首先釋放行程代碼段和資料段佔用的記憶體頁(yè)面,關(guān)閉行程打開(kāi)著(zhù)的所有檔,對行程使用的當前工作目錄、根目錄和執行程式的i節點(diǎn)進(jìn)行同步操作。如果行程有子行程,則讓init行程作為其所有子行程的父行程。如果行程是一個(gè)會(huì )話(huà)頭行程并且有控制終端,則釋放控制終端,並向屬于該會(huì )話(huà)的所有行程發(fā)送掛斷信號SIGHUP,這通常會(huì )終止該會(huì )話(huà)中的所有行程。然后把行程狀態(tài)置為僵死狀態(tài)TASK_ZOMBIE。並向其原父行程發(fā)送SIGCHLD信號,通知其某個(gè)子行程已經(jīng)終止,最后do_exit ( )呼叫調度函數去執行其它行程。由此可見(jiàn)在行程被終止時(shí),它的任務(wù)資料結構仍然保留著(zhù)。因為其父行程還需要使用其中的資訊。

在子行程在執行期間,父行程通常使用wait ( )或waitpid ( )函數等待其某個(gè)子行程終止。當等待的子行程被終止並處於僵死狀態(tài)時(shí),父行程就會(huì )把子行程執行所使用的時(shí)間累加到自己行程中。最終釋放已終止子行程任務(wù)資料結構所佔用的記憶體頁(yè)面,並置空子行程在任務(wù)陣列中佔用的指標項。

5.8 Linux系統中堆棧的使用方法

本節內容概要描述了Linux內核從開(kāi)機引導到系統正常執行過(guò)程中對堆棧的使用方式。這部分內容的說(shuō)明與內核代碼關(guān)系比較密切,可以先跳過(guò)。在開(kāi)始閱讀相應代碼時(shí)再回來(lái)仔細研究。

Linux 0.12系統中共使用了四種堆棧。一種是系統開(kāi)機初始化時(shí)臨時(shí)使用的堆棧;一種己進(jìn)入保護模式之后提供內核程式初始化使用的堆棧,位於內核代碼位址空間固定位置處。該堆棧也是后來(lái)任務(wù)0使用的用戶(hù)態(tài)堆棧;另一種是每個(gè)任務(wù)透過(guò)系統呼叫,執行內核程式時(shí)使用的堆棧;我們稱(chēng)之為任務(wù)的內核態(tài)堆棧。每個(gè)任務(wù)都有自己獨立的內核態(tài)堆棧;最后一種是任務(wù)在用戶(hù)態(tài)執行的堆棧,位於任務(wù)(行程)邏輯位址空間近末端處。

使用多個(gè)堆?;蛟诓煌闆r下使用不同堆棧的主要原因有兩個(gè)。首先是由於從真實(shí)模式進(jìn)入保護模式,使得CPU對記憶體定址存取方式發(fā)生了變化,因此需要重新調整設置堆棧區域。另外,為了解決不同CPU特權級共用使用堆棧帶來(lái)的保護問(wèn)題,執行0級的內核代碼和執行3級的用戶(hù)代碼需要使用不同的堆棧。當一個(gè)任務(wù)進(jìn)入內核態(tài)執行時(shí),就會(huì )使用其TSS段中給出的特權級0的堆棧指標tss.ss ( )、tss.esp( ),即內核堆棧。原用戶(hù)堆棧指標會(huì )被保存在內核堆棧中。而當初從內核態(tài)返回用戶(hù)態(tài)時(shí),就會(huì )恢復使用用戶(hù)態(tài)的堆棧。下面分別對它們進(jìn)行說(shuō)明。


5.8.1初始化陪段

開(kāi)機初始化時(shí)(bootsect.s,setup.s)

當bootsect代碼被ROM BIOS開(kāi)機載入到實(shí)體記憶體0x7c00虞睛,并沒(méi)有設置堆棧,當然程式也沒(méi)有使用堆棧。直到bootsect被移勤到Ox9000:O處時(shí),才把堆棧段暫存器SS設置為Ox9000,堆棧指標esp暫存器設置為Oxff00,也即堆棧頂端在Ox9000:0xff00處,參見(jiàn)boot/bootsect.s第61、62行。Setup.s程式中也沿用了bootsect設置的堆棧段。這就是系統初始化時(shí)臨時(shí)使用的堆棧。


進(jìn)入保護模式時(shí)fhead.s

從head.s程式起,系統開(kāi)始正式在保護模式下執行。此時(shí)堆棧段被設置為內核資料段(0x10),堆棧指標esp設置成指向user_stack陣列的頂端(參兄head.s,第3l行),保留了1頁(yè)記憶體(4K)作為堆棧使用。user_stack陣列定義在sched.c的67- -72行,共含有1024個(gè)字。它在實(shí)體記憶體中的位置示意圖可參見(jiàn)下圖5-24所示。此時(shí)堆棧是內核程式自己使用的堆棧。其中的給出位址是大約值,它們與編譯時(shí)的實(shí)際設置參數有關(guān)。這些位址位置是從編譯內核時(shí)生成的system.map楷案中查到的。






初始化階段(main.c)

在init/main.c程式中,在執行move_to_user_mode( )代碣把控制權移交給任務(wù)0之前,系統一直使用上述堆棧。而在執行過(guò)move_to_user_mode( )之後,main.c的代碼被“切換”成任務(wù)0中孰行。透過(guò)執行fork( )系梳呼叫,main.c中的init( )將在任務(wù)1中執行,并使用任務(wù)1的堆棧。而main( )本身則在被“切換”成為任務(wù)0後,仍然繼續使用上述內核程式自己的堆棧作為任務(wù)0的用戶(hù)態(tài)堆棧。關(guān)于任務(wù)0所使用堆棧的詳細描述後請見(jiàn)后面說(shuō)明。

5.8.2任務(wù)的堆棧

每個(gè)任務(wù)都有兩個(gè)堆棧,分別用于用戶(hù)態(tài)和內核態(tài)程式的執行,并且分別稱(chēng)為用戶(hù)態(tài)堆棧和內核態(tài)堆棧。除了處于不同CPU特權極中,這兩個(gè)堆棧之間的主要區別在於任移的內核太堆棧很小,所保存的資料量最多不能超過(guò)(4096 – 任務(wù)資料結構區塊)個(gè)位元組,大約為3K位元組。而任務(wù)的 用戶(hù)態(tài)堆棧卻可以在用戶(hù)的64MB空同內延伸。


在用戶(hù)態(tài)執行時(shí)

每個(gè)任務(wù)(除了任務(wù)0任務(wù)1)有自己的64MB位址空間。當一個(gè)任務(wù)(行程)剛被建立時(shí),它的用戶(hù)態(tài)堆棧指標被設置在其位址空間的靠近末端(64MB頂端)部分。實(shí)際上末端部分還要包括執行程式的參數和環(huán)境變數,然后才是用戶(hù)堆??臻g,見(jiàn)圖5-25所示 。應用程式在用戶(hù)態(tài)下執行時(shí)就一直使用這個(gè)堆棧。堆棧實(shí)際使用的實(shí)體記憶則由CPU分頁(yè)機制確定。由於Linux實(shí)現了寫(xiě)時(shí)復制功能(Copy on Write),因此在行程被建立后,若該行程及其父行程都沒(méi)有使用堆棧,則兩者共用同一堆棧對應的實(shí)體記憶體頁(yè)面。只有當其中一個(gè)行程執行堆棧寫(xiě)操作(例如push操作)時(shí)內核記憶體管理程式才會(huì )為寫(xiě)操作行程分配新的記憶體頁(yè)面。而行程 0和行程1的用戶(hù)堆棧比較特殊,見(jiàn)后面說(shuō)明。







在內核態(tài)執行時(shí)

每個(gè)任務(wù)有其自己的內核態(tài)堆棧,用於任務(wù)在內核代碼中執行期間。其所在線(xiàn)性位址中的位置由該任務(wù)TSS段中ss0和esp0兩個(gè)欄位指定。ss0是任務(wù)內核態(tài)堆棧的段選擇符,esp0是堆棧棧底指標。因此每當任務(wù)從用戶(hù)代碼轉移進(jìn)入內核代碼中執行時(shí),任務(wù)的內核態(tài)堆??偸强盏?。任務(wù)內核態(tài)堆棧被設置在位於其任務(wù)資料結構所在頁(yè)面的末端,即與任務(wù)的任務(wù)資料結構(task_struct)放在同一頁(yè)面內。這是在建立新任務(wù)時(shí),fork( )程式在任務(wù)tss段的內核級堆棧欄位(tss.esp0和tss.ss0)中設置的,參見(jiàn)kernel/fork.c,92行:

p->tss.espO = PAGE_SIZE + (1ong)p ;
p->tss.ssO = 0x10 ;

其中p是新任務(wù)的任務(wù)資料結構指標,tss是任務(wù)狀態(tài)段結構。內核為新任務(wù)申請記憶體用作保存其task_struct結構資料,而tss結構(段)是task_struct中的一個(gè)欄位。該任務(wù)的 內核堆棧段值tss.ss0也被設置成為0x10(即內核資料段選擇符),而tss.esp0 則指向保存task_struct結構頁(yè)面的末端。見(jiàn)圖5-26所示。實(shí)際上tss.espO被設置指向該頁(yè)面(外)上一位元組處(圖中堆棧底處)。這是因為Intel CPU執行堆棧操作時(shí)是先遞減堆棧指標esp值,然后在esp指標處保存入堆棧內容。






為什麼從主記憶體區申請得來(lái)的用於保存任務(wù)資料結構的一頁(yè)記憶體也能被設置成內核資料段中的資料呢,也即tss.ss0為什麼能被設置成0x10呢? 這是因為用戶(hù)內核態(tài)堆棧仍然屬于內核資料空間。我們可以從內核代碼段的長(cháng)度范圍來(lái)說(shuō)明。在head.s程式的末端,分別設置了內核代碼段和資料段的描述符,段長(cháng)度都被設置成16MB。這個(gè)長(cháng)度值是Linux 0.12內核所能支持的最大實(shí)體記憶體長(cháng)度(參見(jiàn)head.s,110行開(kāi)始的注釋)。因此,內核代碼可以定址到整個(gè)實(shí)體記憶體范圍中的任何位置,當然也包括主記憶體區。每當任務(wù)執行內核程式而需要使用其內核堆棧時(shí),CPU就會(huì )利用TSS結構把它的內核態(tài)堆棧設置成由tss.ss0和tss.espO這兩個(gè)值構成。在任務(wù)切換時(shí),老任務(wù)的內核堆棧指標esp0不會(huì )被保存。對CPU來(lái)講,這兩個(gè)值是唯讀的。因此每當一個(gè)任務(wù)進(jìn)入內核態(tài)執行時(shí),其內核態(tài)堆??偸强?。


任務(wù)0和任務(wù)1的堆棧

任務(wù)0(空閒行程idle)和任務(wù)1(初始化行程init)的堆棧比較特殊,需要特別予以說(shuō)明。任務(wù)0和任務(wù)1的代碼段和資料段相同,限長(cháng)也都是640KB,但它們被映射到不同的線(xiàn)性位址范圍中。任務(wù)0的段基底位址從線(xiàn)性位址。開(kāi)始,而任務(wù)1的段基底位址從64MB開(kāi)始。但是它們全都映射到實(shí)體位址O- -640KB范圍中。這個(gè)位址范圍也就是內核代碼和基本資料所存放的地方,在執行了move_to_user_mode( ),任務(wù)O和任務(wù)1的內核態(tài)堆棧分別位於各自
任務(wù)資料結構所在頁(yè)面的末端,而任務(wù)0的用戶(hù)態(tài)堆棧就是前面進(jìn)入保護模式后所使用的堆棧,即sched.c的user_stack[]陣列的位置。由于任務(wù)1在建立時(shí)復制了任務(wù)0的用戶(hù)堆棧,因此剛開(kāi)始時(shí)任務(wù)0和任務(wù)l(shuí)共用使用同一個(gè)用戶(hù)堆??臻g。但是當任務(wù)1開(kāi)始執行,由于任務(wù)1映射到user_stack[]處的頁(yè)表項被設置成唯讀,使得任務(wù)l(shuí)在執行堆棧操作時(shí)將會(huì )引起寫(xiě)頁(yè)面異常,從而內核會(huì )使用寫(xiě)時(shí)復制機制²為任務(wù)1另行分配主記憶體區頁(yè)面作為堆??臻g使用。只有到此時(shí),任務(wù)1才開(kāi)始使用自己獨立的用戶(hù)堆棧記憶體頁(yè)面。因此任務(wù)0的堆棧需要在任務(wù)1實(shí)際開(kāi)始使用之前保持“干淨”,即任務(wù)0此時(shí)不能使用堆棧,以確保復制的堆棧頁(yè)面中不含有任務(wù)0的資料。

任務(wù)0的內核態(tài)堆棧是在其人工設置的初始化任務(wù)資料結構中指定的,而它的用戶(hù)態(tài)堆棧是在執行move_to_user_mode( )時(shí),在類(lèi)此iret返回之前的堆棧中設置的,參見(jiàn)圖5-22所示。我們知道,當進(jìn)行特權級會(huì )發(fā)生變化的控制權轉移時(shí),目的代碼會(huì )使用新特權級的堆棧,而原特權級代碼堆棧指標將保留在新堆棧中。因此這里先把任務(wù)0用戶(hù)堆棧指標壓入當前處於特權級O的堆棧中,同時(shí)把代碼指標也壓入堆棧,然后執行IRET指令即可實(shí)現把控制權從特權級0的代碼轉移到特權級3的任務(wù)O中。在這個(gè)人工設置內容的堆棧中,原esp值被設
置成仍然是user_stack中原來(lái)的位置值,而原ss段選擇符被設置成0x17,即設置成用戶(hù)態(tài)區域表LDT中的資料段選擇符。然后把任務(wù)0代碼段選擇符0xlf壓入堆棧作為堆棧中原CS 段的選擇符,把下一條指令的指標作為原EIP壓入堆棧。這樣,透過(guò)執行IRET指令即可“返回”到任務(wù)0的代碼中繼續執行了。


5.8.3 任務(wù)內核態(tài)堆棧與用戶(hù)態(tài)堆棧之間的切換

在Linux 0.12系統中,所有中斷服務(wù)程式部屬於內核代碼。如果一個(gè)中斷產(chǎn)生時(shí)任務(wù)正在用戶(hù)代碼中執行,那麼該中斷就會(huì )引起CPU特權級從3級到O級的變化,此時(shí)CPU就會(huì )進(jìn)行用戶(hù)態(tài)堆棧到內核態(tài)堆棧的切換操作。CPU會(huì )從當前任務(wù)的任務(wù)狀態(tài)段TSS中取得新堆棧的段選擇符和偏移值。因為中斷服務(wù)程式在內核中,屬於0級特權級代碼,所以48Bit的內核態(tài)堆棧指標會(huì )從TSS的ss0和espO欄位中獲得。在定位了新堆棧(內核態(tài)堆棧)之后,CPU就會(huì )首先把原用戶(hù)態(tài)堆棧指標ss和esp壓入內核態(tài)堆棧,隨后把標志寄存器eflags的內容和返回位置cs、eip壓入核態(tài)堆棧。

內核的系統呼叫是一個(gè)軟件中斷,因此任務(wù)呼叫系統呼叫時(shí)就會(huì )進(jìn)入內核並執行內核中的中斷服務(wù)代碼。此時(shí)內核代碼就會(huì )使用該任務(wù)的內核態(tài)堆棧進(jìn)行操作。同樣,當進(jìn)入內核程式時(shí),由於特權級別發(fā)生了改變(從用戶(hù)態(tài)轉到內核態(tài)),用戶(hù)態(tài)堆棧的堆棧段和堆棧指標以及eflags會(huì )被保存在任務(wù)的內核態(tài)堆棧中。而在執行iret退出內核程式返回到用戶(hù)程式時(shí),將恢復用戶(hù)態(tài)的堆棧和eflags。這個(gè)過(guò)程見(jiàn)圖5-27所示。







如果一個(gè)任務(wù)正在內核態(tài)中執行,那么若CPU回應中斷就不再需要進(jìn)行堆棧切換操作,因為此時(shí)該任務(wù)執行的內核代碼已經(jīng)在使用內核態(tài)堆棧,並且不涉及優(yōu)先級別的變化,所以CPU 直把eflags和中斷返回指標cs、eip壓入當前內核態(tài)堆棧,然后執行中斷服務(wù)過(guò)程。

5.9 Linux 0.12用的檔案系統

內核代碼若要正常執行就需要檔案系統的支援。用於向內核提供最基本資訊和支援的是根檔案系統,即Linux系統引導啟動(dòng)時(shí),預設使用的檔案系統是根檔案系統。其中包括作業(yè)系統最起碼的一些配置檔和命令執行程式。對於Linux系統中使用的UNIX類(lèi)檔案系釉 其中主要包括一些規定的目錄、配置檔、裝置驅動(dòng)程式、開(kāi)發(fā)程式以及所有其他用戶(hù)資料或文字檔案等。其中一般都包括以下一些子目錄和檔案:

etc/ 目錄主要含有一些系統配置檔;
dev/ 含有裝置特殊檔,用于使用檔操作語(yǔ)句操作裝置;
bin/ 存放系統執行程式。列如sh、mkfs、fdisk等;
usr/ 存放程式庫函數、手冊和其他一些文件;
usr/bin 存放用戶(hù)常用的普通命令;
var/ 用於存放系統執行時(shí)可變的資料或者是日誌等資訊。


存放檔案系統的裝置就是檔案系統裝置。比如,對于一般使用的Windows 2000作業(yè)系統,硬碟C就是檔案系統裝置,而硬碟上按一定規則存放的檔案就組成檔案系統,Windows 2000有NTFS或FAT32等檔案系統。而Linux 0.12內核所支援的檔案系統是MINIX 1.0檔案系統。目前Linux系統上使用最廣泛的則是ext2或ext3檔案系統。

對於第l章中介紹對于在軟碟上執行的Linux 0.12系統,它由簡(jiǎn)單的2張軟碟組成:bootimage磁碟和rootimage磁碟。bootimage是開(kāi)機啟動(dòng)Image檔,其中主要包括磁片開(kāi)機磁區代碼、作業(yè)系統載入程式和內核執行代碼。rootimage就是用於向內核提供最基本支援的根檔案系統。這兩個(gè)磁碟合起來(lái)就相當於一張可啟動(dòng)的DOS作業(yè)系統碟。

當linux啟動(dòng)磁碟載入根檔案系統時(shí),會(huì )根據啟動(dòng)磁碟上開(kāi)機磁區第509、51O位元組處一個(gè)字(ROOT_DEV)中的根檔案系統裝置號從指定的裝置中載入根檔案系統。如果這個(gè)裝置號是0的話(huà),則表示需要從開(kāi)機碟所在當前驅動(dòng)器中載入根檔案系統。若該裝置號是一個(gè)硬碟分區裝置號的話(huà),就會(huì )從該指定硬碟分區中載入根檔案系統。


5.10內核原始碼的目錄結構

由於Linux內核是種內核模式的系統,因此,內核中所有的程式幾乎都有緊密的關(guān)聯(lián),它們之間的依賴(lài)和呼叫關(guān)系非常密切。所以在閱讀一個(gè)原始碼檔時(shí)往往需要參閱其他相關(guān)的檔案。因此有必要在開(kāi)始閱讀內核原始碼之前,先熟悉一下原始碼檔的目錄結構和安排。

這裡我們首先列出Linux內核完整的原始碼目錄,包括其中的子目錄。然后逐一介紹各個(gè)目錄中所包含程式的主要功能,使得整個(gè)內核原始碼的安排形式能在我們的頭腦中建立起一 大概的框架,以便於下一章開(kāi)始的原始碼閱讀工作。

當我們使用tar命令將linux-0.12.tar.gz解開(kāi)時(shí),內核原始碼檔被放到了linux/目錄中。其中的目錄結構見(jiàn)圖5-28所示:






該內核版本的原始碼目錄中含有14個(gè)子目錄,總共包括102個(gè)代碼檔。下面逐個(gè)對這些子目錄中的內容進(jìn)行描述。


5.10.1 內核目錄linux
linux目錄是原始碼的主目錄,在該主目錄中除了包括所有的14個(gè)子目錄以外,還含有唯一的一個(gè)Makefile檔。該檔是編譯輔助工具軟體make的參數配置檔。make工具軟體的主要用途是透過(guò)識別哪些檔案已被修改過(guò),從而自動(dòng)地決定在一個(gè)含有多個(gè)根源程式檔的程式系統中哪些檔案需要被重新編譯。因此,make工具軟體是程式專(zhuān)案的管理軟件。

linux目錄下的這個(gè)Makefile檔還巢狀呼叫了所有子目錄中包含的Makefile檔,這樣,當linux目錄(包括子目錄)下的任何檔被修改過(guò)時(shí),make都會(huì )對其進(jìn)行重新編譯。因此為了編譯整個(gè)內核所有的原始碼檔,只要在linux目錄下執行一次make軟體即可。


5.10.2 開(kāi)機啟動(dòng)程式目錄boot
boot目錄中含有3個(gè)組合語(yǔ)言檔,是內核原始碼檔中最先被編譯的程式。這3個(gè)程式完成的主要功能是當計算機加電時(shí)開(kāi)機內核啟動(dòng),將內核代碼載入到記憶體中,並做一些進(jìn)入32位元保護執行方式前的系統初始化工作。其中bootsect.s和setup.s程式需要使用as86 軟件來(lái)編譯,使用的是as86的組合語(yǔ)言格式(與微軟的類(lèi)似),而head.s需要用GNU as來(lái)編譯,使用的是AT&T格式的組合語(yǔ)言。這兩種組合語(yǔ)言在下一章的代碼注釋裡以及代碼列表后面的說(shuō)明中會(huì )有簡(jiǎn)單的介紹。

Bootsect.s程式是磁碟開(kāi)機區塊程式,編譯后會(huì )駐留在磁碟的第一個(gè)磁區中(開(kāi)機磁區,O磁軌(柱面),0磁頭,第l個(gè)磁區)。在PC機加電ROM BIOS自檢后,將被BIOS載入到記憶體0x7C00處開(kāi)始執行。
Setup.s程式主要用于讀取機器的硬件配置參數,並把內核模組system移動(dòng)到適當的記憶體位置處。

Head.s程式會(huì )被編譯連接在system模組的最前部分,主要進(jìn)行硬體裝置的探測設置和記憶體管理頁(yè)面的初始設置工作。


5.10.3 檔案系統目錄S

Linux 0.12內核的檔案系統採用了1.0版的MINIX檔案系統,這是由於Linux是在MINIX系統上開(kāi)發(fā)的,採用MINIX檔案系統便於進(jìn)行交叉編譯,並且可以從MINIX中載入Linux分區。雖然使用的是MINIX檔案系統,但Linux對其處理方式與MINIX系統不同。主要的區別在於MINIX對檔案系統採用單執行緒處理方式,而Linux則採用了多執行緒方式。由於採用了多執行緒處理方式,Linux程式就必須處理多執行緒帶來(lái)的競爭條件、鎖死等問(wèn)題,因此Linux檔案系統代碼要比MINIX系統的復雜得多。為了避免競爭條件的發(fā)生,Linux系統對資源分配進(jìn)行了嚴格地檢查,並且在內核模式下執行時(shí),如果任務(wù)沒(méi)有主動(dòng)睡眠(呼叫sleep( )),就不讓內核切換任務(wù)。

Fs/目錄是檔案系統實(shí)現程式的目錄,共包含18個(gè)C語(yǔ)言程式。這些程式之間的主要參照引用關(guān)系見(jiàn) 5-29所示圖中每個(gè)方框代表一個(gè)檔案,從上到下按基本按引用關(guān)系放置。其中各檔案名均略去了尾碼.c,虛框中是的程式檔不屬於檔案系統,帶箭頭的線(xiàn)條表示引用關(guān)系,粗線(xiàn)條表示有相互引用關(guān)系。








由圖可以看出,該目錄中程式可以劃分成四個(gè)部分:高速緩沖區管理、低層檔操作、檔資料存取和檔高層函數,在對本目錄中檔案進(jìn)行注釋說(shuō)明時(shí),我們也將分成這四個(gè)部分來(lái)描述。

對於檔案系統,我們可以將它看成是記憶體高速緩沖區的擴展部分。所有對檔案系統中資料的存取,都需要首先讀取到高速緩沖區中。本目錄中的程式主要用來(lái)管理高速緩沖區中緩沖區塊的使用分配和區塊裝置上的檔案系統。管理高速緩沖區的程式是buffer.c,而其他程式則主要都是用於檔案系統管理。

在file table.c檔中,目前僅定義了一個(gè)檔案控制碼(描述符)結構陣列。ioctl.c檔將引用kernel/chr_drv/tty.c中的函數,實(shí)現字元裝置的io控制功能。Exec.c程式主要包含一個(gè)執行程式函數do_execve( ),它是所有exec( )函數簇中的主要函數。fcntl.c程式用于實(shí)現檔i/o控制的系統呼叫函數。read_write.c程式用於實(shí)現檔案讀/寫(xiě)和定位三個(gè)系統呼叫函數。Stat.c程式中實(shí)現了兩個(gè)獲取檔案狀態(tài)的系統呼叫函數。Open.c程式主要包含實(shí)現修改檔案屬性和建立與關(guān)閉檔案的系統呼叫函數。

char_dev.c 主要包含字元裝置讀寫(xiě)函數rw_char()。pipe.c程式中包含管道讀寫(xiě)函數和建立管道的系統呼叫。file_dev.c程式中包含基於i節點(diǎn)和描述符結構的檔案讀寫(xiě)函數。namei.c程式主要包括檔案系統中目錄名和檔案名的操作函數和系統呼叫函數。block_dev.c套件程式含塊資料讀和寫(xiě)函數。Inode.c程式中包含針對檔案系統i節點(diǎn)操作的函數。truncate.c程式用於在刪除檔案時(shí)釋放檔案所佔用的裝置資料空間。Bitmap.c程式用於處理檔案系統中i節點(diǎn)和邏輯資料區塊的點(diǎn)陣圖。super.c程式中包含對檔案系統超級區塊的處理函數。buffer.c程式主要用於對記憶體高速緩沖區進(jìn)行處理。虛框中的ll_rw_block是區塊裝置的底層讀函數,它並不在fs目錄中,而是kernel/blk_drv/ll_rw_block.c中的區塊裝置讀寫(xiě)驅動(dòng)函數。放在這里裡只是讓我們清楚的看到,檔案系統對於區塊裝置中資料的讀寫(xiě),都需要透過(guò)高速緩沖區與區塊裝置的驅動(dòng)程式(ll_rw_block())來(lái)操作來(lái)進(jìn)行,檔案系統程式集本身並不直接與區塊裝置的驅動(dòng)程式打交道。

在對程式進(jìn)行注釋過(guò)程中,我們將另外給出這些檔案中各個(gè)主要函數之間的呼叫層次關(guān)系。


5.10.4 標頭檔主目錄include

標頭檔目錄中總共有32個(gè).h標頭檔。其中主目錄下有13個(gè),asm子目錄中有4個(gè),linux子目錄中有lO個(gè),sys子目錄中有5個(gè)。這些標頭檔各自的功能見(jiàn)如下簡(jiǎn)述,具體的作用和 所包含的資訊請參見(jiàn)對標頭檔的注釋一章。


a.out標頭檔,定義了a.out執行檔格式和一些巨集。
常數符號標頭檔,目前僅定義了i節點(diǎn)中i_mode欄位的各標志位元。
字元類(lèi)號標頭檔。定義了一些有關(guān)字元類(lèi)型判斷和轉換的巨集。
錯誤號標頭檔。包含系統中各種出錯號。(Linus從minix中引進(jìn)的)。
檔案控制標頭檔。用於檔及其描述符的操作控制常數符號的定義。
信號標頭檔。定義信號符號常數,信號結構以及信號操作函數原型。
標準參數標頭檔。以巨集的形式定義變數參數列表。主要說(shuō)明了一個(gè)類(lèi)型 (va_list)和三個(gè)巨集(va_start,va_arg和va_end),用于vsprintf、vprintf’Vfprintf函數。
標準定義標頭檔。定義了NULL,offsetof(TYPE,MEMBER)。
字串標頭擋。主要定義了一些有關(guān)字串操作的嵌入函數。
終端輸入輸出標頭檔。主要定義控制非同步通信口的終端介面。
時(shí)間類(lèi)型標頭檔。其中最主要定義了tm結構和一些有關(guān)時(shí)間的函數原形。
Linux標準標頭檔。定義了各種符號常數和類(lèi)型,並聲明了各種函數。如定義了_IBRARY__,則還包括系統呼叫號和內嵌組合_syscallO( )等。
用戶(hù)時(shí)間標頭檔。定義了存取和修改時(shí)問(wèn)結構以及utime( )原型。


體系結構相關(guān)標頭檔子目錄include/asm

這些標頭檔主要定義了一些CPU體系結構密切相關(guān)的資料結構、巨集函數和變數。共4個(gè)檔案。


io標頭檔。以巨集的嵌入組合語(yǔ)言程式形式定義對io端口操作的函數。
記憶體拷貝標頭檔。含有memcpy( )嵌入式組合巨集函數。
段操作標頭檔。定義了有關(guān)段寄存器操作的嵌入式組合函數。
系統標頭檔。定義了設置或修改描述符/中斷門(mén)等的嵌入式組合巨集。


Linux內核專(zhuān)用標頭檔子目錄include/linux

內核配置標頭檔。定義鍵盤(pán)語(yǔ)言和硬碟類(lèi)型(HD_TYPE)可選項。
軟盤(pán)機標頭檔。含有軟碟控制卡參數的一些定義。
檔案系統標頭檔。定義檔案表結構(file,buffer_head,m_inode等)。
硬碟參數標頭檔。定義存取硬碟寄存器端口,狀態(tài)碼,分區表等信息。
<]inux/head.h> head標頭檔,定義了段描述符的簡(jiǎn)單結構,和幾個(gè)選擇符常數。
內核標頭檔。含有一些內核常用函數的原形定義。
記憶體管理標頭檔。含有頁(yè)面大小定義和一些頁(yè)面釋放函數原型。
程式標頭檔,定義了任務(wù)結構task_struct、初始任務(wù)0的資料。
還有一些有關(guān)描述符參數設置和獲取的嵌入式組合函數巨集式。
系統呼叫標頭檔。含有72個(gè)系統呼叫C函數處理程式,以‘sys_’開(kāi)頭。
tty標頭檔,定義了有關(guān)tty_io,串列通信方面的參數、常數。


系統專(zhuān)用資料結構子目錄include/sys

檔案狀態(tài)標頭檔。含有檔或檔案系統狀態(tài)結構stat{ }和常數。
定義了行程中執行時(shí)間結構tms以及times( )函數原型。
類(lèi)型標頭檔。定義了基本的系統資料類(lèi)型。
系統名稱(chēng)結構標頭檔o
等待呼叫標頭檔。定義系統呼叫wait( )核waitpid( )及相關(guān)常數符號。


5.10.5 內核初始化程式目錄init

該目錄中僅包含一個(gè)檔main.c。用于執行內核所有的初始化工作,然后移到用戶(hù)模式建立新行程,并在控臺裝置上執行shell程式。

程式首先根據機器記憶體的多少對緩沖區記憶體容量進(jìn)行分配,如果還設置了要使用虛擬碟,則在緩沖區記憶體后面也為它留下空間。之后就進(jìn)行所有硬件的初始化工作,包括人工建立第一個(gè)任務(wù)(task 0) ,並設置了中斷允許標志。在執行從核心態(tài)移到用戶(hù)態(tài)之后,系統第一次呼叫建立行程函數fork( ),建立出一個(gè)用於執行init( )的行程,在該子行程中,系統將進(jìn)行主控臺環(huán)境設置,並且在生成一個(gè)子行程用來(lái)執行shell程式。


5.10.6 內核程式主目黲kernel

Linux/kernel目錄中共包含12個(gè)代碼檔和一個(gè)Makefile檔,另外還有3個(gè)子目錄。所有處理任務(wù)的程式都保存在kernel/目錄中,其中包括象fork、exit、調度程式以及一些系統呼叫程式等。還包括處理中斷異常和陷阱的處理過(guò)程。子目錄中包括了低層的裝置驅動(dòng)程式,如get_hd_block和try_write等。由于這些檔中代碼之間呼叫關(guān)系復雜,因此這裡就不詳細列出各檔之間的引用關(guān)系圖,但仍然可以進(jìn)行大概分類(lèi),見(jiàn)圖5-30所示。







asm.s程式是用於處理系統硬件異常所引起的中斷,對各硬件異常的實(shí)際處理程式則是在traps.c檔中,在各個(gè)中斷處理過(guò)程中,將分別呼叫traps.c中相應的C語(yǔ)言處理函數。

exit.c程式主要包括用於處理行程終止的系統呼叫。包含行程釋放、會(huì )話(huà)(行程組)終止和程式退出處理函數以及殺死行程、終止行程、掛起行程等系統呼叫函數。

fork.c程式給出了sys_fk( )系統呼叫中使用了兩個(gè)C語(yǔ)言函數:
find_empty_process( )和copy ocess( )。

mktime.c套件程式含一個(gè)內核使用的時(shí)間函數mktime( ),用於計算從1970年1月l日0時(shí)起到開(kāi)機當日的秒數,作為開(kāi)機秒時(shí)間。僅在init/main.c中被呼叫一次。

panic.套件程式含一個(gè)顯示內核出錯資訊並停機的函數panic( )。
printk.c套件程式含一個(gè)內核專(zhuān)用資訊顯示函數printk( )。
sched.c程式中包括有關(guān)調度的基本函數(sleep_on、wakeup、schedule等)以及一些簡(jiǎn)單的系統呼叫函數。另外還有幾個(gè)與定時(shí)相關(guān)的軟碟操作函數。

signal.c程式中包括了有關(guān)信號處理的4個(gè)系統呼叫以及一個(gè)在對應的中斷處理程式中處理信號的函數do_signal( )。
sys.c程式包括很多系統呼叫函數,其中有些還沒(méi)有實(shí)現。
system_call.s程式實(shí)現了Linux系統呼叫(int Ox80)的介面處理過(guò)程,實(shí)際的處理過(guò)程則包含在各系統統呼叫相應的C語(yǔ)言處理函數中,這些處理函數分佈在整個(gè)Linux內核代碼中。

vsprintf.c程式實(shí)現了現在已經(jīng)歸入標準程式庫函數中的字串格式化函數。

區塊裝置驅動(dòng)程式子目錄kernel/blk_drv

通常情況下,用戶(hù)是透過(guò)檔案系統來(lái)存取裝置的,因此裝置驅動(dòng)程式為檔案系統實(shí)現了呼叫介面。在使用區塊裝置時(shí),由於其資料吞吐量大,為了能夠高效率地使用區塊裝置上的資料,在用戶(hù)行程與區塊裝置之間使用了高速緩沖機制。在存取區塊裝置上的資料時(shí),系統首先以資料塊的形式把區塊裝置上的資料讀入到高速緩沖區中,然后在提供給用戶(hù)。blk_drv子目錄共包含4個(gè)c檔和標頭檔。標頭檔blk.h由于是區塊裝置程式專(zhuān)用的,所以與C檔放在一起。這幾個(gè)檔之間的大致關(guān)系,見(jiàn)圖5-3l所示。







字元裝置驅動(dòng)程式子目錄kernel/chr_drv

字元裝置程式子目錄共含有4個(gè)C語(yǔ)言程式和2個(gè)組合語(yǔ)言程式檔。這些檔案實(shí)現了對序列端口rs-232、串列終端、鍵盤(pán)和主控臺終端裝置的驅動(dòng)。圖5-32是這些檔案之間的大致呼叫層關(guān)系。







tty_iov.c程式中包含tty字裝置讀函數tty_read( )和寫(xiě)函數tty_write( ),為檔案系統提供了上層存取介面。另外還包括在串列中斷處理過(guò)程中呼叫的C函數do_tty_interrupt( ),該函數將會(huì )在中斷類(lèi)型為讀字元的處理中被呼叫。

Console.c檔主要包含主控臺初始化程式和主控臺寫(xiě)函數con_write( ),用於被tty裝置呼叫。還包含對顯示器和鍵盤(pán)中斷的初始化設置程式con_init( )。

rs_io.s組合語(yǔ)言程式用于實(shí)現兩個(gè)串列介面的中斷處理程式。該中斷處理程式會(huì )根據從中斷標識寄存器(端口0x3fa或0x2fa)中取得的4種中斷類(lèi)型分別進(jìn)行處理,並在處理中斷類(lèi)型為讀字元的代碼中呼叫do_tty_interrupt( )。

serial.c用於對非同步串列通信晶片UART進(jìn)行初始化操作,並設置兩個(gè)通信端口的中斷向量。另外還包括tty用于往串口輸出的rs_write( )函數。

tty_ioctl.c程式實(shí)現了tty的io控制介面函數tty_ioctl( )以及對termio(s)終端io結構的讀寫(xiě)函數,並會(huì )在實(shí)現系統呼叫sys_ioctl( )的fs/ioctl.c程式中被呼叫。

keyboard.s程式主要實(shí)現了鍵盤(pán)中斷處理過(guò)程keyboard_interrupt。

輔助運算器模擬和操作程式子目錄kernel/math

該子目錄中目前僅有一個(gè)C程式式math_emulate.c。其中的math_emulate( )函數是中斷int7的中斷處理程式呼叫的C函數。當機器中沒(méi)有數學(xué)輔助運算器,而CPU卻又執行了輔助運算器的指令時(shí),就會(huì )引發(fā)該中斷。因此,使用該中斷就可以用軟體來(lái)模擬輔佐算器的功能。本文章所討論的內核版本還沒(méi)有包含有顯輔助運算器的模擬代碼。本程式中只是列印一條出錯資訊,並向用戶(hù)程式發(fā)送一個(gè)輔助運算器錯誤信號SIGFPE。


5.10.7內核程式庫函數目錄lib

與普通用戶(hù)程式不同,內核代碼不能使用標準C函數庫及其他一些函數庫。主要原因是由於完整的C函數庫很大。因此在內核原始碼中有專(zhuān)門(mén)一個(gè)lib/目錄提供內核需要用到的一些函數。內核函數庫用於為內核初始化程式init/main.c執行在用戶(hù)態(tài)的行程(行程0、1)提供呼叫支援。它與普通靜態(tài)庫的實(shí)現方法完全一樣。讀者可從中了解一般libc函數庫的基本組成原理。在lib/目錄中共有12個(gè)C語(yǔ)言檔,除了一個(gè)由tytso編制的malloc.c程式較長(cháng)以外,其他的程式很短,有的只有一二行代碼,實(shí)現了一些系統呼叫的介面函數。

這些檔中主要包括有退出函數_exi.t( ),關(guān)閉檔案函數close(fd)、復制檔案描述符函數dup( )、檔案開(kāi)啟函數open( )、寫(xiě)入檔案函數write( )、執行程式函數execve( )、記憶體分配由malloc( )、等待子行程狀態(tài)函數wait( )、建立會(huì )話(huà)系統呼叫setsid( )以及在include/string.h中實(shí)現的所有字串操作函數。

5.10.8 記憶體管主等程式目錄mm

該目錄包括3個(gè)代碼檔。主要用於管理程式對主記憶體區的使用,實(shí)現了行程邏輯位址到線(xiàn)性位址以及線(xiàn)性位址到實(shí)體記憶體位址的映射操作,並透過(guò)記憶體分頁(yè)管理機制,在行程的虛擬記憶體頁(yè)與主記憶體區的實(shí)體記憶體頁(yè)之間建立了對應關(guān)系,同時(shí)還真正實(shí)現了虛擬儲存技術(shù)。

Linux內核對記憶體的處理使用了分頁(yè)和分段兩種方式。首先是將386的4G虛擬位址空間分割成64個(gè)段,每個(gè)段64MB。所有內核程式佔用其中第一個(gè)段,並且實(shí)體位址與該段線(xiàn)位址相同。然后每個(gè)任務(wù)分配一個(gè)段使用。分頁(yè)機制用於把指定的實(shí)體記憶體頁(yè)面映射到段內,檢測fork建立的任何重復的拷貝,並執行寫(xiě)時(shí)復制機制。

Page.s檔包括記憶體頁(yè)面異常中斷(int 14)處理程式,主要用於處理程式由於缺頁(yè)而引起的頁(yè)面中斷和存取非法位址而引起的頁(yè)保護。

Memory.c程式包括記憶體進(jìn)行初始化的函數mem_init( ),由page.s的記憶體處理中斷程序呼叫的do_no_page( )和do_wp_page( )函數。在建立新行程而執行復制行程操作時(shí),即使用該檔中的記憶體處理函數來(lái)分配管理記憶體空間。

swap.c程式用於管理主記憶體中實(shí)體頁(yè)面和高速二級儲存(硬碟)空間之間的頁(yè)面交換。當主記憶體空間不夠用時(shí)就可以先把暫時(shí)不用的記憶體頁(yè)面保存到硬碟中。當發(fā)生缺頁(yè)異常時(shí)就首先在硬碟中查看要求的頁(yè)面是否在硬碟交換空間中,若存在則把頁(yè)面從交換空間直接讀入記憶體中。


5.10.9 編譯內核具私式目錄

該目錄下的build.c程式用于將Linux各個(gè)目錄中被分別編譯生成的目標代碼連接合併成一個(gè)可執行的內核映射檔image。其具體的功能可參見(jiàn)下一章內容。


5.11 內核系統與應用程式的關(guān)系

在Linux系統中,內核為用戶(hù)程式提供了兩方面的支援。其一是系統呼叫介面(在第5章中說(shuō)明),也即中斷呼叫int 0x80;另一方面是透過(guò)開(kāi)發(fā)環(huán)境程式庫函數或內核程式庫函數與內核進(jìn)行資訊交流。不過(guò)內核程式庫函數僅供內核建立的任務(wù)0和任務(wù)l(shuí)使用,它們最終還是去呼叫系統呼叫。因此內核對所有用戶(hù)程式或行程實(shí)際上只提供系統呼叫這一種統一的介面。lib/目錄下內核程式庫函數代碼的實(shí)現方法與基本C函數庫libc中類(lèi)似函數的實(shí)現方法基本相同,為了使用內核資源,最終都是透過(guò)內嵌組合代碼呼叫了內核系統呼叫功能,參見(jiàn)圖5-4所示。

系統呼叫主要提供給系統軟件程式設計或者用於程式庫函數的實(shí)現。而一般用戶(hù)開(kāi)發(fā)的程式則是透過(guò)呼叫象libc等庫中函數來(lái)存取內核資源。這些程式庫中的函數或資源通常被稱(chēng)為應用程式編成介面(API) 。其中定義了應用程式使用的一組標準編成介面。透過(guò)呼叫這些庫中的程式,應用程式碼能夠完成各種常用工作,例如,打開(kāi)和關(guān)閉對檔案或裝置的存取、進(jìn)行科學(xué)計算、出錯處理以及存取群組和用戶(hù)標識號ID等系統信息。


在UNIX類(lèi)作業(yè)系統中,最為普遍使用的是基於POSIX標準的API介面。Linux當然也不例外。API與系統呼叫的區別在於:為了實(shí)現某一應用程式介面標準,例如POSIX,其中的API可以與一個(gè)系統呼叫對應,也可能由幾個(gè)系統呼叫的功能共同實(shí)現。當然某些API函數可能根本就不需要使用系統呼叫,即不使用內核功能,因此函數庫可看作是實(shí)現像POSIX標準的主體介面,應用程式不用管它與系統呼叫之間到底在什麼關(guān)系。不管一個(gè)作業(yè)系統提供的系統呼叫是多麼的不同或有區別,只要它尊循同一個(gè)API標準,那麼應用程式就在這些作業(yè)系統之問(wèn)具有可攜性。


系統呼叫是內核與外界介面的最高層。在內核中,每個(gè)系統呼叫都有一個(gè)序列號(在include/unistd 標頭檔中定義),並且常以巨集的形式實(shí)現,應用程式不應該直接使用系統呼叫,因為這樣的話(huà),程式的移植性就不好了。因此目前Linux標準庫LSB(Lir Standard Base)和許多其他標準都不允許應用程式直接存取系統呼叫巨集。系統呼叫的有關(guān)文檔可參見(jiàn)Linux作業(yè)系統的線(xiàn)上手冊的第2部分。


程式庫函數一般包括C語(yǔ)言沒(méi)有提供的執行進(jìn)階功能的用戶(hù)級函數,例如輸入/輸出和字串處理函數。某些程式庫函數只是系統呼叫的增強功能版。例如,標準I/O程式庫函數fopen fclose提供了與系統呼叫open和close類(lèi)似的功能,但卻是在更高的層次上。這種情況下,系統呼叫通常能提供比程式庫函數略微好一些的性能,但是程式函數卻能提供更多的功能,而且更具檢錯能力。系統提供的程式庫函數有關(guān)文檔可參見(jiàn)作業(yè)系統的線(xiàn)上手冊第3部分。


5.12 linux/Makefile 檔

從本節起,我們開(kāi)始對內核原始碼檔進(jìn)行注釋。首先注釋linux目錄下遇到的第一個(gè)檔Makefile。后續章節將按照這裡類(lèi)似的描述結構進(jìn)行注釋。


5.12.1功能描述

Makefile檔相當于程式便宜過(guò)程中的批次檔案。是工具程式make執行時(shí)的輸入資料檔案。只要在含有Makefile的當前目錄中鍵入make命令,它就會(huì )依據Makefile檔中的設置對根源程式或目標代碼檔進(jìn)行編譯、連接或進(jìn)行安裝等活動(dòng)。

make工具程式能[ 地確定一個(gè)大程式系統中那些程式檔需要被重新編譯,並發(fā)出命令對這些程式檔進(jìn)行編譯。在使用make之前,需要編寫(xiě)Makefile資訊檔,該檔描述了整個(gè)套件程式中各程式之間的關(guān)系,並針對每個(gè)需要更新的檔案給與具體的控制命令。通常,執行程式是根據其目標檔進(jìn)行更新的,而這些目標檔則是由編譯程序盡力的。一旦編寫(xiě)好一個(gè)合適的Makefile檔,那麼在你每次修改過(guò)程式系統中的某些原始碼檔后,執行make命令就能進(jìn)行所有必要的重新編譯工作。make程式是使用Makefile資料檔案和代碼檔的最后修改時(shí)間
(1ast-modification time)來(lái)確定那些檔案需要進(jìn)行更新,對於每一個(gè)需要更新的檔它會(huì )根據Makefile中的訊發(fā)出相應的命令。在Makefile檔中,開(kāi)頭為‘#’的行是注釋行。檔案開(kāi)頭部分的‘=’代入語(yǔ)句定義了一些參數或命令的縮寫(xiě)。

這個(gè)Makefile檔的主要作用是指示make程式最終使用獨立編譯連接成的tools/目錄中的build執行程式所有內核編譯代碼連接和合併成一個(gè)可執行的內核映射檔image。具體是對tools/中的bootsect.s,setup.s使用8086組合器進(jìn)行編譯,分別生成各自的執行模組。再對原始碼中的其他所有程式使用GNU的編譯器gcc/gas進(jìn)行編譯,並鏈接模組system。最后再用build工具將這三塊組合成一個(gè)內核映射檔image。Build是由tools/build.c根源程式編譯而成的一個(gè)獨立的執行程式,它本身並沒(méi)有被編譯鏈結到內核代碼中?;揪幾g連接/組合結構如圖5-33所示。







5.12.2代碼注釋









本章小結

本章概述了Linux早期作業(yè)系統的內核模式和體系結構。首先帶出了Linux 0.12內核使用和管理記憶體的方法、內核堆棧態(tài)和用戶(hù)態(tài)堆棧的設置和使用方法、中斷機制、系統時(shí)鐘定時(shí)以及行程建立、調度和終止方法。然后根據原始碼的目錄結構形式,詳細地介紹了各個(gè)子目錄中代碼檔的基本功能和層次關(guān)系。同時(shí)說(shuō)明了Linux 0.12所使用的目標檔格式。最后從Linux內核主目錄下的makefile檔著(zhù)手,開(kāi)始對內核原始碼進(jìn)行注釋。

本章內容可以看作是對Linux 0.12內核重要資訊的歸納說(shuō)明,因此可作為閱讀后續章節的參考內容。


To be continued......
本站僅提供存儲服務(wù),所有內容均由用戶(hù)發(fā)布,如發(fā)現有害或侵權內容,請點(diǎn)擊舉報。
打開(kāi)APP,閱讀全文并永久保存 查看更多類(lèi)似文章
猜你喜歡
類(lèi)似文章
linux進(jìn)程及進(jìn)程控制
006:
深入理解Linux的系統調用
uc-OS和linux
常見(jiàn)操作系統概述總結
Linux下的多進(jìn)程編程 - fanqiang.com
更多類(lèi)似文章 >>
生活服務(wù)
分享 收藏 導長(cháng)圖 關(guān)注 下載文章
綁定賬號成功
后續可登錄賬號暢享VIP特權!
如果VIP功能使用有故障,
可點(diǎn)擊這里聯(lián)系客服!

聯(lián)系客服

欧美性猛交XXXX免费看蜜桃,成人网18免费韩国,亚洲国产成人精品区综合,欧美日韩一区二区三区高清不卡,亚洲综合一区二区精品久久