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()在映像中的位置是系统初始化时就预先确定并记录在案的,所以在进入这个函数之前也不需要连接。

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