fork()介紹

fork()函數:
用於創建一個進程,所創建的進程複製父進程的代碼段/數據段/BSS段/堆/棧等所有用戶空間信息;在內核中操作系統重新爲其申請了一個PCB,並使用父進程的PCB進行初始化;
子進程執行的位置是fork()函數執行後的代碼處,猜想是複製了父進程的PC指針給子進程。

例題:

#include "stdio.h"
#include "sys/types.h"
#include "unistd.h"

int main()
{
    pid_t pid1;
    pid_t pid2;

    pid1 = fork();
    pid2 = fork();

    printf("pid1:%d, pid2:%d\n", pid1, pid2);
}

要求如下:
已知從這個程序執行到這個程序的所有進程結束這個時間段內,沒有其它新進程執行。

  1、請說出執行這個程序後,將一共運行幾個進程。

  2、如果其中一個進程的輸出結果是“pid1:1001, pid2:1002”,寫出其他進程的輸出結果(不考慮進程執行順序)。

1/假設父進程(main所在的進程)爲P0,經兩次fork()函數,會創建它的2個子進程P1/P2;子進程P1創建後,從fork()執行後的代碼處開始執行,即執行代碼段

pid1:1001, pid2:1002
pid1:0, pid2:1002
pid1:1001, pid2:0
pid1:0, pid2:0

fork()具體步驟如下:
第一階段:打開目標映像文件

第二階段:創建內核中的進程對象

第三階段:創建初始線程

第四階段:通知windows子系統進程csrss.exe進程來對新進程進行管理

第五階段:啓動初始線程

第六階段:用戶空間的初始化和Dll連接

具體內容:

在Windows中,CreateProcess要先通過系統調用NtCreateProcess創建進程,成功以後就立即通過系統調用NtCreateThread創建其第一個線程。

第一階段:打開目標映像文件

首先用CreateProcess(實際上是CreateProcessW)打開指定的可執行映像文件,並創建一個內存區對象。注意,內存區對象並沒有被映射到內存中(由於目標進程尚未建立起來,不可能完成內存映射),但它確實是打開了。

第二階段:創建內核中的進程對象

實際上就是創建以EPROCESS爲核心的相關數據結構,主要包括:

調用內核中的NtCreateProcessEx 系統服務,實際的調用過程是這樣的:kernel32.dll 中的CreateProcessW調用ntdll.dll 中的存根函數NtCreateProcessEx,而ntdll.dll的NtCreateProcessEx 利用處理器的陷阱機制切換到內核模式下;在內核模式下,系統服務分發函數KiSystemService 獲得控制,它利用當前線程指定的系統服務表,調用到執行體層的NtCreateProcessEx 函數。然後,執行體層的NtCreateProcessEx 函數執行前面介紹的進程創建邏輯,包括創建EPROCESS 對象、初始化其中的域、創建初始的進程地址空間、創建和初始化句柄表,並設置好EPROCESS 和KPROCESS 中的各種屬性,如進程優先級、安全屬性、創建時間等。到這裏,執行體層的進程對象已經建立起來,進程的地址空間已經初始化,並且EPROCESS 中的PEB 也已初始化。

第三階段:創建初始線程

這個階段是通過調用NtCreateThread()完成的,主要包括: 現在,雖然進程對象已經建立起來,但是它沒有線程,所以,它自己還不能做任何事情。接下來需要創建一個初始線程,在此之前,首先要構造一個棧以及一個可供運行的環境。初始線程的棧的大小可以通過映像文件獲得,而創建線程則可以通過調用ntdll.dll 中的NtCreateThread 函數來完成。 創建和設置目標線程的ETHREAD數據結構,並處理好與EPROCESS的關係(例如進程塊中的線程計數等等)。 在目標進程的用戶空間創建並設置目標線程的TEB。 將目標線程在用戶空間的起始地址設置成指向Kernel32.dll中的BaseProcessStart()或BaseThreadStart(),前者用於進程中的第一個線程,後者用於隨後的線程。 用戶程序在調用NtCreateThread()時也要提供一個用戶級的起始函數(地址), BaseProcessStart()和BaseThreadStart()在完成初始化時會調用這個起始函數。 ETHREAD數據結構中有兩個成份,分別用來存放這兩個地址。 調用KeInitThread設置目標線程的KTHREAD數據結構併爲其分配堆棧和建立執行環境。   特別地,將其上下文中的斷點(返回點)設置成指向內核中的一段程序KiThreadStartup,使得該線程一旦被調度運行時就從這裏開始執行。 系統中可能登記了一些每當創建線程時就應加以調用的“通知”函數,調用這些函數。

第四階段:通知windows子系統

每個進程在創建/退出的時候都要向windows子系統進程csrss.exe進程發出通知,因爲它擔負着對windows所有進程的管理的責任, 注意,這裏發出通知的是CreateProcess的調用者,不是新建出來的進程,因爲它還沒有開始運行。

至此,CreateProcess的操作已經完成,但子進程中的線程卻尚未開始運行,它的運行還要經歷下面的第五和第六階段。

第五階段:啓動初始線程

在內核中,新線程的啓動例程是KiThreadStartup函數,這是當PspCreateThread 調用KeInitThread 函數時,KeInitThread 函數調用KiInitializeContextThread(參見base\ntos\ke\i386\thredini.c 文件)來設置的。

KiThreadStartup 函數首先將IRQL 降低到APC_LEVEL,然後調用系統初始的線程函數PspUserThreadStartup。這裏的PspUserThreadStartup 函數是PspCreateThread 函數在調用KeInitThread 時指定的,。注意,PspCreateThread函數在創建系統線程時指定的初始線程函數爲PspSystemThreadStartup 。線程啓動函數被作爲一個參數傳遞給PspUserThreadStartup,在這裏,它應該是kernel32.dll 中的BaseProcessStart。

PspUserThreadStartup 函數被調用。邏輯並不複雜,但是涉及異步函數調用(APC)機制。

新創建的線程未必是可以被立即調度運行的,因爲用戶可能在創建時把標誌位CREATE_ SUSPENDED設成了1; 如果那樣的話,就需要等待別的進程通過系統調用恢復其運行資格以後纔可以被調度運行。否則現在已經可以被調度運行了。至於什麼時候纔會被調度運行,則就要看優先級等等條件了。

第六階段:用戶空間的初始化和Dll連接

PspUserThreadStartup 函數返回以後,KiThreadStartup 函數返回到用戶模式,此時,PspUserThreadStartup 插入的APC 被交付,於是LdrInitializeThunk 函數被調用,這是映像加載器(image loader)的初始化函數。LdrInitializeThunk 函數完成加載器、堆管理器等初始化工作,然後加載任何必要的DLL,並且調用這些DLL 的入口函數。最後,當LdrInitializeThunk 返回到用戶模式APC 分發器時,該線程開始在用戶模式下執行,調用應用程序指定的線程啓動函數,此啓動函數的地址已經在APC 交付時被壓到用戶棧中。

DLL連接由ntdll.dll中的LdrInitializeThunk()在用戶空間完成。在此之前ntdll.dll與應用軟件尚未連接,但是已經被映射到了用戶空間 函數LdrInitializeThunk()在映像中的位置是系統初始化時就預先確定並記錄在案的,所以在進入這個函數之前也不需要連接。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章