Windows 线程调度器的实现分散在内核各处,而且与许多组件都有关联,很难进行系统地学习,因此我打算写几篇文章来记录下本身学习过程当中的思考和分析,同时也方便往后查阅,此文能够看做是《Windows内核原理与实现》中线程调度部分的读书笔记和简单总结。数据结构
在对调度器函数进行分析学习以前,首先要明确一个概念:调度器只由内核层进行负责实现,不涉及执行体层。所以线程相关的数据结构只有 KTHREAD,其中调度相关的最重要的成员是 State,它标识了线程的当前状态,取值由名为 KTHREAD_STATE 的枚举类型定义:app
typedef enum _KTHREAD_STATE { Initialized, Ready, Running, Standby, Terminated, Waiting, Transition, DeferredReady, GateWait } KTHREAD_STATE;
已初始化 (Initialized):线程建立过程当中的内部状态,此时线程不参与调度。
就绪 (Ready):线程已经准备好运行,等待被调度。
运行中 (Running):线程正在某一处理器上运行。
待命 (Standby):线程被选为某一处理器上下一个将要被执行的线程。
已终止 (Terminated):线程已终止,正在进行资源回收。
等待中 (Waiting):线程正在等待某个条件知足,好比事件对象被触发。
转移 (Transition):线程已经准备好运行但内核栈不在内存中。
延迟就绪 (DeferredReady):线程还没有被肯定在哪一个处理器上运行,此状态对于单处理器系统没有意义。
门等待 (GateWait):线程正在等待一个门对象。ide
其中就绪和延迟就绪状态的主要区别是:延迟就绪线程还没有肯定被分配到哪一个处理器上运行,而就绪线程已经被分配到了某个处理器上。函数
对于线程各个状态间的转移规则,能够参考线程状态转移图(引自潘爱民老师的《Windows内核原理与实现》):学习
进程在内核层所对应的 KPROCESS 结构中,也有一个用来标识当前状态的 State 成员,它的取值由名为 KPROCESS_STATE 的枚举类型定义:spa
typedef enum _KPROCESS_STATE { ProcessInMemory, ProcessOutOfMemory, ProcessInTransition, ProcessOutTransition, ProcessInSwap, ProcessOutSwap } KPROCESS_STATE;
ProcessInMemory:表示进程的虚拟地址空间内容在物理内存中。
ProcessOutOfMemory:表示进程的虚拟地址空间内容已被换出物理内存。
ProcessInTransition:表示进程的虚拟地址空间内容不在物理内存中,但已请求换入。
ProcessOutTransition:表示进程的虚拟地址空间内容存在于物理内存中,但已请求换出。
ProcessInSwap:表示正在将进程的虚拟地址空间内容换入物理内存,换入完成后,状态将变动为 ProcessInMemory。
ProcessOutSwap:表示正在将进程的虚拟地址空间内容换出物理内存,换出完成后,状态将变动为 ProcessOutOfMemory。线程
换入或换出进程的虚拟地址空间会致使进程状态的切换,此工做是由名为 平衡集管理器 (Balance Set Manager) 的内核组件负责的,在内核第一阶段初始化接近结束时,MmInitSystem 函数建立了两个平衡集管理器线程,其对应例程分别是 KeBalanceSetManager 和 KeSwapProcessOrStack 函数。code
KeBalanceSetManager 线程循环等待一个每秒触发一次的定时器对象和一个工做集管理器事件对象,当等待成功后,它触发名为 KiSwapEvent 的事件对象来通知交换线程,以尝试对知足条件的线程的内核栈执行换出操做。KeSwapProcessOrStack 即为交换线程,它循环等待上述的 KiSwapEvent 对象,一旦等待成功,会根据状况执行进程和线程内核栈的换入换出工做。对象
一个进程的换出操做发生在进程的 StackCount 为 0 时,StackCount 记录了该进程中有多少个线程的内核栈位于内存中,当该进程的全部线程的内核栈都被换出内存时,KiOutSwapKernelStacks 会将进程插入到待换出链表中,并触发 KiSwapEvent 对象,交换线程会在下次循环中调用 KiOutSwapProcesses 函数将该进程换出内存。blog
平衡集管理器实质上是内存管理器组件,有关它更多更详细的内容将在以后的文章中更新。
KiReadyThread 从名字上来看是将一个线程转为就绪状态,而实际上这个函数根据三种不一样状况来进行处理:
void __fastcall KiReadyThread(IN PKTHREAD Thread) { PKPROCESS Process; Process = Thread->ApcState.Process; if (Process->State != ProcessInMemory) { Thread->State = Ready; Thread->ProcessReadyQueue = TRUE; InsertTailList(&Process->ReadyListHead, &Thread->WaitListEntry); if (Process->State == ProcessOutOfMemory) { Process->State = ProcessInTransition; InterlockedPushEntrySingleList(&KiProcessInSwapListHead, &Process->SwapListEntry); KiSetInternalEvent(&KiSwapEvent, KiSwappingThread); } return; } else if (Thread->KernelStackResident == FALSE) { ASSERT(Process->StackCount != MAXULONG_PTR); Process->StackCount += 1; ASSERT(Thread->State != Transition); Thread->State = Transition; InterlockedPushEntrySingleList(&KiStackInSwapListHead, &Thread->SwapListEntry); KiSetInternalEvent(&KiSwapEvent, KiSwappingThread); return; } else { KiInsertDeferredReadyList(Thread); return; } }
分支一:
首先,此函数根据上文提到的 KPROCESS 的 State 成员,来判断目标线程所属进程当前是否处于 ProcessInMemory 状态,即进程虚拟地址空间是否在物理内存中,若不是则将目标线程设置为就绪状态,并将线程的 ProcessReadyQueue 标志设置为 TRUE,而后将线程插入到所属进程的就绪链表 (ReadyListHead) 中,ProcessReadyQueue 用来标识线程是否在其所属进程的就绪链表中。然后进一步判断进程是否处于 ProcessOutOfMemory 状态,如果则将该进程设置为 ProcessInTransition 状态,并插入到待换入进程链表中,最后触发 KiSwapEvent 对象通知交换线程执行进程换入操做。由此能够看出,ProcessInTransition 是一种中间状态,他标识了进程将要但尚未被执行换入操做,此状态介于 ProcessInMemory 和 ProcessInSwap 之间。
当进程当前处于 ProcessOutOfMemory 状态时,其后续操做是:平衡集管理器的交换线程成功等待到 KiSwapEvent,进而调用 KiInSwapProcesses 函数将以前插入到待换入进程链表中的进程换入内存(经过 MmInSwapProcess 函数),以后将进程状态修改成 ProcessInMemory。此时进程虚拟地址空间已在物理内存中,能够对进程中全部的就绪线程进行调度,因此 KiInSwapProcesses 函数遍历该进程的就绪链表,对其中的全部线程再次调用 KiReadyThread,然后将线程从链表中移除。因为这一次进程已存在于内存中,因此这次 KiReadyThread 函数不会再执行到此分支。
而对于 ProcessInTransition 和 ProcessInSwap 这两种状态,则不须要通知交换线程将进程换入内存,由于此时交换线程已经或将要执行 KiInSwapProcesses 函数,如上所述,此函数会在将进程换入内存后,对该进程就绪链表中的全部线程再次调用 KiReadyThread。
最后,若进程处于 ProcessOutTransition 或 ProcessOutSwap 状态(进程因其全部线程的内核栈都被换出内存而致使自身也被换出内存,在换出的过程当中,若是有属于该进程的新线程被建立,或某一现有线程挂靠到该进程上,则 KiReadyThread 被调用,此时进程可能处于这两种状态),那么剩下的工做将由交换线程经过调用 KiOutSwapProcesses 函数来完成,此函数负责将待换出进程链表中的进程换出内存,它在两个阶段分别检查待换出进程的就绪链表:若进程还没有换出内存,则取消换出操做并将进程状态修改成 ProcessInMemory,而后对该进程就绪链表中的全部线程再次调用 KiReadyThread;若进程已换出内存,则修改进程状态为 ProcessInTransition 并触发 KiSwapEvent 对象,交换线程会在下次循环中调用 KiInSwapProcesses 执行后续操做。
综上所述,只有当进程处于 ProcessOutOfMemory 状态时,此函数才通知交换线程将进程换入内存,其他状况平衡集管理器会进行判断和处理,而不管哪一种一状况,进程最后都会变为 ProcessInMemory 状态,进而交由其余分支处理,所谓异途同归。
分支二:
若是进程当前处于 ProcessInMemory 状态(经分支一处理后,进程必然处于此状态),则继续判断目标线程的内核栈是否在物理内存中(由 KernelStackResident 标志指示)。上文提到,线程栈的换入和换出操做也是由平衡集管理器负责的,当一个线程处于等待状态超过必定时间以后,交换线程调用 KiOutSwapKernelStacks 函数将其内核栈换出物理内存。所以若线程的内核栈已被换出物理内存,则要先通知交换线程将内核栈其换入内存,交换线程经过调用 KiInSwapKernelStacks 函数将线程内核栈换入物理内存,然后直接调用 KiInsertDeferredReadyList 函数将线程插入到延迟就绪链表中,关于 KiInsertDeferredReadyList 函数,见分支三。
另外上文还提到,进程 KPROCESS 对象中的 StackCount 成员记录了该进程中有多少个线程的内核栈位于内存中,对于一个将要被换入内存的线程,天然要将其所属进程的 StackCount 加一(因为线程终止或挂靠到其余进程时也会引发 StackCount 的变更,因此此成员不禁平衡集管理器维护)。
分支三:
进入到分支三就表示线程已知足执行条件(内核栈和所属进程都已在物理内存中),所以调用 KiInsertDeferredReadyList 函数执行下一步操做:
PKPRCB Prcb; Prcb = KeGetCurrentPrcb(); Thread->State = DeferredReady; Thread->DeferredProcessor = Prcb->Number; PushEntryList(&Prcb->DeferredReadyListHead, &Thread->SwapListEntry);
此函数逻辑十分简单,所作的仅仅是将线程设置为延迟就绪状态,并将其插入到当前处理器 PRCB 结构中的延迟就绪链表中,之后当调度器得到控制权时,KiProcessDeferredReadyList 函数将遍历此链表,并对每一个线程调用 KiDeferredReadyThread 函数,使其有机会变为就绪或待命状态。注:此处所说的就绪状态是真正的就绪,区别于上文所说进程就绪链表中的线程,后者不知足执行条件(须要等待其所属进程被换入内存)。
至此 KiReadyThread 函数已分析完毕,能够看出,通过此函数处理后的任何线程都会变为延迟就绪状态,这对线程来讲是一个重要转折点,意味着它将有机会得到执行权,而在此以前,该线程不会被考虑执行。