天天看點

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()在映像中的位置是系統初始化時就預先确定并記錄在案的,是以在進入這個函數之前也不需要連接配接。

繼續閱讀