進(jìn)程和線(xiàn)程的概念我就不講了??傊?,你記著(zhù):內核調度的對象是線(xiàn)程,而不是進(jìn)程。linux系統中的線(xiàn)程很特別,它對線(xiàn)程和進(jìn)程并不做特別區分。進(jìn)程的另外一個(gè)名字叫任務(wù)(task).我和作者一樣,習慣了把用戶(hù)空間運行的程序叫做進(jìn)程,把內核中運行的程序叫做任務(wù)。
內核把進(jìn)程存放在叫做任務(wù)隊列(task list)的雙向循環(huán)鏈表中,鏈表中的每一項都是類(lèi)型為task_struct,名稱(chēng)叫做進(jìn)程描述符(process descriptor)的結構,該結構定義在include/linux/sched.h文件中,它包含了一個(gè)具體進(jìn)程的所有信息。
linux通過(guò)slab分配器分配task_struct結構,這樣能達到對象復用和緩存著(zhù)色的目的。在2.6以前的內核中,各個(gè)進(jìn)程的task_struct存放在它們內核棧的尾端。由于現在用slab分配器動(dòng)態(tài)生成task_struct,所以只需在棧底或棧頂創(chuàng )建一個(gè)新的結構(struct thread_info),他在asm/thread_info.h中定義,需要的請具體參考。每個(gè)任務(wù)中的thread_info結構在它的內核棧中的尾端分配,結構中task域存放的是指向該任務(wù)實(shí)際task_struct指針。
在內核中,訪(fǎng)問(wèn)任務(wù)通常需要獲得指向其task_struct指針。實(shí)際上,內核中大部分處理進(jìn)程的代碼都是通過(guò)task_struct進(jìn)行的。通過(guò)current宏查找到當前正在執行的進(jìn)程的進(jìn)程描述符就顯得尤為重要。在x86系統上,current把棧指針的后13個(gè)有效位屏蔽掉,用來(lái)計算thread_info的偏移,該操作通過(guò)current_thread_info函數完成,匯編代碼如下:
movl $-8192, %eax
andl %esp, %eax
最后,current再從thread_info的task域中提取并返回task_struct的值:current_thread_info()->task;
進(jìn)程描述符中的state域描述了進(jìn)程的當前狀態(tài)。系統中的每個(gè)進(jìn)程都必然處于五種進(jìn)程狀態(tài)中的一種,什么運行態(tài)啦,阻塞態(tài)啦,它們之間轉化的條件啦等等,這一點(diǎn)我也不細說(shuō)了,為啥?隨便一本操作系統的書(shū)里,講得都比我好,要講就要講別人講不好的,是不?現在我關(guān)心的問(wèn)題是:當內核需要調整某個(gè)進(jìn)程的狀態(tài)時(shí),該怎么做?這時(shí)最好使用set_task_state(task, state)函數,該函數將指定的進(jìn)程設置為指定的狀態(tài),必要的時(shí)候,它會(huì )設置內存屏蔽來(lái)強制其他處理器作重新排序。(一般只有在SMP系統中有此必要)否則,它等價(jià)于:task->state = state; 另外set_current_state(state)和set_task_state(current, state)含義是等價(jià)的。
一般程序在用戶(hù)空間執行。當一個(gè)程序執行了系統調用或者觸發(fā)了某個(gè)異常,它就陷入內核空間。系統調用和異常處理程序是對內核明確定義的接口,進(jìn)程只有通過(guò)這些接口才能陷入內核執行----對內核的所有訪(fǎng)問(wèn)都必須通過(guò)這些接口。
linux進(jìn)程之間存在一個(gè)明顯的繼承關(guān)系。所有的進(jìn)程都是PID為1的init進(jìn)程的后代,內核在系統啟動(dòng)的最后階段啟動(dòng)init進(jìn)程。該進(jìn)程讀取系統的初始化腳本并執行其他的相關(guān)程序,最終完成系統啟動(dòng)的整個(gè)過(guò)程。
系統中的每個(gè)進(jìn)程必有一個(gè)父進(jìn)程,每個(gè)進(jìn)程也可以擁有一個(gè)或多個(gè)子進(jìn)程。進(jìn)程既然有父子之稱(chēng),當然就有兄弟之意了。每個(gè)task_struct都包含一個(gè)指向其父進(jìn)程task_struct且叫做parent的指針,同時(shí)包含一個(gè)稱(chēng)為children的子進(jìn)程鏈表。所以訪(fǎng)問(wèn)父進(jìn)程:struct task_struct *task = current->parent;按照如下方式訪(fǎng)問(wèn)子進(jìn)程:
struct task_struct *task;
struct list_head *list;
list_for_each(list, ¤t->children){
task = list_entry(list, struct task_struct, sibling);
}
其中init進(jìn)程描述符是作為init_task靜態(tài)分配的。通過(guò)上面的init進(jìn)程,父子進(jìn)程關(guān)系,兄弟進(jìn)程關(guān)系以及進(jìn)程描述符的結構,我們可以得到一個(gè)驚人的事實(shí):可以通過(guò)這種關(guān)系從系統的任何一個(gè)進(jìn)程出發(fā)查找到任意指定的其他進(jìn)程。而且方式還挺多的,這個(gè)就看書(shū)了,內容挺多我就不說(shuō)了,只是最后需要指出的是,在一個(gè)擁有大量進(jìn)程的系統中通過(guò)重復來(lái)遍歷所有的進(jìn)程是非常耗費時(shí)間的,因此,如果沒(méi)有充足的理由千萬(wàn)別這樣做。愛(ài)要一萬(wàn)個(gè)理由,這么做呢,沒(méi)看出來(lái).
許多的操作系統都提供了產(chǎn)生進(jìn)程的機制,linux這優(yōu)秀的系統也不例外。Unix很簡(jiǎn)單:首先f(wàn)ork()通過(guò)拷貝當前進(jìn)程創(chuàng )建一個(gè)子進(jìn)程。子父進(jìn)程的區別僅僅在于PID,PPID和某些資源和統計量。然后exec()函數負責讀取可執行文件并將其載入地址空間并執行。從上面分析可以看出,傳統的fork()系統調用直接把所有的資源復制給心創(chuàng )建的進(jìn)程。這種方式過(guò)于簡(jiǎn)單但效率底下。在Linux下使用了一種叫做寫(xiě)時(shí)拷貝(copy-on-write)頁(yè)實(shí)現。這種技術(shù)原理是:內存并不復制整個(gè)進(jìn)程地址空間,而是讓父進(jìn)程和子進(jìn)程共享同一拷貝,只有在需要寫(xiě)入的時(shí)候,數據才會(huì )被復制。不懂?簡(jiǎn)單點(diǎn),就是資源的復制只是發(fā)生在需要寫(xiě)入的時(shí)候才進(jìn)行,在此之前,都是以只讀的方式共享。
linux通過(guò)clone()系統調用實(shí)現fork(),通過(guò)參數標志來(lái)說(shuō)父子進(jìn)程共享的資源。無(wú)論是fork(),還是vfork(),__clone()最后都根據各自需要的參數標志去調用clone().然后有clone()去調用do_fork().這樣一說(shuō),我想大家明白我的意思了,問(wèn)題的關(guān)鍵糾結于do_fork(),它定義在kernel/fork.c中,完成了大部分工作,該函數調用copy_process()函數,然后讓進(jìn)城開(kāi)始運行,copy_precess()函數完成的工作很有意思:
1.調用dup_task_struct()為新進(jìn)程創(chuàng )建一個(gè)內核棧,它的定義在kernel/fork.c文件中。該函數調用copy_process()函
數。然后讓進(jìn)程開(kāi)始運行。從函數的名字dup就可知,此時(shí),子進(jìn)程和父進(jìn)程的描述符是完全相同的。
2.檢查這個(gè)新創(chuàng )建的的子進(jìn)程后,當前用戶(hù)所擁有的進(jìn)程數目沒(méi)有超過(guò)給他分配的資源的限制。
3.現在,子進(jìn)程開(kāi)始使自己與父進(jìn)程區別開(kāi)來(lái)。進(jìn)程描述符內的許多成員都要被清0或設為初始值。
4.接下來(lái),子進(jìn)程的狀態(tài)被設置為T(mén)ASK_UNINTERRUPTIBLE以保證它不會(huì )投入運行。
5.調用copy_flags()以更新task_struct的flags成員,表明進(jìn)程是否擁有超級用戶(hù)權限的PF_SUPERPRIV標志被清0。表
明進(jìn)程還沒(méi)有調用exec函數的PF_FORKNOEXEC標志。
6.調用get_pid()為新進(jìn)程獲取一個(gè)有效的PID.
7.根據傳遞給clone()的參數標志,拷貝或共享打開(kāi)的文件,文件系統信息,信號處理函數。進(jìn)程地址空間和命名空間等。
一般情況下,這些資源會(huì )被給定進(jìn)程的所有線(xiàn)程共享;否則,這些資源對每個(gè)進(jìn)程是不同的,因此被拷貝到這里.
8.讓父進(jìn)程和子進(jìn)程平分剩余的時(shí)間片
9.最后,作掃尾工作并返回一個(gè)指向子進(jìn)程的指針。
經(jīng)過(guò)上面的操作,再回到do_fork()函數,如果copy_process()函數成功返回。新創(chuàng )建的子進(jìn)程被喚醒并讓其投入運行。內核有意選擇子進(jìn)程先運行。因為一般子進(jìn)程都會(huì )馬上調用exec()函數,這樣可以避免寫(xiě)時(shí)拷貝的額外開(kāi)銷(xiāo)。如果父進(jìn)程首先執行的話(huà),有可能會(huì )開(kāi)始向地址空間寫(xiě)入。
說(shuō)完了fork,接下來(lái)說(shuō)說(shuō)他的兄弟---vfork(),兄弟就是兄弟,這像!兩者功能相同,不同點(diǎn)在于vfork()不拷貝父進(jìn)程的頁(yè)表項。子進(jìn)程作為父進(jìn)程的一個(gè)單獨的線(xiàn)程在它的地址空間里運行,父進(jìn)程被阻塞,直到子進(jìn)程退出或執行exec(),子進(jìn)程不能向地址空間寫(xiě)入。按照剛才的方法,分析一下vfork(),它是通過(guò)向clone()系統調用傳遞一個(gè)特殊標志來(lái)進(jìn)行的,過(guò)程如下:
1.在調用copy_process時(shí),task_struct的vfor_done成員被設置為NULL
2.在執行do_fork()時(shí),如果給定特別標志,則vfork_done會(huì )指向一個(gè)特殊地址。
3.子進(jìn)程開(kāi)始執行后,父進(jìn)程不是馬上恢復執行,而是一直等待,直到子進(jìn)程通過(guò)vfork_done指針向它發(fā)送信號。
4.在調用mm_release()時(shí),該函數用于進(jìn)程退出內存地址空間,如果vfork_done不為空,會(huì )向父進(jìn)程發(fā)送信號。
5.回到do_fork(),父進(jìn)程醒來(lái)并返回。
上面步驟的順利完成就意味著(zhù)父子進(jìn)程將會(huì )在各自的地址空間里運行。說(shuō)句真的,通過(guò)研究發(fā)現這樣的開(kāi)銷(xiāo)是降低了,但技術(shù)上不算咋優(yōu)良。
如果說(shuō)進(jìn)程是80年代早上初升的太陽(yáng), 那不得不說(shuō)的線(xiàn)程就是當前正午的烈日。線(xiàn)程機制提供了在同一程序內共享內存地址空間運行的一組線(xiàn)程。線(xiàn)程機制支持并發(fā)程序設計技術(shù),可以共享打開(kāi)的文件和其他資源。如果你的系統是多核心的,那多線(xiàn)程技術(shù)可保證系統的真正并行。然而,有一件令人奇怪的事情,在linux中,并沒(méi)有線(xiàn)程這個(gè)概念,linux中所有的線(xiàn)程都當作進(jìn)程來(lái)處理,換句話(huà)說(shuō)就是在內核中并沒(méi)有什么特殊的結構和算法來(lái)表示線(xiàn)程。那么,說(shuō)了這多,到底在linux中啥是線(xiàn)程,我們說(shuō)在linux中,線(xiàn)程僅僅是一個(gè)使用共享資源的進(jìn)程。每個(gè)線(xiàn)程都擁有一個(gè)隸屬于自己的task_struct.所以說(shuō)線(xiàn)程本質(zhì)上還是進(jìn)程,只不過(guò)該進(jìn)程可以和其他一些進(jìn)程共享某些資源信息。
這樣一說(shuō),后面就明白了也好解決了,兩者既然屬于同一類(lèi),那創(chuàng )建的方式也是一樣的,但總要有不同啊,這個(gè)不同咋體現呢,這個(gè)好辦,我們在調用clone()的時(shí)候傳遞一些參數標志來(lái)指明需要共享的資源就可以了:clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);這段代碼產(chǎn)生的結果和調用fork()差不多,只是父子倆共享地址空間,文件系統資源,文件描述符和信號處理程序。換個(gè)說(shuō)法就是這里的父進(jìn)程和子進(jìn)程都叫做線(xiàn)程。也就是說(shuō)clone()的參數決定了clone的行為,具體有哪些參數,我是個(gè)懶人,也不想說(shuō)了。
前邊說(shuō)的主要是用戶(hù)級線(xiàn)程,現在我們接著(zhù)來(lái)說(shuō)說(shuō)內核級線(xiàn)程。內核線(xiàn)程和用戶(hù)級線(xiàn)程的區別在于內核線(xiàn)程沒(méi)有獨立的地址空間(實(shí)際上它的mm指針被設置為NULL).它也可以被調度也可以被搶占。內核線(xiàn)程也只能由其他內核線(xiàn)程創(chuàng )建。方法如下:int kernel_thread(int (*fn)(void *), void *arg, unsigned long flags).新的任務(wù)也是通過(guò)像普通的clone()系統調用傳遞特定的flags參數而創(chuàng )建的。上面函數返回時(shí),父進(jìn)程退出,并返回一個(gè)子線(xiàn)程task_struct的指針。子進(jìn)程開(kāi)始運行fn指向的函數,arg是運行時(shí)需要用到的參數。一個(gè)特殊的clone標志CLONE_KERNEL定義了內核線(xiàn)程常用到參數標志:CLONE_FS, CLONE_FILES, CLONE_SIGHAND.大部分的內核線(xiàn)程把這個(gè)標志傳遞給它們的flags參數。
我雖有才,還是不如書(shū)上說(shuō)的好啊,講了那么多的創(chuàng )建,出生,突然來(lái)點(diǎn)終結的的話(huà), 多少有點(diǎn)感傷啊。但感傷歸感傷,進(jìn)程終歸是要終結的。一個(gè)進(jìn)程終結時(shí)必須釋放它所占用的資源并把這一消息告訴其父進(jìn)程。進(jìn)程終止的方式有很多種,進(jìn)程的析構發(fā)生在它調用exit()之后,即可能顯示地調用這個(gè)系統調用,也可能隱式地從某個(gè)程序的主函數返回。當進(jìn)程接受到它即不能處理也不能忽略的信號或異常時(shí),它還可能被動(dòng)地終結。但話(huà)說(shuō)回來(lái),不管進(jìn)程怎么終結,該任務(wù)大部分都要靠do_exit()來(lái)完成,它定義在kernel/exit.c中,具體的工作如下所示:
1.將tast_struct中的標志成員設置為PF_EXITING.
2.如果BSD的進(jìn)程記賬功能是開(kāi)啟的,要調用acct_process來(lái)輸出記賬信息。
3.調用__exit_mm()函數放棄進(jìn)程占用的mm_struct,如果沒(méi)有別的進(jìn)程使用它們即沒(méi)被共享,就徹底釋放它們。
4.調用sem_exit()函數。如果進(jìn)程排隊等候IPC信號,它則離開(kāi)隊列。
5.調用__exit_files(), __exit_fs(), __exit_namespace()和exit_sighand()以分別遞減文件描述符,文件系統數據,進(jìn)程
名字空間和信號處理函數的引用計數。當引用計數的值為0時(shí),就代表沒(méi)有進(jìn)程在使用這些資源,此時(shí)就釋放。
6.把存放在task_struct的exit_code成員中的任務(wù)退出代碼置為exit()提供的代碼中,或者去完成任何其他由內核機制
制定的退出動(dòng)作。
7.調用exit_notify()向父進(jìn)程發(fā)送信號,將子進(jìn)程的父進(jìn)程重新設置為線(xiàn)程組中的其他線(xiàn)程或init進(jìn)程,并把進(jìn)程狀態(tài)
設為T(mén)ASK_ZOMBIE.
8.最后,調用schedule()切換到其他進(jìn)程。
經(jīng)過(guò)上面的步驟,與進(jìn)程相關(guān)的資源都被釋放掉了,它以不能夠再運行且處于TASK_ZOMBLE狀態(tài)?,F在它占用的所有資源就是保存threadk_info的內核棧和保存tast_struct結構的那一小片slab。此時(shí)進(jìn)程存在的唯一目的就是向它的父進(jìn)程提供信息。
僵死的進(jìn)程是不能再運行的。但系統仍然保留它的進(jìn)程描述符,這樣就有辦法在子進(jìn)程終結時(shí)仍可以獲得它的信息。在父進(jìn)程獲得已終結的子進(jìn)程的信息后,子進(jìn)程的task_struct結構才被釋放。
熟悉linux系統中子進(jìn)程相關(guān)知識的我們都知道在linux中有一系列wait()函數,這些函數都是基于系統調用wait4()實(shí)現的。它的動(dòng)作就是掛起調用它的進(jìn)程直到其中的一個(gè)子進(jìn)程退出,此時(shí)函數會(huì )返回該退出子進(jìn)程的PID.調用該函數時(shí)提供的指針會(huì )包含子函數退出時(shí)的退出代碼。最終釋放進(jìn)程描述符時(shí),會(huì )調用release_task(),完成的工作如下:
1.調用free_uid()來(lái)減少該進(jìn)程擁有者的進(jìn)程使用計數。
2.調用unhash_process()從pidhash上刪除該進(jìn)程,同時(shí)也要從task_list中刪除該進(jìn)程。
3.如果這個(gè)進(jìn)程正在被ptrace追蹤,將追蹤進(jìn)程的父進(jìn)程重設為其最初的父進(jìn)程并將它從ptrace_list上刪除。
4.最后,調用put_task_struct釋放進(jìn)程內核棧和thread_info結構所占的頁(yè),并釋放task_struct所占的slab高速緩存.
至此,進(jìn)程描述符和所有進(jìn)程獨享的資源就全部釋放掉了。
最后,我們討論進(jìn)程相關(guān)的最后一個(gè)問(wèn)題:前邊的一切看似很完美,很美好,美好讓人還怕,不是么?哪里出問(wèn)題了,父進(jìn)程創(chuàng )建子進(jìn)程,然后子進(jìn)程退出處釋放占用的資源并告訴父進(jìn)程自己的PID以及退出狀態(tài)。問(wèn)題就出在這里,子進(jìn)程一定能保證在父進(jìn)程前邊退出么,這是沒(méi)辦法保證的,所以必須要有機制來(lái)保證子進(jìn)程在這種情況下能找到一個(gè)新的父進(jìn)程。否則的話(huà),這些成為孤兒的進(jìn)程就會(huì )在退出時(shí)永遠處于僵死狀態(tài),白白的耗費內存。解決這個(gè)問(wèn)題的辦法,就是給子進(jìn)程在當前線(xiàn)程組內找一個(gè)線(xiàn)程作為父親,如果這樣也不行(運氣太背了,不是)。在do_exit()會(huì )調用notify_present(),該函數會(huì )通過(guò)forget_original_parent來(lái)執行尋父過(guò)程,具體我就不講了,講到這個(gè)詳細的地步,還不自己看看,我沒(méi)辦法了.
一旦系統給進(jìn)程成功地找到和設置了新的父進(jìn)程,就不會(huì )再有出現駐留僵死進(jìn)程的危險了,init進(jìn)程會(huì )例行調用wait()來(lái)等待子進(jìn)程,清除所有與其相關(guān)的僵死進(jìn)程。