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