進程和線程的概念我就不講了??傊阌浿簝群苏{度的對象是線程,而不是進程。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函數完成,匯編代碼如下:
最后,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;按照如下方式訪問子進程:
其中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()函數完成的工作很有意思:
經過上面的操作,再回到do_fork()函數,如果copy_process()函數成功返回。新創(chuàng)建的子進程被喚醒并讓其投入運行。內核有意選擇子進 程先運行。因為一般子進程都會馬上調用exec()函數,這樣可以避免寫時拷貝的額外開銷。如果父進程首先執(zhí)行的話,有可能會開始向地址空間寫入。 說完了fork,接下來說說他的兄弟---vfork(),兄弟就是兄弟,這像!兩者功能相同,不同點在于vfork()不拷貝父進程的頁表項。子進程作 為父進程的一個單獨的線程在它的地址空間里運行,父進程被阻塞,直到子進程退出或執(zhí)行exec(),子進程不能向地址空間寫入。按照剛才的方法,分析一下 vfork(),它是通過向clone()系統(tǒng)調用傳遞一個特殊標志來進行的,過程如下:
上面步驟的順利完成就意味著父子進程將會在各自的地址空間里運行。說句真的,通過研究發(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中,具體的工作如下所示:
經過上面的步驟,與進程相關的資源都被釋放掉了,它以不能夠再運行且處于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(),完成的工作如下:
至此,進程描述符和所有進程獨享的資源就全部釋放掉了。 最后,我們討論進程相關的最后一個問題:前邊的一切看似很完美,很美好,美好讓人還怕,不是么?哪里出問題了,父進程創(chuàng)建子進程,然后子進程退出處釋放占用的資源并告訴父進程自己的PID以及退出狀態(tài)。問 題就出在這里,子進程一定能保證在父進程前邊退出么,這是沒辦法保證的,所以必須要有機制來保證子進程在這種情況下能找到一個新的父進程。否則的話,這些 成為孤兒的進程就會在退出時永遠處于僵死狀態(tài),白白的耗費內存。解決這個問題的辦法,就是給子進程在當前線程組內找一個線程作為父親,如果這樣也不行(運 氣太背了,不是)。在do_exit()會調用notify_present(),該函數會通過forget_original_parent來執(zhí)行尋父過程,具體我就不講了,講到這個詳細的地步,還不自己看看,我沒辦法了. 一旦系統(tǒng)給進程成功地找到和設置了新的父進程,就不會再有出現(xiàn)駐留僵死進程的危險了,init進程會例行調用wait()來等待子進程,清除所有與其相關的僵死進程。 |
|