午夜视频在线网站,日韩视频精品在线,中文字幕精品一区二区三区在线,在线播放精品,1024你懂我懂的旧版人,欧美日韩一级黄色片,一区二区三区在线观看视频

分享

linux內核分析筆記----進程管理

 rookie 2012-04-12

      進程和線程的概念我就不講了??傊阌浿簝群苏{度的對象是線程,而不是進程。linux系統(tǒng)中的線程很特別,它對線程和進程并不做特別區(qū)分。進程的另 外一個名字叫任務(task).我和作者一樣,習慣了把用戶空間運行的程序叫做進程,把內核中運行的程序叫做任務。

      內核把進程存放在叫做任務隊列(task list)的雙向循環(huán)鏈表中,鏈表中的每一項都是類型為task_struct,名稱叫做進程描述符(process descriptor)的結構,該結構定義在include/linux/sched.h文件中,它包含了一個具體進程的所有信息。

      linux通過slab分配器分配task_struct結構,這樣能達到對象復用和緩存著色的目的。在2.6以前的內核中,各個進程的 task_struct存放在它們內核棧的尾端。由于現(xiàn)在用slab分配器動態(tài)生成task_struct,所以只需在棧底或棧頂創(chuàng)建一個新的結構 (struct thread_info),他在asm/thread_info.h中定義,需要的請具體參考。每個任務中的thread_info結構在它的內核棧中的 尾端分配,結構中task域存放的是指向該任務實際task_struct指針。

      在內核中,訪問任務通常需要獲得指向其task_struct指針。實際上,內核中大部分處理進程的代碼都是通過task_struct進行的。通過 current宏查找到當前正在執(zhí)行的進程的進程描述符就顯得尤為重要。在x86系統(tǒng)上,current把棧指針的后13個有效位屏蔽掉,用來計算 thread_info的偏移,該操作通過current_thread_info函數完成,匯編代碼如下:

movl $-8192, %eax

andl  %esp, %eax

    最后,current再從thread_info的task域中提取并返回task_struct的值:current_thread_info()->task;

     進程描述符中的state域描述了進程的當前狀態(tài)。系統(tǒng)中的每個進程都必然處于五種進程狀態(tài)中的一種,什么運行態(tài)啦,阻塞態(tài)啦,它們之間轉化的條件啦等 等,這一點我也不細說了,為啥?隨便一本操作系統(tǒng)的書里,講得都比我好,要講就要講別人講不好的,是不?現(xiàn)在我關心的問題是:當內核需要調整某個進程的狀 態(tài)時,該怎么做?這時最好使用set_task_state(task, state)函數,該函數將指定的進程設置為指定的狀態(tài),必要的時候,它會設置內存屏蔽來強制其他處理器作重新排序。(一般只有在SMP系統(tǒng)中有此必要) 否則,它等價于:task->state = state; 另外set_current_state(state)和set_task_state(current, state)含義是等價的。

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

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

     系統(tǒng)中的每個進程必有一個父進程,每個進程也可以擁有一個或多個子進程。進程既然有父子之稱,當然就有兄弟之意了。每個task_struct都包含一個 指向其父進程task_struct且叫做parent的指針,同時包含一個稱為children的子進程鏈表。所以訪問父進程:struct task_struct *task = current->parent;按照如下方式訪問子進程:

struct task_struct *task;
struct list_head *list;
list_for_each(list, &current->children){
           task = list_entry(list, struct task_struct, sibling);
}

      其中init進程描述符是作為init_task靜態(tài)分配的。通過上面的init進程,父子進程關系,兄弟進程關系以及進程描述符的結構,我們可以得到一 個驚人的事實:可以通過這種關系從系統(tǒng)的任何一個進程出發(fā)查找到任意指定的其他進程。而且方式還挺多的,這個就看書了,內容挺多我就不說了,只是最后需要 指出的是,在一個擁有大量進程的系統(tǒng)中通過重復來遍歷所有的進程是非常耗費時間的,因此,如果沒有充足的理由千萬別這樣做。愛要一萬個理由,這么做呢,沒看出來.

      許多的操作系統(tǒng)都提供了產生進程的機制,linux這優(yōu)秀的系統(tǒng)也不例外。Unix很簡單:首先fork()通過拷貝當前進程創(chuàng)建一個子進程。子父進程的 區(qū)別僅僅在于PID,PPID和某些資源和統(tǒng)計量。然后exec()函數負責讀取可執(zhí)行文件并將其載入地址空間并執(zhí)行。從上面分析可以看出,傳統(tǒng)的 fork()系統(tǒng)調用直接把所有的資源復制給心創(chuàng)建的進程。這種方式過于簡單但效率底下。在Linux下使用了一種叫做寫時拷貝(copy-on-write)頁實現(xiàn)。這種技術原理是:內存并不復制整個進程地址空間,而是讓父進程和子進程共享同一拷貝,只有在需要寫入的時候,數據才會被復制。不懂?簡單點,就是資源的復制只是發(fā)生在需要寫入的時候才進行,在此之前,都是以只讀的方式共享。

      linux通過clone()系統(tǒng)調用實現(xiàn)fork(),通過參數標志來說父子進程共享的資源。無論是fork(),還是 vfork(),__clone()最后都根據各自需要的參數標志去調用clone().然后有clone()去調用do_fork().這樣一說,我想 大家明白我的意思了,問題的關鍵糾結于do_fork(),它定義在kernel/fork.c中,完成了大部分工作,該函數調用 copy_process()函數,然后讓進城開始運行,copy_precess()函數完成的工作很有意思:

1.調用dup_task_struct()為新進程創(chuàng)建一個內核棧,它的定義在kernel/fork.c文件中。該函數調用copy_process()函
   數。然后讓進程開始運行。從函數的名字dup就可知,此時,子進程和父進程的描述符是完全相同的。
2.檢查這個新創(chuàng)建的的子進程后,當前用戶所擁有的進程數目沒有超過給他分配的資源的限制。
3.現(xiàn)在,子進程開始使自己與父進程區(qū)別開來。進程描述符內的許多成員都要被清0或設為初始值。
4.接下來,子進程的狀態(tài)被設置為TASK_UNINTERRUPTIBLE以保證它不會投入運行。
5.調用copy_flags()以更新task_struct的flags成員,表明進程是否擁有超級用戶權限的PF_SUPERPRIV標志被清0。表
   明進程還沒有調用exec函數的PF_FORKNOEXEC標志。
6.調用get_pid()為新進程獲取一個有效的PID.
7.根據傳遞給clone()的參數標志,拷貝或共享打開的文件,文件系統(tǒng)信息,信號處理函數。進程地址空間和命名空間等。
  一般情況下,這些資源會被給定進程的所有線程共享;否則,這些資源對每個進程是不同的,因此被拷貝到這里.
8.讓父進程和子進程平分剩余的時間片
9.最后,作掃尾工作并返回一個指向子進程的指針。

      經過上面的操作,再回到do_fork()函數,如果copy_process()函數成功返回。新創(chuàng)建的子進程被喚醒并讓其投入運行。內核有意選擇子進 程先運行。因為一般子進程都會馬上調用exec()函數,這樣可以避免寫時拷貝的額外開銷。如果父進程首先執(zhí)行的話,有可能會開始向地址空間寫入。

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

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

     上面步驟的順利完成就意味著父子進程將會在各自的地址空間里運行。說句真的,通過研究發(fā)現(xiàn)這樣的開銷是降低了,但技術上不算咋優(yōu)良。

      如果說進程是80年代早上初升的太陽, 那不得不說的線程就是當前正午的烈日。線程機制提供了在同一程序內共享內存地址空間運行的一組線程。線程機制支持并發(fā)程序設計技術,可以共享打開的文件和 其他資源。如果你的系統(tǒng)是多核心的,那多線程技術可保證系統(tǒng)的真正并行。然而,有一件令人奇怪的事情,在linux中,并沒有線程這個概念,linux中 所有的線程都當作進程來處理,換句話說就是在內核中并沒有什么特殊的結構和算法來表示線程。那么,說了這多,到底在linux中啥是線程,我們說在 linux中,線程僅僅是一個使用共享資源的進程。每個線程都擁有一個隸屬于自己的task_struct.所以說線程本質上還是進程,只不過該進程可以 和其他一些進程共享某些資源信息。

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

      前邊說的主要是用戶級線程,現(xiàn)在我們接著來說說內核級線程。內核線程和用戶級線程的區(qū)別在于內核線程沒有獨立的地址空間(實際上它的mm指針被設置為 NULL).它也可以被調度也可以被搶占。內核線程也只能由其他內核線程創(chuàng)建。方法如下:int kernel_thread(int (*fn)(void *), void *arg, unsigned long flags).新的任務也是通過像普通的clone()系統(tǒng)調用傳遞特定的flags參數而創(chuàng)建的。上面函數返回時,父進程退出,并返回一個子線程 task_struct的指針。子進程開始運行fn指向的函數,arg是運行時需要用到的參數。一個特殊的clone標志CLONE_KERNEL定義了 內核線程常用到參數標志:CLONE_FS, CLONE_FILES, CLONE_SIGHAND.大部分的內核線程把這個標志傳遞給它們的flags參數。

      我雖有才,還是不如書上說的好啊,講了那么多的創(chuàng)建,出生,突然來點終結的的話, 多少有點感傷啊。但感傷歸感傷,進程終歸是要終結的。一 個進程終結時必須釋放它所占用的資源并把這一消息告訴其父進程。進程終止的方式有很多種,進程的析構發(fā)生在它調用exit()之后,即可能顯示地調用這個 系統(tǒng)調用,也可能隱式地從某個程序的主函數返回。當進程接受到它即不能處理也不能忽略的信號或異常時,它還可能被動地終結。但話說回來,不管進程怎么終 結,該任務大部分都要靠do_exit()來完成,它定義在kernel/exit.c中,具體的工作如下所示:

1.將tast_struct中的標志成員設置為PF_EXITING.
2.如果BSD的進程記賬功能是開啟的,要調用acct_process來輸出記賬信息。
3.調用__exit_mm()函數放棄進程占用的mm_struct,如果沒有別的進程使用它們即沒被共享,就徹底釋放它們。
4.調用sem_exit()函數。如果進程排隊等候IPC信號,它則離開隊列。
5.調用__exit_files(), __exit_fs(), __exit_namespace()和exit_sighand()以分別遞減文件描述符,文件系統(tǒng)數據,進程
  名字空間和信號處理函數的引用計數。當引用計數的值為0時,就代表沒有進程在使用這些資源,此時就釋放。
6.把存放在task_struct的exit_code成員中的任務退出代碼置為exit()提供的代碼中,或者去完成任何其他由內核機制
  制定的退出動作。
7.調用exit_notify()向父進程發(fā)送信號,將子進程的父進程重新設置為線程組中的其他線程或init進程,并把進程狀態(tài)
  設為TASK_ZOMBIE.
8.最后,調用schedule()切換到其他進程。

      經過上面的步驟,與進程相關的資源都被釋放掉了,它以不能夠再運行且處于TASK_ZOMBLE狀態(tài)?,F(xiàn)在它占用的所有資源就是保存 threadk_info的內核棧和保存tast_struct結構的那一小片slab。此時進程存在的唯一目的就是向它的父進程提供信息。

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

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

1.調用free_uid()來減少該進程擁有者的進程使用計數。
2.調用unhash_process()從pidhash上刪除該進程,同時也要從task_list中刪除該進程。
3.如果這個進程正在被ptrace追蹤,將追蹤進程的父進程重設為其最初的父進程并將它從ptrace_list上刪除。
4.最后,調用put_task_struct釋放進程內核棧和thread_info結構所占的頁,并釋放task_struct所占的slab高速緩存.

       至此,進程描述符和所有進程獨享的資源就全部釋放掉了。

       最后,我們討論進程相關的最后一個問題:前邊的一切看似很完美,很美好,美好讓人還怕,不是么?哪里出問題了,父進程創(chuàng)建子進程,然后子進程退出處釋放占用的資源并告訴父進程自己的PID以及退出狀態(tài)。問 題就出在這里,子進程一定能保證在父進程前邊退出么,這是沒辦法保證的,所以必須要有機制來保證子進程在這種情況下能找到一個新的父進程。否則的話,這些 成為孤兒的進程就會在退出時永遠處于僵死狀態(tài),白白的耗費內存。解決這個問題的辦法,就是給子進程在當前線程組內找一個線程作為父親,如果這樣也不行(運 氣太背了,不是)。在do_exit()會調用notify_present(),該函數會通過forget_original_parent來執(zhí)行尋父過程,具體我就不講了,講到這個詳細的地步,還不自己看看,我沒辦法了.

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

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

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多