作者:Sandy 原創作品轉載請註明出處
《Linux核心分析》MOOC課程http://mooc.study.163.com/course/USTC-1000029000 ”
實驗環境:c+Linux64位 (32位系統可能結果會不同)
依照學術誠信條款,我保證此回答為本人原創,所有回答中引用的外部材料已經做了出處標記。 一,關於Linux進程
系統允許一個進程建立新進程,新進程即為子進程,子進程還可以建立新的子進程,形成進程樹結構模型。整個linux系統的所有進程也是一個樹形結構。樹根是系統自動構造的,即在核心態下執行的0號進程,它是所有進程的祖先。由0號進程建立1號進程(核心態),1號負責執行核心的部分初始化工作及進行系統配置,並建立若干個用於快取和虛擬主存管理的核心線程。隨後,1號進程調用execve()運行可執行程式init,並演變成使用者態1號進程,即init進程。它按照設定檔/etc/initab的要求,完成系統啟動工作,建立編號為1號、2號…的若干終端註冊進程getty。每個getty進程設定其進程組標識號,並監視配置到系統終端的介面線路。當檢測到來自終端的串連訊號時,getty進程將通過函數execve()執行註冊程式login,此時使用者就可輸入註冊名和密碼進入登入過程,如果成功,由login程式再通過函數execv()執行shell,該shell進程接收getty進程的pid,取代原來的getty進程。再由shell直接或間接地產生其他進程。
上述過程可描述為:0號進程->1號核心進程->1號核心線程->1號使用者進程(init進程)->getty進程->shell進程
注意,上述流程說明中提到:1號核心進程調用執行init並演變成1號使用者態進程(init進程),這裡前者是init是函數,後者是進程。兩者容易混淆,區別如下:
1.init()函數在核心態運行,是核心代碼
2.init進程是核心啟動並啟動並執行第一個使用者進程,運行在使用者態下。
3.init()函數調用execve()從檔案/etc/inittab中載入可執行程式init並執行,這個過程並沒有使用調用do_fork(),因此兩個進程都是1號進程。
摘自文章http://www.xuebuyuan.com/1163044.html
進程的組成主要有四個部分: 進程式控制制塊(PCB):進程標誌 進程程式塊:可與其他進程共用 進程資料區塊:進程專屬空間,用於存放各種私人資料以及堆棧空間 獨立的空間,32位Linux中為32G(如果沒有這一項則認為是線程) 二,進程建立流程
Linux中一般進程都是由現有的一個進程建立的,也就是我們所說的父進程,子進程。具體的建立是通過fork()實現的。下面就讓我們一起瞭解一下0.11核心中fork()的大體工作過程:
1)在記憶體中申請一頁記憶體存放進程式控制制塊task_struct,並返回進程號nr,並在task數組的nr處存放task_struct的指標,還要將task的當前指標current指到nr處;
2)將父進程的task_struct的內容複寫到新進程的task_struct中作為模版
3)對task_struct中的資訊進行修改,主要進行一下工作:設定父進程、清除訊號位元影像、時間片、已耗用時間、根據當前環境設定tss(核心態指標esp0指向task_struct所在頁的頂端)、設定LDT的選擇子等(根據nr指向GDT中相應的ldt描述符)。
4)設定新進程的程式碼片段、資料區段的基地址和段長:更新task_struct中的代碼開始地址:進程號(nr)×64M,更新task_struct中局部描述符表中的程式碼片段和資料區段描述符。
5)複製父進程的頁表目錄項和頁表:在頁目錄表中,複製父進程的頁表目錄項,目的地址由新進程的線性地址計算出來;對每個對應的頁表目錄項申請一個空閑頁,並用頁表地址更新頁表目錄項,最後將父進程頁表中各項複製到新進程對
應的頁表中,也就是說,這個時候,子進程與父進程共用實體記憶體。
6)更新task_struct中的檔案資訊:檔案開啟次數加1,父進程的目前的目錄引用數加1。
7)設定TSS和LDT描述符項:在通用描述元表(GDT)中設定新任務的TSS描述符項和LDT段的描述符項,使TSS描述符項和LDT描述符項分別指向task_struct的TSS結構和LDT結構。
8)將任務設定為就緒狀態,向當前進程(父進程)返回新進程號。
http://www.360doc.com/content/14/0928/15/12747488_413000182.shtml 三,clone, fork, vfork實現方式
1、fork
fork 創造的子進程複製了父親進程的資源,包括記憶體的內容task_struct內容,新舊進程使用同一程式碼片段,複製資料區段和堆棧段,這裡的複製採用了註明的copy_on_write技術,即一旦子進程開始運行,則新舊進程的地址空間已經分開,兩者運行獨立。
2、vfork
vfork函數建立的子進程完全運行在父進程的地址空間上,子進程對虛擬位址空間任何資料的修改都為父進程所見。這與fork是完全不同的,fork進程是獨立的空間。另外一點不同的是vfork建立的子進程後,父進程會被阻塞,直到子進程執行exec()和exit()。
3、clone
函數功能強大,帶了眾多參數,因此由他建立的進程要比前面2種方法要複雜。clone可以讓你有選擇性的繼承父進程的資源,你可以選擇想vfork一樣和父進程共用一個虛存空間,從而使創造的是線程,你也可以不和父進程共用,你甚至可以選擇創造出來的進程和父進程不再是父子關係,而是兄弟關係
int clone(int (fn)(void ), void *child_stack, int flags, void *arg);
這裡fn是函數指標,我們知道進程的4要素,這個就是指向程式的指標,就是所謂的“劇本”, child_stack明顯是為子進程分配系統堆棧空間(在linux下系統堆棧空間是2頁面,就是8K的記憶體,其中在這塊記憶體中,低地址上放入了值,這個值就是進程式控制制塊task_struct的值),flags就是標誌用來描述你需要從父進程繼承那些資源, arg就是傳給子進程的參數)。
總結:
相同:
系統調用服務常式sys_clone, sys_fork, sys_vfork三者最終都是調用do_fork函數完成。
do_fork的參數與clone系統調用的參數類似, 不過多了一個regs(核心棧儲存的使用者模式寄存器). 實際上其他的參數也都是用regs取的
區別在於:
clone:
clone的API外衣, 把fn, arg壓入使用者棧中, 然後引發系統調用. 返回使用者模式後下一條指令就是fn.
sysclone: parent_tidptr, child_tidptr都傳到了 do_fork的參數中
sysclone: 檢查是否有新的棧, 如果沒有就用父進程的棧 (開始地址就是regs.esp)
fork, vfork:
服務常式就是直接調用do_fork, 不過參數稍加修改
clone_flags:
sys_fork: SIGCHLD|0;
sys_vfork: SIGCHLD| (clone_vfork | clone_vm)
使用者棧: 都是父進程的棧.
parent_tidptr, child_ctidptr都是NULL.
http://www.xuebuyuan.com/1163044.html
參考文獻
http://www.xuebuyuan.com/1163044.html
http://www.360doc.com/content/14/0928/15/12747488_413000182.shtml
http://www.cnblogs.com/LittleHann/p/3853854.html
http://www.tucaobj.com/note/os/201407191030272928.jhtml
http://www.cnblogs.com/LittleHann/p/3853854.html