您的位置:首页 > 其它

进程和线程的创建过程

2014-04-13 04:31 225 查看
进程和线程的创建过程
http://book.douban.com/annotation/28879242/
------------------------------------------------------------------------------------------------

windows过程/线程创建过程 --- windows操纵体系进修
http://www.mysjtu.com/page/M0/S924/924201.html
------------------------------------------------------------------------------------------------

Windows APC机制
http://www.3600safe.com/?post=109
------------------------------------------------------------------------------------------------

章节名:windows进程和线程

页码:第150页 2013-10-09 11:43:08

进程和线程的创建过程
在内核中,Windows 创建一个进程的过程是从NtCreateProcess 函数开始的,它首先创建一个执行体进程对象,即EPROCESS 对象,然后创建一个初始线程,为初始线程建立一个栈,并设置好它的初始执行环境。完成这些工作以后,该线程就可以参与系统的线程调度了。然而,通过Windows API 函数创建的进程也要接受Windows 子系统的管理,在这种情况下,仅仅内核部分的工作还不够,系统在创建进程过程中,还需要跟子系统打交道。另外,建立起独立的内存地址空间是Windows 创建进程过程中不可避免的步骤,关于进程地址空间的创建,请参考4.3.1 节。

NtCreateProcess 函数的代码位于base\ntos\ps\create.c 文件中(815~850 行),它只是简单地对参数稍作处理,然后把创建进程的任务交给NtCreateProcessEx 函数(位于同一文件中,852~917 行),所以我们来看NtCreateProcessEx 的原型及其流程。
NTSTATUS
NtCreateProcessEx(
__out PHANDLE ProcessHandle,
__in ACCESS_MASK DesiredAccess,
__in_opt POBJECT_ATTRIBUTES ObjectAttributes,
__in HANDLE ParentProcess,
__in ULONG Flags,
__in_opt HANDLE SectionHandle,
__in_opt HANDLE DebugPort,
__in_opt HANDLE ExceptionPort,
__in ULONG JobMemberLevel
);
NtCreateProcessEx 的参数并不复杂,ProcessHandle 是一个输出参数,如果创建成功,则它包含了所创建的进程的句柄。DesiredAccess 包含了对新进程的访问权限。ObjectAttributes 是一个可选的指针参数(意指可以为NULL),它指定了进程对象的属性。ParentProcess 指向父进程的句柄,这是一个必需的参数,不能为NULL,并且调用者必须对该进程具有PROCESS_CREATE_PROCESS 访问权限。Flags 是创建标志,其中有一个标志PROCESS_CREATE_FLAGS_INHERIT_HANDLES 特别值得一提:NtCreateProcess专门有一个布尔参数指定是否该标志为TRUE,表明新进程的对象句柄表是否要复制父进程的句柄表,或者初始设置为空。SectionHandle 是一个可选的句柄,指向一个内存区对象,代表了该进程的映像文件,调用者对于此内存区对象必须具有SECTION_MAP_EXECUTE 访问权限。DebugPort 也是一个可选的句柄,指向一个端口对象,如果此句柄参数不为NULL,则此端口被赋为新进程的调试端口,否则,新进程没有调试端口。而且,调用者对于调试端口对象必须具有PORT_WRITE 和PORT_READ 访问权限。类似地,ExceptionPort 也是一个可选的句柄,指向一个端口对象,如果此句柄参数不为NULL,则此端口被赋为新进程的异常端口,否则,新进程没有异常端口。调用者对于异常端口对象必须具有PORT_WRITE 和PORT_READ 访问权限。JobMemberLevel 指定了要创建的进程在一个Job 集中的级别。NtCreateProcessEx 函数的代码只是简单地检查ProcessHandle 参数代表的句柄是否可写,然后把真正的创建工作交给PspCreateProcess 函数,所以,PspCreateProcess 才是真正创建进程的函数,它也位于文件base\ntos\ps\create.c 中(966~1 758 行)。PspCreateProcess被三个函数调用,它们是NtCreateProcessEx、PsCreateSystemProcess 和PspInitPhase0。其中PspInitPhase0 是在系统初始化的早期被调用的,它创建的进程(即System 进程)的句柄保存在全局变量PspInitialSystemProcessHandle 中, 进程对象存放于全局变量PsInitialSystemProcess 中(参考后面3.4.5 节的介绍)。PsCreateSystemProcess 可用于创建系统进程对象,它创建的进程都是PsInitialSystemProcess 的子进程。所以,PspCreateProcess函数负责创建系统中的所有进程,包括System 进程。下面介绍此函数的基本流程。(1) 如果父进程句柄不为NULL,则通过ObReferenceObjectByHandle 获得父进程对象的EPROCESS 指针,放在Parent 局部变量中,同时也获得父进程的Affinity 设置。新进程的工作集最大/ 最小值被初始化为全局变量PsMinimumWorkingSet 和PsMaximumWorkingSet 。如果父进程句柄为NULL, 则Affinity 设置为全局变量KeActiveProcessors,即系统中当前的可用处理器。因为新进程对象尚未创建,所以这些设置都保存在局部变量中。(2) 调用ObCreateObject 函数,创建一个类型为PsProcessType 的内核对象,置于局部变量Process 中,其对象体为EPROCESS 数据结构。(3) 把Process 对象中的所有域置为0,然后初始化其中部分成员(见1 131~1 153 行)。(4) 检查内存区句柄参数SectionHandle,对于系统进程,此参数为NULL,此时,除非父进程为PsInitialSystemProcess,否则内存区对象继承自父进程,并且不得为NULL。如果此参数不为NULL,则利用此句柄参数调用ObReferenceObjectByHandle 获得内存区对象的指针。所以,新进程对象的内存区对象已经完成初始化。(5) 接下来根据DebugPort 参数来初始化新进程对象的DebugPort 成员。(6) 同样地,根据ExceptionPort 参数来初始化新进程对象的ExceptionPort 成员。(7) 如果指定的父进程不为NULL,则创建一个全新的地址空间;否则(对于系统进程),让新进程的句柄表指向当前进程的句柄表,并且利用空闲线程的地址空间来初始化新进程的地址空间。(8) 然后调用KeInitializeProcess 函数来初始化新进程对象的基本优先级、Affinity、进程页表目录和超空间的页帧号。(9) 通过PspInitializeProcessSecurity 函数初始化新进程的安全属性,主要是从父进程复制一个令牌。(10) 接下来设置新进程的优先级类别。如果父进程不为NULL,则拷贝父进程的优先级类别,并且初始化新进程的句柄表,若Flags 参数中包含了句柄继承标志,则把父进程句柄表中凡是有继承属性的对象拷贝到新进程句柄表中。(11) 接下来初始化新进程的进程地址空间。有三种可能性:a. 新进程有新的可执行映像内存区对象。调用MmInitializeProcessAddressSpace 函数,根据指定的内存区对象来初始化进程地址空间。b. 没有指定映像内存区对象,但指定了父进程,而父进程并非PsInitialSystemProcess。也调用MmInitializeProcessAddressSpace 函数,但根据父进程来初始化进程地址空间。并且把父进程的映像名称字符串拷贝到新进程对象的数据结构中。c. 没有指定映像内存区对象,但指定了PsInitialSystemProcess 作为父进程。这对应于系统进程的情形。调用MmInitializeProcessAddressSpace,不指定内存区和父进程,直接初始化。同样地,把父进程的映像名称字符串拷贝到新进程对象的数据结构中。实际上还有第四种可能性,即没有指定映像内存区对象,也没有指定父进程。这对应于系统的引导进程(后蜕化成空闲进程),它的地址空间是在MmInitSystem 执行过程中初始化的,由MiInitMachineDependent 函数调用MmInitializeProcessAddressSpace 来完成。(12) 创建进程ID。利用ExCreateHandle 函数在CID 句柄表中创建一个进程ID 项。(13) 对这次进程创建行为进行审计。(14) 如果父进程属于一个作业对象,则也加入到父进程所在的作业中。(15) 对于通过映像内存区对象来创建进程的情形,创建一个PEB;对于进程拷贝(fork)的情形,则使用继承的PEB。(16) 把新进程加入到全局的进程链表PsActiveProcessHead 中。(17) 调用ObInsertObject 函数,把新进程对象插入到当前进程的句柄表中。(18) 接下来计算新进程的基本优先级和时限重置值,并且设置进程的内存优先级。这是通过调用PspComputeQuantumAndPriority 函数来完成的。(19) 然后设置进程的访问权限,即GrantedAccess 域。由于新进程已经被加入到句柄表中,所以它现在能够被终止了(PROCESS_TERMINATE 权限)。对于有父进程但父进程不是PsInitialSystemProcess 的进程,首先执行访问检查,然后计算进程的访问权限。如果是PsInitialSystemProcess 的子进程,则授予所有的访问权限。(20) 最后,设置进程的创建时间。并把新进程的句柄赋到输出参数ProcessHandle 中,从而创建者可以获得新进程的句柄。以上是创建并初始化一个进程对象的过程。然而,经过PspCreateProcess 函数以后,新建的进程中并没有任何线程对象,所以,它现在还只是一个死的进程空间,也就是说,其中的代码并没有被真正运行起来。接下来我们讨论线程对象的创建和初始化。类似于进程的创建过程,线程的创建是从NtCreateThread 函数开始的,它也位于base\ntos\ps\create.c 文件中(77~169 行)。其原型如下:
NTSTATUS
NtCreateThread(
__out PHANDLE ThreadHandle,
__in ACCESS_MASK DesiredAccess,
__in_opt POBJECT_ATTRIBUTES ObjectAttributes,
__in HANDLE ProcessHandle,
__out PCLIENT_ID ClientId,
__in PCONTEXT ThreadContext,
__in PINITIAL_TEB InitialTeb,
__in BOOLEAN CreateSuspended
);
NtCreateThread 所做的事情很简单,首先,对于非内核模式传递过来的调用,检查几个参数是否可写,包括输出参数ThreadHandle、ClientId 和输入参数ThreadContext 及InitialTeb;其次,处理InitialTeb 参数,将它放到局部变量CapturedInitialTeb 中。这些处理工作是在一个try 块中完成的,所以,其中发生的异常能够被捕捉住,以保证内核的健壮性。完成了这些参数处理以后,NtCreateThread调用真正创建线程的函数PspCreateThread(位于同一文件中,242~813 行),以下是PspCreateThread 的原型:
NTSTATUS
PspCreateThread(
OUT PHANDLE ThreadHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN HANDLE ProcessHandle,
IN PEPROCESS ProcessPointer,
OUT PCLIENT_ID ClientId OPTIONAL,
IN PCONTEXT ThreadContext OPTIONAL,
IN PINITIAL_TEB InitialTeb OPTIONAL,
IN BOOLEAN CreateSuspended,
IN PKSTART_ROUTINE StartRoutine OPTIONAL,
IN PVOID StartContext
);
PspCreateThread 的参数虽然多,但是并不复杂。ThreadHandle 是一个输出参数,如果创建成功,则它包含了所创建的线程的句柄。DesiredAccess 包含了对新线程的访问权限。ObjectAttributes 是一个可选的指针(意指可以为NULL),它指定了新线程对象的属性。ProcessHandle 指向一个进程的句柄,新线程将运行在此进程的环境中。ProcessPointer参数指向所属进程的EPROCESS 对象,此参数仅当创建系统线程时才会指向全局的PsInitialSystemProcess 对象,其他情况下为NULL。ClientId 参数返回新线程的CLIENT_ID结构。ThreadContext 参数提供了新线程的执行环境,它代表了用户模式线程的初始执行环境,如果该参数为NULL,则说明此次调用将创建一个系统线程。InitialTeb 参数为新线程的TEB 结构提供初始值。CreateSuspended 参数是一个布尔值,指明了新线程被创建起来以后是否被挂起。如果CreateSuspended 参数为TRUE,则意味着新线程创建完成以后并不立即执行,以后必须通过NtResumeThread 函数让它开始运行。StartRoutine 参数指定了系统线程启动函数的地址。StartContext 参数指定了系统线程启动函数的执行环境。PspCreateThread 函数仅仅被NtCreateThread 和PsCreateSystemThread 这两个函数调用,分别用于创建用户线程和系统线程对象。在PspCreateThread 函数的参数中,ThreadContext 和InitialTeb 参数针对用户线程的创建操作,而StartRoutine 和StartContext参数则针对系统线程的创建操作。下面分析PspCreateThread 函数的执行流程,读者可以参考base\ntos\ps\create.c 文件中的代码。(1) 首先获得当前线程对象,以及此次创建操作来自于内核模式还是用户模式(即PreviousMode)。然后根据进程句柄参数ProcessHandle 获得相应的进程对象,放到局部变量Process 中。(2) 调用ObCreateObject 函数创建一个线程对象ETHREAD。并把整个ETHREAD 结构清零。(3) 对一些基本的域进行初始化,包括RundownProtect、ThreadsProcess(指向由进程句柄参数解出来的EPROCESS 对象)、Cid(包括UniqueProcess 和UniqueThread 成员)。这里Cid.UniqueProcess 是从Process 对象中来的,而Cid.UniqueThread 则是通过调用ExCreateHandle 函数在CID 句柄表中创建一个句柄表项而获得的。(4) 继续初始化新线程对象ETHREAD 结构中的一些域(409~441 行),包括ReadClusterSize、LpcReplySemaphore、LpcReplyChain、IrpList、PostBlockList 成员,以及线程锁成员ThreadLock 和ActiveTimerListLock,并且初始化新线程的定时器链表ActiveTimerListHead 成员。(5) 获得进程的RundownProtect 锁,以避免在创建过程中该进程被停掉(rundown)。直到该线程被插入到进程的线程链表中(通过调用KeStartThread 函数)以后,PspCreateThread 才释放该锁。(6) 如果ThreadContext 非NULL,则此次创建的是用户模式线程,于是创建一个TEB,并且用InitialTeb 进行初始化。然后,利用ThreadContext 中的程序指针(Eip 寄存器)来设置线程的启动地址StartAddress 域,并且将ThreadContext 中的Eax 寄存器设置到线程的Win32StartAddress 域。完成了这些操作以后,调用KeInitThread 函数,根据进程对象中的信息来初始化新线程的一些属性,包括跟同步相关的各个域,例如同步头(Header域)、WaitBlock 的初始化,等等。线程的系统服务表(ServiceTable 域)以及与APC 和定时器相关的域也在这个函数中被初始化。此外,线程的内核栈也在这个函数中被创建并初始化。最后,KeInitThread 函数根据所提供的参数信息调用KiInitializeContextThread,以便完成与特定处理器相关的执行环境的初始化。因此,当这个函数返回时,新线程的状态是“已初始化(Initialized)”。(7) 否则,则是系统线程。首先在CrossThreadFlags 标志中设置系统线程位。然后将线程的启动地址设置为StartRoutine 参数。最后同样地调用KeInitThread 函数。所以,KeInitThread 函数既可以被用来初始化用户模式线程,也可以被用于初始化系统线程。(8) 接下来锁住进程,并确保此进程并不是在退出或终止过程中。(9) 然后进程的活动线程数加1,并且将新线程加入到进程的线程链表中。接着调用KeStartThread 函数,初始化剩余的域,尤其是跟调度相关的域,比如优先级、时限设置、处理器亲和性,等等。经过这一步以后,新线程就可以开始运行了。注意,在PspCreateThread和KeStartThread 这两个函数中,我们都可以看到“InsertTailList (&Process->ThreadListHead,&Thread->ThreadListEntry);”这样的调用,实际上,这是分别在执行体层和内核层维护线程与进程之间的关系。也就是说,这里Process 和Thread 分别对应于不同的结构类型,即EPROCESS 或KPROCESS,以及ETHREAD 或KTHREAD。(10) 接下来,若这是进程中的第一个线程,则触发该进程的创建通知(见576~596 行)。(11) 如果新线程的进程在一个作业中,则需要做特定的处理(见607~625 行)。(12) 通知那些接收线程创建事件的出调例程(callout routine)(见633~649 行)。(13) 线程对象的引用计数加2,一个针对当前的创建操作,另一个针对要返回的线程句柄。(14) 如果CreateSuspended 参数指示新线程立即被挂起,则调用KeSuspendThread 挂起新线程。(15) 根据指定的期望访问权限,调用SeCreateAccessStateEx 函数创建一个访问状态结构(ACCESS_STATE)。(16) 调用ObInsertObject 函数把新线程对象插入到当前进程的句柄表中。ObInsertObject 调用如果不成功,则终止新线程;否则,设置好输出参数ThreadHandle 和ClientId,准备返回。(17) 设置线程的创建时间。(18) 然后设置线程的访问权限,即GrantedAccess 域(见754~807 行)。(19) 最后,调用KeReadyThread 函数,使新线程进入“就绪(ready)”状态,准备马上执行;或者,若此时进程尚未在内存中,则新线程的状态为“转移(transition)”,以等待换入内存后再执行。(20) 引用计数减1,当前操作完成。返回。我们从上述步骤可以看到,一旦PspCreateThread 函数返回,新线程对象的状态已经完全设置好,它可被马上执行。因为线程的创建是在进程已经创建完成以后才做的动作,所以,线程创建是一个相对简单的过程。而完整的进程创建过程其实并不像前面介绍的步骤那么直截了当。譬如,我们在PspCreateProcess 中根本没有看到任何创建线程的动作,甚至,我们也没有看到进程的可执行映像文件是怎么打开的。不过我们在PspCreateProcess 的参数中看到了对应于可执行映像文件的内存区对象的句柄。在WRK中并不能看到完整的进程创建过程,但是理解这一过程仍然是非常必要的,下面我们从上层应用程序的角度来讨论进程的创建全过程。为了创建一个进程,在Windows 中最常见的手段是调用某个API 函数,比如CreateProcess,此函数一旦成功返回,则新的进程便已建立起来。CreateProcess 只是kernel32.dll 中的一个函数而已,当应用程序调用此函数时,它不仅会调用执行体层的进程对象创建函数(即前面介绍的NtCreateProcess 或NtCreateProcessEx 函数),还需要跟Windows 子系统打交道,以便让Windows 子系统参与进程的管理。我们在前面介绍进程和线程数据结构时已经看到了有些域是专门为Windows 子系统预留的。接下来我们讨论创建Windows 子系统进程的完整步骤:(1) 首先用CreateProcess(实际上是CreateProcessW)打开指定的可执行映像文件,并创建一个内存区对象。注意,内存区对象并没有被映射到内存中(由于目标进程尚未建立起来,不可能完成内存映射),但它确实是打开了。(2) 接下来调用内核中的NtCreateProcessEx 系统服务,实际的调用过程是这样的:kernel32.dll 中的CreateProcessW调用ntdll.dll 中的存根函数NtCreateProcessEx,而ntdll.dll的NtCreateProcessEx 利用处理器的陷阱机制切换到内核模式下;在内核模式下,系统服务分发函数KiSystemService 获得控制,它利用当前线程指定的系统服务表,调用到执行体层的NtCreateProcessEx 函数。然后,执行体层的NtCreateProcessEx 函数执行前面介绍的进程创建逻辑,包括创建EPROCESS 对象、初始化其中的域、创建初始的进程地址空间、创建和初始化句柄表,并设置好EPROCESS 和KPROCESS 中的各种属性,如进程优先级、安全属性、创建时间等。到这里,执行体层的进程对象已经建立起来,进程的地址空间已经初始化,并且EPROCESS 中的PEB 也已初始化。(3) 现在,虽然进程对象已经建立起来,但是它没有线程,所以,它自己还不能做任何事情。接下来需要创建一个初始线程,在此之前,首先要构造一个栈以及一个可供运行的环境。初始线程的栈的大小可以通过映像文件获得,而创建线程则可以通过调用ntdll.dll 中的NtCreateThread 函数来完成。Ntdll.dll 中的NtCreateThread 又把任务转发给执行体层的NtCreateThread,即前面刚刚介绍的线程创建逻辑,包括创建ETHREAD 对象、初始化其中的域、生成线程ID、建立TEB 和设置线程的安全属性等工作。进程的第一个线程的启动函数是kernel32.dll 中的BaseProcessStart 函数。然而,这里新创建的线程不会立即运行,它处于挂起状态,要等到进程完全初始化以后才真正开始运行。(4) 到现在,从内核角度来看,进程对象和第一个线程对象已经创建起来了,但是,从子系统的角度而言,进程创建才刚刚开始。Kernel32.dll 给Windows 子系统发送一个消息,消息中包括进程和线程的句柄、进程创建者的ID 等必要的信息。Windows 子系统进程(csrss.exe)接收到此消息,执行以下一系列动作:a. 保留一份句柄。b. 设定新进程的优先级类别。c. 在子系统中分配一个内部进程块。d. 设置新进程的异常端口,从而子系统可以接收到该进程中发生的异常。e. 对于正在被调试的进程,设置它的调试端口,从而子系统可以接收到该进程的调试事件。f. 分配并初始化一个内部线程块,并插入到进程的线程列表中。g. 窗口会话中的进程计数增1。h. 设置进程的停机级别为默认级别。i. 将新进程插入到子系统的进程列表中。j. 分配并初始化一块内存供子系统的内核模式部分使用(W32PROCESS 结构)。k. 显示应用程序启动光标。(5) 到这时候,进程环境已经建好,其线程将要使用的资源也分配好了,Windows 子系统已经知道并登记了此进程和线程。所以,初始线程被恢复执行,余下部分的初始化工作是初始线程在新进程环境中完成的。在内核中,新线程的启动例程是KiThreadStartup函数,这是当PspCreateThread 调用KeInitThread 函数时,KeInitThread 函数调用KiInitializeContextThread(参见base\ntos\ke\i386\thredini.c 文件)来设置的。现在回到WRK中,读者可以看到KiThreadStartup 函数的代码。这是一段简单的汇编代码,如下所示:
cPublicProc _KiThreadStartup ,1
xor ebx,ebx ; clear registers
xor esi,esi ;
xor edi,edi ;
xor ebp,ebp ;
LowerIrql APC_LEVEL ; KeLowerIrql(APC_LEVEL)
pop eax ; (eax)->SystemRoutine
call eax ; SystemRoutine(StartRoutine, StartContext)
pop ecx ; (ecx) = UserContextFlag
or ecx, ecx
jz short kits10 ; No user context, go bugcheck
mov ebp,esp ; (bp) -> TrapFrame holding UserContext
jmp _KiServiceExit2
kits10: stdCall _KeBugCheck, <NO_USER_MODE_CONTEXT>
stdENDP _KiThreadStartup
KiThreadStartup 函数首先将IRQL 降低到APC_LEVEL,然后调用系统初始的线程函数PspUserThreadStartup。这里的PspUserThreadStartup 函数是PspCreateThread 函数在调用KeInitThread 时指定的,参见base\ntos\ps\create.c 的490 行代码。注意,PspCreateThread函数在创建系统线程时指定的初始线程函数为PspSystemThreadStartup ( 见base\ntos\ps\create.c 的514 行代码) 。线程启动函数被作为一个参数传递给PspUserThreadStartup,在这里,它应该是kernel32.dll 中的BaseProcessStart。(6) PspUserThreadStartup 函数被调用,其代码位于base\ntos\ps\create.c 的2 002~2 147行。逻辑并不复杂,但是涉及异步函数调用(APC)机制。以下是它的基本流程:a. 获得当前线程和进程对象。b. 是否由于创建过程中出错而需要终止本线程。c. 如果需要,通知调试器。d. 如果这是进程中的第一个线程,则判断系统是否支持应用程序预取的特性,如果是,则通知缓存管理器预取可执行映像文件中的页面(见2 106 行的CcPfBeginAppLaunch调用)。所谓应用程序预取,是指将该进程上一次启动的前10 s 内引用到的页面直接读入到内存中。e. 然后,PspUserThreadStartup 把一个用户模式APC 插入到线程的用户APC 队列中,此APC 例程是在全局变量PspSystemDll 中指定的,指向ntdll.dll 的LdrInitializeThunk 函数。f. 接下来填充系统范围的一个Cookie 值。(7) PspUserThreadStartup 函数返回以后,KiThreadStartup 函数返回到用户模式,此时,PspUserThreadStartup 插入的APC 被交付,于是LdrInitializeThunk 函数被调用,这是映像加载器(image loader)的初始化函数。LdrInitializeThunk 函数完成加载器、堆管理器等初始化工作,然后加载任何必要的DLL,并且调用这些DLL 的入口函数。最后,当LdrInitializeThunk 返回到用户模式APC 分发器时,该线程开始在用户模式下执行,调用应用程序指定的线程启动函数,此启动函数的地址已经在APC 交付时被压到用户栈中。(8) 至此,进程已完全建立起来,开始执行用户空间中的代码。现在我们理解了Windows 系统中一个用户进程的整个创建过程,虽然有一部分工作是由Windows 子系统来完成的,但是从操作系统内核的角度,我们依然可以清楚地看到,Windows 为了支持进程和线程的概念,是如何以对象的方式来管理它们,并创建和初始化进程和线程,使它们变成真正可以工作的功能实体。进程和线程的结束处理在执行体层上,线程的终止函数是NtTerminateThread,它调用PspTerminateThreadByPointer函数来完成真正的终止处理。系统线程的终止函数则是PsTerminateSystemThread,它也调用PspTerminateThreadByPointer 来完成真正的终止处理。以下是这三个函数的原型:
NTSTATUS
NtTerminateThread(
__in_opt HANDLE ThreadHandle,
__in NTSTATUS ExitStatus
);
NTSTATUS
PsTerminateSystemThread(
__in NTSTATUS ExitStatus
);
NTSTATUS
PspTerminateThreadByPointer(
IN PETHREAD Thread,
IN NTSTATUS ExitStatus,
IN BOOLEAN DirectTerminate
);
NtTerminateThread 函数的逻辑非常简单,它首先把参数ThreadHandle 所指的引用解出来,然后调用PspTerminateThreadByPointer 来结束指定的线程。如果要终止的线程是当前线程,则PspTerminateThreadByPointer 的DirectTerminate 参数被设置为TRUE,表示此函数永不返回(因为线程已经结束了);否则,此参数设置为FALSE,并且PspTerminateThreadByPointer 返回以后,线程的句柄数减1。PsTerminateSystemThread 函数更加简单,直接调用PspTerminateThreadByPointer 函数,并且指定不再返回。在PspTerminateThreadByPointer 函数中,如果当前线程被终止,则设置结束标志PS_CROSS_THREAD_FLAGS_TERMINATED,并调用PspExitThread 退出线程,此函数执行实际的终止过程,并且不返回。如果不是当前线程,则在该线程中插入一个内核模式APC,为它指定PsExitSpecialApc、PspExitApcRundown 和PspExitNormalApc 例程,由它们完成真正的终止过程。关于APC 对象的交付过程,即它指定的这些例程是如何被执行的,请参考本书5.2.6 节。最终PsExitSpecialApc 函数被执行,它也调用PspExitThread 函数来完成终止过程,所以下面只讨论PspExitThread 函数的处理过程。PspExitThread 函数的代码位于base\ntos\ps\psdelete.c 的792~1 342 行。它首先获得进程对象,并判断线程当前的状态是否允许终止。然后关掉各种跨线程的引用,通知那些已注册的线程删除事件接收者。进程的活动线程计数减1,如果这是最后一个线程,则必须等到该进程的线程链表中所有的线程都退出才能继续往下进行。接下来,如果有必要,则发送调试信息。然后,处理TerminationPort,向所有已经登记过要接收终止通知的线程发送终止消息。如果TerminationPort 为空,但异常端口存在,则向异常端口发送终止消息。之后,通知Windows 子系统当前线程退出。如果这是进程的最后一个线程,还要通知Windows 子系统当前进程退出。接下来,取消该线程的I/O,取消该线程的定时器,以及清除任何尚未完成的注册表通知请求,并且释放当前线程所拥有的突变体内核对象(mutant)。然后释放TEB,调用LpcExitThread 以便让LPC 组件来处理LPC 应答消息(LpcReplyMessage 域中的消息)。接下来设置线程的退出时间(ExitTime)和退出状态(ExitStatus)。如果这是进程的最后一个线程,则还要调用PspExitProcess 以退出进程,并设置进程退出的相关属性,以及销毁句柄表和解除对进程的内存区对象的引用。然后强制恢复线程运行,以便处理可能有的APC。遍历用户模式APC,如果此APC有一个终止函数(RundownRoutine),则执行此函数,否则直接扔掉此APC。接着,如果这是进程的最后一个线程,则清除当前进程的地址空间。到这时候,可以收尾了,因为线程的所有代码已经执行完了(也不会再有内核APC了)。而且,如果这是进程的最后一个线程,则进程的地址空间也不存在了。在这种情况下,PspExitThread 将进程对象的状态变成有信号状态,以唤醒那些可能在等待此进程对象的线程。最后,PspExitThread 调用KeTerminateThread 函数(在base\ntos\ke\thredobj.c文件的2 217~2 412 行),设置线程对象的状态为有信号状态,并设置线程的调度状态为“已终止(Terminated)”,然后选择一个新的线程来运行。KeTerminateThread 函数永不返回,所以,PspExitThread 也不返回。处理器将执行新选择的线程,以后调度算法再也不会选择这一已终止的线程了。在了解了线程的结束处理过程以后,我们实际上已经知道了进程的结束处理,因为最后一个线程的终止处理实际上就是进程的终止步骤中的最后一步。执行体层上的进程终止函数是NtTerminateProcess(在base\ntos\ps\psdelete.c 文件的273~393 行),它的逻辑比较简单:首先,对进程中的所有线程使用一个for 循环进行迭代,凡是非当前线程,调用PspTerminateThreadByPointer 函数令其终止;然后,如果要终止的是当前进程,则调用PspTerminateThreadByPointer 函数终止自己,此调用不再返回。另一个函数PsTerminateProcess 也是完成进程终止任务的,比如在系统停机的时候会被调用到。它调用了PspTerminateProcess 函数(在base\ntos\ps\psdelete.c 文件中的404~467行),其逻辑跟NtTerminateProcess 很类似,只不过它更加简单,不考虑是否在终止自己,只是简单地用一个循环来调用PspTerminateThreadByPointer 以删除指定进程中的所有线程。如果该进程没有任何线程,则只是清除进程的句柄表。系统初始进程和线程Windows 内核在接收到ntldr 转交过来的P0 处理器控制权时,内核环境尚未建立起来,所以,这时还根本没有进程和线程的概念,内核得到的只是一个基本的控制流。内核的入口点是汇编函数_KiSystemStartup,它调用C 函数KiInitializeKernel 以执行内核层的初始化。在KiInitializeKernel 函数中,调用KeInitializeProcess 函数,以初始化当前进程对象,并设置相关的参数,这便是空闲进程(idle process),其进程ID 为0。然后KiInitializeKernel又调用KeInitializeThread 函数以便初始化当前线程对象,而KeInitializeThread 则调用KeInitThread 来设置线程的执行环境和各种参数,再进一步调用KeStartThread 将线程插入到当前进程环境中。在阶段0 初始化完成以后,当前线程蜕变成P0 处理器上的空闲线程。这样,空闲进程和P0 处理器上的空闲线程被初始建立起来。注意,这里提到的进程和线程初始化函数调用均发生在执行体初始化以前,此时执行体本身尚未被初始化。执行体在阶段0 初始化过程中,其初始化函数ExpInitializeExecutive 调用PsInitSystem函数,而PsInitSystem 将控制转给PspInitPhase0 函数。在PspInitPhase0 中,除了完成全局初始化工作以外,进一步完成空闲进程的初始化,并且创建System 进程和用于运行阶段1 初始化的Phase1Initialization 线程。在阶段1 初始化过程中,Phase1InitializationDiscard 函数也调用了PsInitSystem 函数,而PsInitSystem 将控制转给PspInitPhase1 函数。在PspInitPhase1 中, 调用PspInitializeSystemDll 函数以初始化系统DLL(即ntdll.dll),并找到系统DLL 中的入口函数,以及其他一些负责异常分发和用户APC 分发或回调至用户模式的函数的地址,参见PspLookupKernelUserEntryPoints 函数的代码。执行阶段1 初始化的线程最终蜕变成系统的零页面线程。经过阶段1 初始化以后,进程和线程管理子系统已经可以正常工作。值得一提的是, 空闲进程和System 进程在整个系统中只有一个, 它们的进程对象分别是PsIdleProcess 和PsInitialSystemProcess,其ID 分别为0 和4。零页面线程也只有一个,但空闲线程则每个处理器对应有一个。除了P0 处理器的初始线程蜕变成空闲线程以外,其他处理器在阶段1 初始化过程中被启动起来以后,也从_KiSystemStartup 函数开始执行,并且在完成了该处理器上的初始化任务以后转到空闲循环,蜕变成该处理器的空闲线程。System 进程容纳了所有的系统线程,内核在调用PsCreateSystemThread 函数时若不指定目标进程,则新建的线程运行在System 进程空间中。有一些不依赖于任何其他进程环境的操作可以放在System 进程中运行。DPC 例程或线程调度器可以将一些需要访问换页内存或等待分发器对象的代码逻辑转移到系统线程中。此外,System 进程中有一组称为系统辅助线程的线程池,专门运行各种工作项目(WorkItem),因此,Windows 内核或驱动程序可以不创建任何线程,而是将要做的工作包装成一个工作项目,然后交给System进程中的系统辅助线程来完成。经过内核初始化以后,尽管系统的引导过程还有很多工作要做,包括会话创建和用户登录等,但是,内核的进程和线程管理已经起作用,后面的工作由线程调度器安排和运行。由于所有的处理器在初始时都从空闲循环进入线程调度器,因此,这里简单提一下空闲循环所做的事情,其代码在base\ntos\ke\i386\ctxswap.asm 文件中(KiIdleLoop):它快速地开一下中断,然后又关上中断以便处理后续的DPC 和线程检查。检查当前处理器上是否有DPC(延迟的过程调用,通常是中断处理例程登记的过程)正在等待执行,如果有DPC 在等待,则清除软中断,并交付DPC。然后检查是否有一个线程已被选出来作为该处理器上运行的下一个线程,如果有,则分发该线程,使它运行。否则,判断该处理器是否已被置上空闲调度(idle schedule)标志,若是,则调用KiIdleSchedule,以查找在该处理器上运行的下一个线程;否则回到空闲循环开始处。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: