Linux的系統呼叫

來源:互聯網
上載者:User
第五章

5.5 Linux的系統呼叫

5.5.1系統呼叫介面

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

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

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

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

#define__LIBRARY__
#include

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

5.5.2系統呼叫處理過程

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

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

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

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

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

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

對於include/unistd.h中給出的每個系統呼叫巨集,都有2+2*n個參數。其中第1個參數對應系統呼叫傳回值的類型;第2個參數是系統呼叫的名稱;隨後是系統呼叫所攜帶參數的類型 名稱。這個巨集會被擴充成包含內嵌組合語句的C函數,見如下所示。

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;
}

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

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

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

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

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

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

關於Linux用戶行程向系統中斷呼叫過程傳遞參數方面,Linux系統使用了通用寄存器傳遞方法,例如寄存器ebx、ecx和edx。這種使用寄存器傳 遞參數方法的一個明顯優點就是:當進入系統中斷服務程式而儲存寄存器值時,這些傳遞參數的寄存器也被自動地放在了內核態堆棧上,因此用不著再專門對傳遞參 數的寄存器進行特殊處理。這種方法是Linus 當時所知的最簡單最快速的參數傳遞方法。另外還有一種使用Intel CPU提供的系統呼叫門(System Call gate)的參數專遞方法,它在行程用戶態堆棧和內核態堆棧自動複製傳遞的參數。但這種
方法吏用起來步驟比較複雜。

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

5.6系統時間和定時

5.6.1系統時間

為了讓作業系統能自動地準確提供目前時間和日期資訊,PC/AT微機系統中提供了用電池供電的真即時鐘RT(Real Time)電路支援。通常這部分電路與儲存系統資訊的少量CMOS RAM整合在一個晶片上,因此這部分電路被稱為RT/CMOS RAM電路。PC/AT微機或其相容機中使用了Motorola公司的MC146818晶片。

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

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

# define CURRENT_TIME(startup_time + jiffiles/HZ)

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

5.6.2 系統定時

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

時鐘中斷處理程式timer_interrupt主要用來透過jiffies變數來累計自系統啟動以來經過的時鐘滴答數。每當發生一次時鐘中斷 jiflies值就增加1。然後呼叫C語言函數do_timer( )作進一步的處理。呼叫時所帶的參數CPL是從被中斷程式的段選擇符(儲存在堆棧中的CS段寄存器值)中取得當前代碼特權級CPL。

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

¹從Linux2.4核心起,Robert Love開發出了可搶佔式的核心升級套件。這使得在核心空間低優先順序的行程也能被高優先順序行程搶佔,從而能使系統回應效能最大提高200%。參見Robert Love編著的《Linux核心開發》一書。

注意 上述計時器專門用於軟碟馬達開啟和關閉定時操作。這種計時器類似現代Linux系統中的動態計時器(Dynamic Timer),僅供內核使用。這種計時器可以在非要時動態地建立,而在定時到期時動態地撤銷。在Linux 0.12內核中計時器同時最多可以有64個。計時器的處理代碼在sched.c程式283- -368行。

5.7 Linux行程式控制制

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

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

我們已經知道,Linux系統中一個行程可以在內核態(kernel mode)或使用者態(user mode)下執行,並且分別使用各自獨立的內核態堆棧和用戶態堆棧。用戶堆疊用於行程在用戶態下臨時儲存 呼叫函數的參數、地區變數等資料;內核堆棧則含有內核程式執行函數呼叫時的資訊。

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

5.7.1 任務資料結構

內核程式透過行程表對行程進行程管理,每個行程在行程表中佔有一項。在Linux系統中,行程表項是一個task_struct任務結構指標。任務資料結 構定義在標題檔include/linux/sched.h中。有寫書上稱其為行程式控制制塊PCB(Process Control Block)或行程描述符PD (Processor Descriptor) 。其中儲存著用於控制和管理行程的所有資訊。主要包括當前執行的狀態資訊、訊號、行程號、父行程號、執行時間累計值、正在使用的檔案和本任務的地區描述符 以及任務狀態段資訊。該結構每個欄位的具體含義如下所示。


};

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

■ long counter欄位儲存著行程在被暫時停止本次執行之前還能執行的時間滴答數,即在正常情況下還需要經過幾個系統時鐘周期才切換到另一個行程。調度程式會 使用行程的counter值來選擇下一個要執行的行程,因此counter可以看作是一個行程的動態特性。在一個行程剛被建立時counter的初值等於 priority。

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

■ long signal欄位是行程當前所收到訊號的點陣圖,共32個Bit位,每個Bit位元代表一種訊號,訊號值二位元位移值 +l。因此Linux內核最多有32個訊號。在每個系統呼叫處理過程的最後,系統會使用該訊號點陣圖對訊號進行預先處理。

■ struct sigaction sigaction [32]結構陣列用來儲存處理各訊號所使用的操作和屬性。陣列的每一項對應一個訊號。

■ long blocked欄位是行程當前不想處理的訊號阻塞點陣圖。與signal欄位類似,其每一Bit位代表一種被阻塞的訊號。

■ int exit欄位是用來儲存程式終止時的退出碼。在子行程結束後父行程可以查詢它的這個退出碼。

■ unsigned long start_code欄位是行程代碼線上性空間中的開始位址。

■ unsigned long end_code欄位儲存著行程代碼的位元組長度值。

■ unsigned long end_data欄位儲存著行程的代碼長度 + 資料長度的總位元組長度值。

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

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

■ long pid是行程標識號,即行程號。它被用來唯一地標識行程。

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

■ long session是行程的會話號,即所屬會話的行程好。

■ long leader是會話首行程號。有關行程群組和會話的概念請參見第7章程式列表後的說明。

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

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

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

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

■ task_struct *p_osptr是指向比自己早建立的相鄰行程的指標。以上4個指標的關係參見圖5-20所示。在Linux 0.11內核的任務資料結構中專門有一個父行程號欄位徹father,但是0.12內核中已經不用。此時我們可以使用行程的pptr->pid來取 得父行程的行程號。

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

■ unsigned short euid是有效用戶標識號,用於指明存取檔的權力。

■ unsigned short suid是儲存的用戶標識號。當執行檔的設定用戶ID標誌。
(set-user-ID)置位元時,suid中儲存著執行檔的uido。否則suid等於行程的euid。

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

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

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

■ long timeout內核定時逾時值。

■ long alarm是行程的警示定時值(滴答數) 主系統定時中斷中會遞減該值。當使用系統呼叫alarm( ) (sched.c第338行) 設定了該值後(參數是以秒為單位,但在儲存到alarm欄位中之前內核會把它轉換為系統滴答數),那麼在經過了指定的秒數後,該值遞減為0,此時系統就會 向該行程發送一個SIGALRM訊號,預設時該訊號會終止程式的執行。當然也可以使用訊號捕捉函數(signal( )或signal ())來捕捉該訊號進行指定的操作。

■ long utime是累計行程在用戶態執行的時間(滴答數)。

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

■ long cutime是累計行程的子行程在用戶態執行的時間 (滴答數)。

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

■ struct start_time是行程產生並開始執行的時刻。

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

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

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

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

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

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

■ struct m_inode * root是行程自己的根目錄 i點節結構。每個行程都可有自己指定的根目錄,用於解析絕對路徑名。只有超級使用者能透過系統呼叫chroot來修改這個根目錄。

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

■ 在系統初始化時,在第1次呼叫執行execve()牧之前,系統建立的所有任務的executable都是0。這些任務包括任務0、任務1以及任務1直接 建立的沒有執行過execve( )的所有任務,即代碼直接包含在核心碼中的所有任務的executable都是0。因為任務0的程式碼封裝含在內核代碼中,它不是由系統從檔案系統上傳入執行 的執行檔,因此內核代碼中固定設定它的executable值為0。另外,建立新行程時,fork( )會複製父行程的任務資料結構,因此任務1的executable也是0。但在執行了exccve( )之後,executable就被賦予了被執行檔的記憶體i節點的指標,此後所有任務的該值就均不會為0 了。

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

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

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

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

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

5.7.2 行程執行狀態

一個程在其生存期內,可處於一組不同的狀態下,稱為行程狀態。見圖5-21所示。行程狀態儲存在行程任務結構的state欄位中。當行程正在等待系統中的 資源而處於等待狀態時,則稱其處於睡眠等待狀態,在Linux系統中,睡眠等待狀態被分為可中斷的和不可中斷的等待狀態。

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

可中斷睡眠狀態 (TASK_INTERRUPTIBLE)

當行程處於可中斷等待(睡眠)狀態時,系統不會調度該行程執行。當系統產生一個中斷或者釋放了行程正在等待的資源,或者行程收到一個訊號,都可以喚醒行程轉換到就緒狀態(執行狀態)。

不可中斷睡眠狀態 (TASK_UNINTERRUPTIBLE)

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

暫停狀態 (TASK_STOPPED)
當行程收到訊號SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU時就會進入暫停狀態。可向其發送SIGCONT訊號讓行程轉換到可執行狀 態。行程在除錯期間接收到任何訊號均會進入該狀態。在Linux 0.12中,還未實現對該狀態的轉換處理。處於該狀態的行程將被作為行程終止來處理。

僵死狀態(TASK ZOMBIE)
當行程已停止執行,但其父行程還沒有呼叫wait ( )詢問其狀態時,則稱該行程處於僵死狀態。為了了讓父行程能夠擷取其停止其執行的資訊,此時子行程的任務資料結構資訊還需要保留著。一旦父行程呼叫 wait ( )取得了子行程的資訊,則處於該狀態行程的任務資料結構就會被釋放掉。

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

只有當行程從“內核執行態”轉移到“睡眠狀態”時,內核才會進行行程切換操作。在內核態下執行的行程不能被其他行程搶佔,而且一個行程不能改變另一個行程門狀態。為了避免行程切換時造成內核心數據錯誤,內核在執行臨界區代碼時會禁止一切中斷。

5.7.3行程初始化

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

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

這裡需要強調的是,內核初始化是一個特殊過程,內核初始化代碼也即是任務0的代碼。從任務0資料結構中設定的初始資料可知,任務0的程式碼片段和資料段的基址 是0、段限長是640KB。而內核程式碼片段和資料段的基址是0、段限長是16MB,因此任務0的程式碼片段和資料段分別包含在核心程式碼片段和資料段中。內核初始化 程式main.c也即是任務0中的代碼,只是在移動到任務0之前系統正以內核態特權級。執行著main.c程式。巨集move_ to_user_mode的功能就是把執行特權級從內核態的0級變換到用戶態的3級,但是仍然繼續執行原來的代碼指今流。

在移動到任務0的過程中,巨集move_to_user_mode使用了中斷返回指令造成特權級改變的方法。使用這種方法進行控制權轉移是由CPU保護機 製造成的。CPU允許低級別(如特權級3)代碼透過呼叫門或中斷、陷阱門來呼叫或轉移到進階別代碼中執行,但反之則不行。因此核心採用了這種類比IRET 返回低級別代碼的方法。該方法的主要思想是在堆棧中構築中斷返回指令需要的內容,把返回位址的段選擇符設定成任務0程式碼片段選擇符,其特權級為3。此後執行 中斷返回指令iret時將導致系統CPU從特權級0跳轉到外層的特權級3上執行。
參見圖5-22所示的特權級發生變化時中斷返回堆棧結構。

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

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

5.7.4 建立新行程

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

在建立新行程的過程中,系統首先在任務陣列中找出一個還沒有被任何行程使用的空項(空槽) 。如果系統已經有64個行程在執行,則fork ( )系統呼叫會因為任務陣列表中沒有可用空項而出錯返回。然後系統為建立行程在主記憶體區中申請一頁記憶體來存放其任務資料結構資訊,並複製當前行程任務資 料結構中的所有內容作為新行程任務資料結構的範本。為了防止這個還未處理完成的建立行程被調度函數執行,此時應該立刻將新行程狀態置為不可斷的等待狀態 (TASK_UNINTERRUPTIBLE)。

隨後對複製的任務資料結構進行修改。把當前行程設定為新行程的父行程,清除訊號點陣圖並重定新行程各統計值,並設定初始執行時間片值為15個系統滴答數 (150毫秒) 。接著根據當前行程設定任務狀態段(TSS)中各寄存器的值。由於建立行程時新行程傳回值應為0,所.以需要設定tss.eax = 0。建立行程內核態堆棧指標tss.esp0被設定成新行程任務資料結構所在記憶體頁面的頂端,而堆棧段 tss.ss0被設定成內核資料段選擇符。tss.1dt被設定為地區表描述符在GDT中的索引值。如果當前行程使用了輔助運算器,則還需要把輔助運算器 的完整狀態儲存到新行程的tss.i387結構中。

此後系統設定新任務的代碼和資料段基址、限長,並複製當前行程記憶體分頁管理的頁表。注意,此時系統並不為新的行程分配實際的實體記憶體頁面,而是讓它共 用其父行程的記憶體頁面。只有當父行程或新行程中任意一個有寫記憶體操作時,系統才會為執行寫操作的行程分配相關的獨自使用的記憶體頁面。這種處理方式稱 為寫時複製(Copy On Write)技術。

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

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

5.7.5行程調度

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

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

調度程式
schedule ( )函數首先掃描任務陣列。透過比較每個就緒態(TASK_RUNNING)任務的執行時間遞減滴答計數counter的值來確定當前哪個行程執行的時間最 少。哪一個的值大,就表示執行時間還不長,於是就選中該行程,並使用任務切換巨集合函式切換到該行程執行。

如果此時所有處於TASK_RUNNING狀態行程的時間片都已經用完,系統就會根據每個行程的優先權值priority,對系統中所有行程(包括正在睡眠的行程)重新計算每個任務需要執行的時間片值counter。計算的公式是:

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

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

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

5.7.6終止行程

當一個行程結束了執行或在半途中終止了執行,那麼內核就需要釋放該行程所佔用的系統資源。這包括行程執行時開啟的檔案、申請的記憶體等。

當一個使用者程式呼叫exit ( )系統呼叫時,就會執行內核函數do_exit ( )。該函數會首先釋允許存取程程式碼片段和資料段佔用的記憶體頁面,關閉行程開啟著的所有檔,對行程使用的當前工作目錄、根目錄和執行程式的i節點進行同步操作。 如果行程有子行程,則讓init行程作為其所有子行程的父行程。如果行程是一個會話頭行程並且有控制終端,則釋放控制終端,並向屬於該會話的所有行程發送 掛斷訊號SIGHUP,這通常會終止該會話中的所有行程。然後把行程狀態置為僵死狀態TASK_ZOMBIE。並向其原父行程發送SIGCHLD訊號,通 知其某個子行程已經終止,最後do_exit ( )呼叫調度函數去執行其它行程。由此可見在行程被終止時,它的任務資料結構仍然保留著。因為其父行程還需要使用其中的資訊。

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

from: http://www.linuxunion.net/index/daima/view.htm?t=nh&id=40 

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.