电竞比分网-中国电竞赛事及体育赛事平台

分享

linux內(nèi)核分析筆記----進(jìn)程管理

 rookie 2012-04-12

      進(jìn)程和線程的概念我就不講了。總之,你記著:內(nèi)核調(diào)度的對象是線程,而不是進(jìn)程。linux系統(tǒng)中的線程很特別,它對線程和進(jìn)程并不做特別區(qū)分。進(jìn)程的另 外一個名字叫任務(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文件中,它包含了一個具體進(jìn)程的所有信息。

      linux通過slab分配器分配task_struct結(jié)構(gòu),這樣能達(dá)到對象復(fù)用和緩存著色的目的。在2.6以前的內(nèi)核中,各個進(jìn)程的 task_struct存放在它們內(nèi)核棧的尾端。由于現(xiàn)在用slab分配器動態(tài)生成task_struct,所以只需在棧底或棧頂創(chuàng)建一個新的結(jié)構(gòu) (struct thread_info),他在asm/thread_info.h中定義,需要的請具體參考。每個任務(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個有效位屏蔽掉,用來計(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)中的每個進(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)整某個進(jìn)程的狀 態(tài)時,該怎么做?這時最好使用set_task_state(task, state)函數(shù),該函數(shù)將指定的進(jìn)程設(shè)置為指定的狀態(tài),必要的時候,它會設(shè)置內(nèi)存屏蔽來強(qiáng)制其他處理器作重新排序。(一般只有在SMP系統(tǒng)中有此必要) 否則,它等價于:task->state = state; 另外set_current_state(state)和set_task_state(current, state)含義是等價的。

     一般程序在用戶空間執(zhí)行。當(dāng)一個程序執(zhí)行了系統(tǒng)調(diào)用或者觸發(fā)了某個異常,它就陷入內(nèi)核空間。系統(tǒng)調(diào)用和異常處理程序是對內(nèi)核明確定義的接口,進(jìn)程只有通過這些接口才能陷入內(nèi)核執(zhí)行----對內(nèi)核的所有訪問都必須通過這些接口。

     linux進(jìn)程之間存在一個明顯的繼承關(guān)系。所有的進(jìn)程都是PID為1的init進(jìn)程的后代,內(nèi)核在系統(tǒng)啟動的最后階段啟動init進(jìn)程。該進(jìn)程讀取系統(tǒng)的初始化腳本并執(zhí)行其他的相關(guān)程序,最終完成系統(tǒng)啟動的整個過程。

     系統(tǒng)中的每個進(jìn)程必有一個父進(jìn)程,每個進(jìn)程也可以擁有一個或多個子進(jìn)程。進(jìn)程既然有父子之稱,當(dāng)然就有兄弟之意了。每個task_struct都包含一個 指向其父進(jìn)程task_struct且叫做parent的指針,同時包含一個稱為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, &current->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),我們可以得到一 個驚人的事實(shí):可以通過這種關(guān)系從系統(tǒng)的任何一個進(jìn)程出發(fā)查找到任意指定的其他進(jìn)程。而且方式還挺多的,這個就看書了,內(nèi)容挺多我就不說了,只是最后需要 指出的是,在一個擁有大量進(jìn)程的系統(tǒng)中通過重復(fù)來遍歷所有的進(jìn)程是非常耗費(fèi)時間的,因此,如果沒有充足的理由千萬別這樣做。愛要一萬個理由,這么做呢,沒看出來.

      許多的操作系統(tǒng)都提供了產(chǎn)生進(jìn)程的機(jī)制,linux這優(yōu)秀的系統(tǒng)也不例外。Unix很簡單:首先fork()通過拷貝當(dāng)前進(jìn)程創(chuàng)建一個子進(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下使用了一種叫做寫時拷貝(copy-on-write)頁實(shí)現(xiàn)。這種技術(shù)原理是:內(nèi)存并不復(fù)制整個進(jìn)程地址空間,而是讓父進(jìn)程和子進(jìn)程共享同一拷貝,只有在需要寫入的時候,數(shù)據(jù)才會被復(fù)制。不懂?簡單點(diǎn),就是資源的復(fù)制只是發(fā)生在需要寫入的時候才進(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)建一個內(nèi)核棧,它的定義在kernel/fork.c文件中。該函數(shù)調(diào)用copy_process()函
   數(shù)。然后讓進(jìn)程開始運(yùn)行。從函數(shù)的名字dup就可知,此時,子進(jìn)程和父進(jìn)程的描述符是完全相同的。
2.檢查這個新創(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以保證它不會投入運(yùn)行。
5.調(diào)用copy_flags()以更新task_struct的flags成員,表明進(jìn)程是否擁有超級用戶權(quán)限的PF_SUPERPRIV標(biāo)志被清0。表
   明進(jìn)程還沒有調(diào)用exec函數(shù)的PF_FORKNOEXEC標(biāo)志。
6.調(diào)用get_pid()為新進(jìn)程獲取一個有效的PID.
7.根據(jù)傳遞給clone()的參數(shù)標(biāo)志,拷貝或共享打開的文件,文件系統(tǒng)信息,信號處理函數(shù)。進(jìn)程地址空間和命名空間等。
  一般情況下,這些資源會被給定進(jìn)程的所有線程共享;否則,這些資源對每個進(jìn)程是不同的,因此被拷貝到這里.
8.讓父進(jìn)程和子進(jìn)程平分剩余的時間片
9.最后,作掃尾工作并返回一個指向子進(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)程都會馬上調(diào)用exec()函數(shù),這樣可以避免寫時拷貝的額外開銷。如果父進(jìn)程首先執(zhí)行的話,有可能會開始向地址空間寫入。

      說完了fork,接下來說說他的兄弟---vfork(),兄弟就是兄弟,這像!兩者功能相同,不同點(diǎn)在于vfork()不拷貝父進(jìn)程的頁表項(xiàng)。子進(jìn)程作 為父進(jìn)程的一個單獨(dú)的線程在它的地址空間里運(yùn)行,父進(jìn)程被阻塞,直到子進(jìn)程退出或執(zhí)行exec(),子進(jìn)程不能向地址空間寫入。按照剛才的方法,分析一下 vfork(),它是通過向clone()系統(tǒng)調(diào)用傳遞一個特殊標(biāo)志來進(jìn)行的,過程如下:

1.在調(diào)用copy_process時,task_struct的vfor_done成員被設(shè)置為NULL
2.在執(zhí)行do_fork()時,如果給定特別標(biāo)志,則vfork_done會指向一個特殊地址。
3.子進(jìn)程開始執(zhí)行后,父進(jìn)程不是馬上恢復(fù)執(zhí)行,而是一直等待,直到子進(jìn)程通過vfork_done指針向它發(fā)送信號。
4.在調(diào)用mm_release()時,該函數(shù)用于進(jìn)程退出內(nèi)存地址空間,如果vfork_done不為空,會向父進(jìn)程發(fā)送信號。
5.回到do_fork(),父進(jìn)程醒來并返回。

     上面步驟的順利完成就意味著父子進(jìn)程將會在各自的地址空間里運(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中,并沒有線程這個概念,linux中 所有的線程都當(dāng)作進(jìn)程來處理,換句話說就是在內(nèi)核中并沒有什么特殊的結(jié)構(gòu)和算法來表示線程。那么,說了這多,到底在linux中啥是線程,我們說在 linux中,線程僅僅是一個使用共享資源的進(jìn)程。每個線程都擁有一個隸屬于自己的task_struct.所以說線程本質(zhì)上還是進(jìn)程,只不過該進(jìn)程可以 和其他一些進(jìn)程共享某些資源信息。

      這樣一說,后面就明白了也好解決了,兩者既然屬于同一類,那創(chuàng)建的方式也是一樣的,但總要有不同啊,這個不同咋體現(xiàn)呢,這個好辦,我們在調(diào)用 clone()的時候傳遞一些參數(shù)標(biāo)志來指明需要共享的資源就可以了:clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);這段代碼產(chǎn)生的結(jié)果和調(diào)用fork()差不多,只是父子倆共享地址空間,文件系統(tǒng)資源,文件描述符和信號處理程序。換個說法就是這里的父進(jìn)程和子進(jìn) 程都叫做線程。也就是說clone()的參數(shù)決定了clone的行為,具體有哪些參數(shù),我是個懶人,也不想說了。

      前邊說的主要是用戶級線程,現(xiàn)在我們接著來說說內(nèi)核級線程。內(nèi)核線程和用戶級線程的區(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ù)返回時,父進(jìn)程退出,并返回一個子線程 task_struct的指針。子進(jìn)程開始運(yùn)行fn指向的函數(shù),arg是運(yùn)行時需要用到的參數(shù)。一個特殊的clone標(biāo)志CLONE_KERNEL定義了 內(nèi)核線程常用到參數(shù)標(biāo)志:CLONE_FS, CLONE_FILES, CLONE_SIGHAND.大部分的內(nèi)核線程把這個標(biāo)志傳遞給它們的flags參數(shù)。

      我雖有才,還是不如書上說的好啊,講了那么多的創(chuàng)建,出生,突然來點(diǎn)終結(jié)的的話, 多少有點(diǎn)感傷啊。但感傷歸感傷,進(jìn)程終歸是要終結(jié)的。一 個進(jìn)程終結(jié)時必須釋放它所占用的資源并把這一消息告訴其父進(jìn)程。進(jìn)程終止的方式有很多種,進(jìn)程的析構(gòu)發(fā)生在它調(diào)用exit()之后,即可能顯示地調(diào)用這個 系統(tǒng)調(diào)用,也可能隱式地從某個程序的主函數(shù)返回。當(dāng)進(jìn)程接受到它即不能處理也不能忽略的信號或異常時,它還可能被動地終結(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信號,它則離開隊(duì)列。
5.調(diào)用__exit_files(), __exit_fs(), __exit_namespace()和exit_sighand()以分別遞減文件描述符,文件系統(tǒng)數(shù)據(jù),進(jìn)程
  名字空間和信號處理函數(shù)的引用計(jì)數(shù)。當(dāng)引用計(jì)數(shù)的值為0時,就代表沒有進(jìn)程在使用這些資源,此時就釋放。
6.把存放在task_struct的exit_code成員中的任務(wù)退出代碼置為exit()提供的代碼中,或者去完成任何其他由內(nèi)核機(jī)制
  制定的退出動作。
7.調(diào)用exit_notify()向父進(jìn)程發(fā)送信號,將子進(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。此時進(jìn)程存在的唯一目的就是向它的父進(jìn)程提供信息。

      僵死的進(jìn)程是不能再運(yùn)行的。但系統(tǒng)仍然保留它的進(jìn)程描述符,這樣就有辦法在子進(jìn)程終結(jié)時仍可以獲得它的信息。在父進(jìn)程獲得已終結(jié)的子進(jìn)程的信息后,子進(jìn)程的task_struct結(jié)構(gòu)才被釋放。

      熟悉linux系統(tǒng)中子進(jìn)程相關(guān)知識的我們都知道在linux中有一系列wait()函數(shù),這些函數(shù)都是基于系統(tǒng)調(diào)用wait4()實(shí)現(xiàn)的。它的動作就是 掛起調(diào)用它的進(jìn)程直到其中的一個子進(jìn)程退出,此時函數(shù)會返回該退出子進(jìn)程的PID.調(diào)用該函數(shù)時提供的指針會包含子函數(shù)退出時的退出代碼。最終釋放進(jìn)程描 述符時,會調(diào)用release_task(),完成的工作如下:

1.調(diào)用free_uid()來減少該進(jìn)程擁有者的進(jìn)程使用計(jì)數(shù)。
2.調(diào)用unhash_process()從pidhash上刪除該進(jìn)程,同時也要從task_list中刪除該進(jìn)程。
3.如果這個進(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)的最后一個問題:前邊的一切看似很完美,很美好,美好讓人還怕,不是么?哪里出問題了,父進(jìn)程創(chuàng)建子進(jìn)程,然后子進(jìn)程退出處釋放占用的資源并告訴父進(jìn)程自己的PID以及退出狀態(tài)。問 題就出在這里,子進(jìn)程一定能保證在父進(jìn)程前邊退出么,這是沒辦法保證的,所以必須要有機(jī)制來保證子進(jìn)程在這種情況下能找到一個新的父進(jìn)程。否則的話,這些 成為孤兒的進(jìn)程就會在退出時永遠(yuǎn)處于僵死狀態(tài),白白的耗費(fèi)內(nèi)存。解決這個問題的辦法,就是給子進(jìn)程在當(dāng)前線程組內(nèi)找一個線程作為父親,如果這樣也不行(運(yùn) 氣太背了,不是)。在do_exit()會調(diào)用notify_present(),該函數(shù)會通過forget_original_parent來執(zhí)行尋父過程,具體我就不講了,講到這個詳細(xì)的地步,還不自己看看,我沒辦法了.

       一旦系統(tǒng)給進(jìn)程成功地找到和設(shè)置了新的父進(jìn)程,就不會再有出現(xiàn)駐留僵死進(jìn)程的危險(xiǎn)了,init進(jìn)程會例行調(diào)用wait()來等待子進(jìn)程,清除所有與其相關(guān)的僵死進(jìn)程。

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多