在Windows中,我们如何深入理解线程这个概念?

理解Windows线程必须从了解Windows线程的数据结构开始。

本文参考和翻译自最新的《Windows Internals Part1》第七版,该书更新了对于Win10的新内容。如有翻译上的问题,请邮箱联系我

Windows线程的数据结构

在操作系统层,一个Windows线程表示为一个执行线程对象。该对象封装了一个ETHREAD(excutive thread)结构,该结构包含一个KTHREAD(kernel thread)结构作为它的第一个成员,后面会提到。

ETHREAD结构和它所指向的其它结构存在于系统地址空间中。

唯一的例外是线程环境块(TEB, Thread Environment Block),它存在于进程地址空间中,它类似于进程环境块(PEB, Process Environment Block)。

并且,Windows子系统进程(Csrss)为Windows进程中创建的每个线程维护了一个平行的结构,称为CSR_THREAD。另外,对于那些已经调用了任意一个Windows子系统USER或GDI函数的线程,Windows子系统的内核模式部分(Win32k.sys)为每个这样的线程维护了一个数据结构,称为W32THREAD结构,线程的KTHREAD结构有一个字段指向此结构。

ETHREAD结构

  • ETHREAD结构图

ETHREAD结构的第一个成员被称为TCB,也就是线程控制块(Thread Control Block),它就是一个KTHREAD型的结构。

第二个成员标记了线程创建和退出的次数,属于线程标识信息。

第三个成员标记了线程ID和所属进程ID,属于线程标识信息。

第四个成员是一个指向该线程所属执行进程的结构的一个指针,用于访问所属进程的环境信息,属于进程标识信息。

第五个成员是线程的一些标志位,属于线程标识信息。

第六个成员是一个访问令牌指针,指向一个令牌,用于获取到安全信息。

第七个成员是线程的起始地址,属于线程标识信息。

第八个成员是身份模仿信息。用户对Microsoft Exchange Server信息的访问在Microsoft Internet Information Server(IIS)进程终点执行线程中处理,如果用户希望通过身份验证的访问权限打开邮箱,则此线程必须模拟Microsoft Windows NT的安全上下文,也就是要授予对Microsoft Exchange Server信息存储的身份验证访问权限,线程必须与一组有效的安全凭据关联。

第九个成员是等待I/O请求相关的信息,是一个指针,它指向一个IRP链表。

IRP,I/O Request Package,I/O请求包

第十个成员是计时器信息。

第十一个成员是异步本地过程调用(ALPC, Asynchronous Local Procedure Call)相关的信息。

第十二个成员是线程列表入口,它也是一个指针,它指向下一个ETHREAD结构,属于线程标识信息。

第十三个成员是线程锁,属于线程标识信息。

第十四个成员是退出代码,又称线程结束代码、线程出口代码,属于线程标识信息。

第十五个成员叫做能量消耗值,与Win10的电源管理有关,Win10特有。

第十六个成员叫做CPU集合,Win10特有。

以上内容可以引申出更多内容,这里仅仅大致分析一下。

KTHREAD结构

  • KTHREAD结构图

KTHREAD结构的第一个成员叫做分发器头,标记了任务分发器相关的信息。

第二个成员是一个指针,该指针指向一个线程环境块(Thread Environment Block)。

第三个成员标记了内核时间和用户时间。

第四个成员是一个系统服务表指针(System Service Table Pointer),该指针指向一个系统服务表(System Service Table)。

第五个成员标记了线程冻结和被挂起的次数。

第六个成员是一个指针,该指针指向一个Win32线程对象的结构,也就是W32THREAD结构。

第七个成员是一个指针,该指针指向线程所属进程的结构,也就是KPROCESS结构。

第八个成员记录了栈的一些信息。

第九个成员记录了线程调度的一些信息。

第十个成员是陷阱帧,指中断、自陷、异常进入内核后,在堆栈上形成的一种数据结构。

第十一个成员记录了线程同步相关的信息。

第十二个成员是等待块。

第十三个成员记录一个正在等待的对象的列表。

第十四个成员记录了一个异步过程调用的列表。

第十五个成员是线程列表入口,是一个指针,它指向下一个KTHREAD结构。

以上内容可以引申出更多内容,这里仅仅大致分析一下。

线程环境块TEB, Thread Environment Block

  • 线程环境块结构图

线程环境块存在于进程地址空间(与系统地址空间相对),在内部,它又一个线程信息块(TIB, Thread Information Block)组成。

线程信息块主要用于OS/2和Win9x应用程序兼容。它还允许在使用初始TIB创建新线程时将异常和堆栈信息保存到较小的结构中。

线程环境块的第一个成员是线程信息块。

线程信息块包含以下内容:

  • 栈的基(地址)
  • 栈的最大空间限制
  • 子系统的TIB
  • 纤程数据
  • 异常列表
  • 指向自己(TIB)的一个指针

线程环境块的第二个成员是客户ID(线程ID和进程ID)。

第三个成员是活跃的RPC句柄。

RPC,Remote Procedure Call ,远程过程调用

第四个成员记录了线程本地存储(TLS, Thread Local Storage)槽相关的信息。

第五个成员记录了LastError的值。

第六个成员是一个指针,该指针指向线程所属进程的一个进程环境块(PEBProcess Environment Block)结构。

第七个成员记录了与GDI(Graphics Device Interface,图形设备接口)有关的信息。

第八个成员记录了User32客户相关的信息。

第九个成员记录了OpenGL相关的信息。

第十个成员存储了Winsock数据。

第十一个成员指出了理想处理器。

第十二个成员存储了Windows事件跟踪(ETW, Event Trace for Windows)相关的数据。

第十三个成员存储了COM/OLE数据,也就是与组件对象模型(Component Object Model)和对象链接和嵌入(Object Linking and Embedding)相关的数据。

第十四个成员记录了规范信息。

第十五个成员存储了Windows驱动程序框架(Windows Driver Frameworks)相关的数据。

第十六个成员存储了TxF相关的数据。

第十七个成员记录了线程总共拥有锁的个数。

以上内容可以引申出更多内容,这里仅仅大致分析一下。

CSR_THREAD结构

  • CSR_THREAD结构图

CSR_THREAD类似于CSR_PROCESS的数据结构,但它适用于线程。 它是由会话中的每个Csrss进程维护的,它标识在其中运行的Windows子系统线程。 CSR_THREAD存储了一个句柄,Csrss保存了线程、各种标志、客户ID(线程ID和进程ID)和线程创建时间的一个拷贝。当线程向Csrss发送第一条消息时,它会在Csrss注册,这通常是由于某些API需要通知Csrss进行某些操作或进行条件判断。

CSR_THREAD的第一个成员记录了线程创建的时间。

第二个成员是一个指针,该指针指向下一个CSR_THREAD结构。

第三个成员是一些指针,指向一些哈希表。

第四个成员是客户ID(线程ID和进程ID)。

第五个成员是线程的句柄。

第六个成员是一些标志。

第七个成员是引用计数信息。

第八个成员是身份模仿计数。

以上内容可以引申出更多内容,这里仅仅大致分析一下。

W32THREAD结构

  • W32THREAD结构图

W32THREAD结构类似于W32PROCESS的数据结构,但它应用于线程,该结构主要包含对GDI子系统(画笔和设备上下文属性)和DirectX以及供应商用于编写用户模式打印机驱动程序的用户模式打印驱动程序(UMPD, User Mode Printer Driver)框架有用的信息。 最后,它包含一个用于桌面合成和反走样(AA, Anti-Aliasing)的渲染状态。

W32THREAD的第一个成员是一个指针,该指针指向一个ETHREAD结构。

第二个成员记录了引用计数信息。

第三个成员记录了画笔、设备上下文的属性信息。

第四个成员存储了用户模式打印数据。

第五个成员记录了DirectX精灵的状态。

第六个成员是一些标志。

第七个成员存储了渲染/反走样数据。

第八个成员是线程锁。

第九个成员是一个指针,该指针指向一个DXGTHREAD结构,与DirectX有关。

以上内容可以引申出更多内容,这里仅仅大致分析一下。

Windows线程的诞生

当进程(在某些线程的上下文中,例如运行主函数的线程)创建一个新线程时,线程的生命周期就开始了。

如前所述,各种线程创建函数(例如最常用的CreateThread函数)最终都要调用CreateRemoteThreadEx函数,最后结束。 在Kernel32.dll中的CreateRemoteThreadEx函数中采取了以下步骤来创建Windows线程:

  1. 将Windows API参数转换为本机标志,并构建描述对象参数(OBJECT_ATTRIBUTES)的本机结构。
  2. 构建一个包含两个条目(客户ID和TEB地址)的属性列表。
  3. 确定线程是在调用进程中创建的,还是通过传入的句柄指示的另一个进程。 如果句柄等于从GetCurrentProcess返回的伪句柄(值为-1),那么它就是相同的进程。 如果进程句柄不同,它仍然可能是同一进程的有效句柄,因此调用NtQueryInformationProcess(在Ntdll中),以确定是否确实如此。
  4. 调用NtCreateThreadEx(在Ntdll中)在内核模式下向执行程序进行转换,并在具有相同名称和参数的函数中继续。
  5. NtCreateThreadEx(在执行部分内部)创建并初始化用户模式线程上下文(其结构是特定于体系结构的),然后调用PspCreateThread创建一个挂起的执行线程对象。
  6. CreateRemoteThreadEx分配一个激活上下文。 然后,它查询激活堆栈,看看它是否需要激活,并在需要时激活它。 激活堆栈指针保存在新线程的TEB中。
  7. 除非调用方设置CREATE_SUSPENDED标志创建线程,否则线程现在将恢复,以便可以被安排执行。
  8. 线程句柄和线程ID返回给调用方。

Windows线程调度

Windows调度概述

Windows实现了优先级驱动、抢占式调度系统。 至少有一个最高优先级的可运行(就绪)线程总是运行,并警告某些准备运行的高优先级线程可能受到它们可能被允许或首选运行的处理器的限制——被称为处理器亲和力的现象。 处理器亲和力是基于给定的处理器组来定义的,该处理器组收集多达64个处理器。 默认情况下,线程只能在与进程关联的处理器组中的可用处理器上运行。 (这是为了保持与仅支持64个处理器的旧版本Windows的兼容性)。

开发人员可以通过使用适当的API或在图像头中设置亲和掩码来改变处理器亲和力,用户可以在运行时或在进程创建时使用工具来改变亲和力。 然而,虽然进程中的多个线程可以与不同的组相关联,但线程本身只能在其指定组中可用的处理器上运行。 此外,开发人员可以选择创建组感知的应用程序,这些应用程序使用扩展的调度API将不同组上的逻辑处理器与其线程的亲和力联系起来。 这样做将进程转换为多组进程,理论上可以在机器内的任何可用处理器上运行其线程。

选择线程运行后,它运行的时间称为量子(quantum,又称作时间片 time slice)。 量子是一个线程被允许在同一优先级级别的另一个线程被赋予运行的时间长度。 量子值可以因系统和过程的不同而有所不同,原因有三个:

  • 系统配置设置有所不同(长或短量子、可变或固定量子和优先级分离)
  • 进程的前台或后台状态不同
  • 使用作业对象会改变量子值

但是,线程可能无法完成其量子,因为Windows实现了抢占式调度程序。 也就是说,如果另一个优先级更高的线程准备运行,那么当前正在运行的线程可能会在完成其量子之前被抢占。 事实上,可以选择一个线程来运行下一个线程,甚至在开始其量子之前被抢占!

Windows调度代码在内核中实现。 然而,没有单一的“调度程序”模块或例程。 代码在发生调度相关事件的内核中传播。以下事件可能需要线程调度:

  • 线程已经准备好执行了——例如,一个线程已经新创建或刚刚从等待状态释放。
  • 线程离开了运行状态。线程在它终止、它放弃执行、等待以及量子结束时会离开运行状态。
  • 线程的优先级发生了变化。线程的优先级发生变化,要么是因为系统服务调用,要么是因为Windows本身改变了优先级值。
  • 线程的处理器亲和力发生了变化。当线程的处理器亲和力变化时,该线程将不再运行在正在运行的处理器上。

Windows必须确定在运行线程的逻辑处理器上接下来应该运行哪个线程,或者线程现在应该在哪个逻辑处理器上运行,如果适用的话。 在逻辑处理器选择了要运行的新线程之后,它最终会对其执行上下文切换(Context Switch)。 上下文切换是保存与正在运行的线程关联的易失性(volatile processor)处理器状态、加载另一个线程的易失性状态以及开始新线程执行的过程。

如前所述,Windows在线程粒度级别进行调度。 当您认为进程不运行时,这种方法是有意义的;相反,它们只提供资源和线程运行的上下文。 由于调度决策是严格在线程的基础上进行的,所以没有考虑线程从属于哪个进程。 例如,如果进程A有10个可运行线程,进程B有2个可运行线程,并且所有12个线程都处于相同的优先级,理论上每个线程将获得CPU时间的十二分之一。 也就是说,Windows不会把50%的CPU执行力分配给给进程A,把50%的CPU执行力分配给进程B。

优先级

要理解线程调度算法,首先必须了解Windows使用的优先级级别。 如下图所示,Windows内部使用32个优先级级别,从0到31不等(31是最高的)。

这些优先级的对应值划分如下:

  • 16个Real-Time级别(16到31)
  • 16个Dynamic级别(0到15),其中0级为零页线程保留

线程优先级级别是从两个不同的角度分配的:Windows API和Windows内核。Windows API首先通过创建进程时分配给它们的优先级类来组织进程(括号中的数字表示内核识别的内部PROCESS_PRIORITY_分类索引)

  • Real-Time (4)
  • High (3)
  • Above Normal (6)
  • Normal (2)
  • Below Normal (5)
  • Idle (1)

Windows API 中提供的SetPriorityClass函数允许将进程的优先级类更改为上述级别之一。

然后,它在这些进程中分配单个线程的相对优先级。 在这里,数字表示应用于进程基优先级的优先级增量:

  • Time-Critical (15)
  • Highest (2)
  • Above-Normal (1)
  • Normal (0)
  • Below-Normal (–1)
  • Lowest (–2)
  • Idle (–15)

Time-Critical和Idle级别(15和-15)称为饱和值(saturation),表示应用的特定级别,而不是真正的偏移量。 这些值可以传递给SetThreadPriority函数,以更改线程的相对优先级。

因此,在Windows API中,每个线程都有一个基本优先级,它是其进程优先级类及其相对线程优先级的函数。 在内核中,使用PspPriorityTable全局数组和前面提到的PROCESS_PRIORITY_CLASS索引将进程优先级类转换为基本优先级,它们分别设置4、8、13、14、6和10的优先级。 (这是一个不能更改的固定映射。) 然后将相对线程优先级作为该基本优先级的微分应用。 例如,优先级最高的线程将接收比其进程的基本优先级高两个级别的线程基本优先级。

下面的图表显示了从Windows优先级到内部Windows数字优先级的映射:

Time-Critical和Idle线程优先级保持各自的值,而不管进程优先级类别如何(除非它是实时的)。这是因为Windows API通过传入16或-16作为请求的相对优先级,从内核请求优先级的饱和。 用于得到这些值的公式如下(HIGH_PRIORITY等于31):

Time-Critical = (HIGH_PRIORITY+1) / 2 = 32 / 2 = 16

Idle = -(HIGH_PRIORITY+1) / 2 = -32 / 2 = -16

然后内核将这些值识别为饱和请求,并设置KTHREAD中的饱和字段(在线程调度相关信息中)。 对于正饱和,这导致线程在其优先级类(dynamic或Real-Time)中接收尽可能高的优先级;对于负饱和,它是尽可能低的优先级。 此外,将来更改进程基本优先级的请求将不再影响这些线程的基本优先级,因为处理代码中跳过饱和线程。

从上面的表格中可以看出:线程有7个可能的优先级,可以从Windows API中设置(高优先级类有6个级别)。Real-Time优先级类实际上允许将所有优先级级别设置在16到31之间。 表中所示的标准常量未涵盖的值可以用-7、-6、-5、-4、-3、3、4、5和6作为设置线程优先级的参数来指定。

不管线程的优先级是如何通过使用Windows API(进程优先级类和相对线程优先级的组合)来实现的,从调度程序的角度来看,只有最终的结果才是重要的。 例如,优先级级别10可以通过两种方式获得:具有最高(2)的线程相对优先级的正常优先级类进程(8),或高于正常优先级类进程(10)和正常线程相对优先级(0)。 从调度程序的角度来看,这些设置导致相同的值(10),因此这些线程在优先级方面是相同的。

进程只有一个基本优先级值,而每个线程有两个优先级值:当前优先值(dynamic)和基本优先值。 调度决策是根据当前优先级做出的。 在某些情况下,系统在dynamic范围(1到15)中短暂地增加线程的优先级。 Windows从不调整Real-Time范围(16到31)中线程的优先级,因此它们总是具有相同的基级和当前优先级。

线程的初始基本优先级是从进程基本优先级继承的。 默认情况下,进程从创建进程继承其基本优先级。 您可以在CreateProcess函数上覆盖此行为,或者使用命令行start命令。 您还可以通过使用SetPriorityClass函数或使用公开该函数的各种工具(如TaskManager或Process Explorer)来更改进程优先级。 (右键单击进程并选择一个新的优先级类。) 例如,您可以降低CPU密集型进程的优先级,使其不干扰正常的系统活动。 更改进程的优先级会使线程优先级上下变化,但它们的相对设置保持不变。

通常,用户应用程序和服务以正常的基本优先级开始,因此它们的初始线程通常在优先级8级执行。 然而,一些Windows系统进程(如会话管理器、服务控制管理器和本地安全身份验证进程)的基本进程优先级略高于正常类(8)的默认值)。 这个较高的默认值确保了这些进程中的线程将以高于默认值8的更高优先级启动。

Real-Time优先级

您可以在任何应用程序的dynamic范围内提高或降低线程优先级。 然而,您必须具有增加调度优先级特权(Se增加基本优先级特权)才能进入Real-Time范围。 请注意,许多重要的Windows内核模式系统线程运行在Real-Time优先级范围内,因此如果线程在此范围内花费过多的时间运行,它们可能会阻塞关键的系统功能(例如内存管理器、缓存管理器或某些设备驱动程序)。

使用标准的Windows API,一旦进程进入Real-Time范围,其所有线程(甚至Idle线程)都必须在Real-Time优先级级别之一运行。 因此,不可能通过标准接口在同一进程中混合Real-Time线程和dynamic线程。

这是因为SetThreadPriority函数用ThreadBasePriorityInformation类调用本机NtSetInformationThread函数,允许优先级只保持在相同的范围内。 此外,这个信息类只允许在-2到2(Time-Critical / Idle)的公认WindowsAPI 中进行优先级更改,除非请求来自CSRSS或其他Real-Time进程。换句话说,这意味着Real-Time进程可以在16到31之间的任何值选择作为线程优先级,尽管标准的Windows API相对线程优先级似乎限制了前面展示的表中的选择。

如前所述,使用一组特殊值之一调用SetThreadPriority函数会导致使用ThreadBasePriorityInformation类调用NtSetInformationThread函数,线程的内核基本优先级可以直接设置,包括在Real-Time进程的dynamic范围内。

名称real-time并不意味着Windows是术语共同定义中的实时操作系统。 这是因为Windows没有提供真实的实时操作系统设施,例如保证中断延迟或线程获得保证执行时间的方法。 “实时”一词实际上意味着“高于所有其他的。”


线程的状态

在查看线程调度算法之前,我们必须了解线程可以进入的各种执行状态。 线程状态如下:

  • 就绪(Ready) 处于就绪状态的线程在完成等待后等待执行或被交换。 在寻找要执行的线程时,分发器只考虑处于就绪状态的线程。
  • 延迟就绪(Deferred ready) 此状态用于选择在特定处理器上运行但实际上尚未开始运行的线程。 这种状态的存在使得内核能够最小化调度数据库上每个处理器锁的保存时间。
  • 备用(Standby) 处于备用状态的线程在特定处理器上运行。 当存在正确的条件时,分发器将执行上下文切换到此线程。 系统上每个处理器只能有一个线程处于备用状态。 请注意,在线程执行之前,可以将其抢占到备用状态之外(例如,如果在待机线程开始执行之前,更高优先级的线程变得可运行)。
  • 运行(Running) 分发器执行上下文切换到线程后,线程进入运行状态,并执行。 线程的执行一直持续到其量子(时间片)结束(并且处于相同优先级的另一个线程已经准备好运行),它被高优先级线程抢占,它终止,它放弃执行,或者它自愿进入等待状态。
  • 等待(Waiting) 线程可以通过几种方式进入等待状态:线程可以自愿等待对象同步执行,操作系统可以代替线程等待(例如解析分页I/O),或者环境子系统可以指示线程挂起自己。 当线程的等待结束时,取决于它的优先级,线程要么立即开始运行,要么被移动回就绪状态。
  • 转换(Transition) 如果线程已准备好执行,但其内核堆栈已从内存中分页,则线程将进入转换状态。 将其内核堆栈带回内存后,线程进入就绪状态。
  • 终止(Terminated) 当线程完成执行时,它会进入终止状态。 线程终止后,执行线程对象(描述线程的系统内存中的数据结构)可能被释放,也可能不被释放。 对象管理器设置有关何时删除对象的策略。 例如,如果线程有任何打开的句柄,则对象保持不变。 线程也可以从其他状态进入终止状态,如果它被其他线程显式地杀死——例如,通过调用TerminateThread函数。
  • 初始(Initialized) 此状态在创建线程时在内部使用。

下图显示了线程的主要状态转换。 (显示的数值表示每个状态的内部值,可以使用诸如Performance Monitor之类的工具查看。) 就绪状态和延迟就绪状态被图中表示为一个状态。 这反映了延迟就绪状态作为调度例程的临时占位符的事实。 对于备用状态也是如此。 这些状态几乎总是非常短暂的。 这些状态中的线程总是快速过渡到就绪、运行或等待。

Preemption: 抢占

Quantum End: 量子结束(时间片结束)

Voluntary switch: 自愿切换

Kernel Stack Inswap/Outswap: 内核栈换入/换出

分发器数据库

为了进行线程调度决策,内核维护一组数据结构,统称为分发器数据库。 分发器数据库跟踪哪些线程正在等待执行,哪些处理器正在执行哪些线程。

为了提高可伸缩性,包括线程分配并发,Windows多处理器系统每个处理器拥有分发器就绪队列和共享处理器组队列,如下图所示。 这样,每个CPU就可以检查自己的共享就绪队列,以便下一个线程运行,而不必锁定整个系统的就绪队列。

P代表进程,T代表线程

就绪队列、就绪摘要和其他一些信息存储在存储在PRCB中的名为KSHARED_READY_QUEUE的内核结构中。 虽然它存在于每个处理器上,但它只在每个处理器组的第一个处理器上使用,并与该组中的其他处理器共享。

分发器就绪队列(Read List Head in KSHARED_READY_QUEUE)包含处于就绪状态、等待计划执行的线程。 32个优先级级别中的每个级别都有一个队列。 为了加快选择要运行或抢占哪个线程,Windows维护了一个称为就绪摘要(Ready Summary)的32位位掩码)。 每个位集表示该优先级级别的就绪队列中的一个或多个线程(位0表示优先级0,位1优先级1,以此类推)。

与其扫描每个就绪队列以查看它是否为空(这将使调度决策取决于不同优先级线程的数量),不如以本机处理器命令的形式执行单个位扫描以查找最高位集。 无论就绪队列中的线程数量如何,此操作只需要一个常量时间。

量子

量子是线程在Windows检查是否在相同优先级的另一个线程等待运行之前被允许运行的时间。 如果一个线程完成了它的量子,并且它的优先级没有其他线程,Windows允许线程运行另一个量子。

在Windows的客户端版本中,线程默认运行两个时钟间隔。 在服务器系统中,线程默认运行12个时钟间隔。 服务器系统中较长默认值的基本原理是最小化上下文切换。 因客户端请求而唤醒的服务器应用程序,通过具有更长的量子,有更好的机会完成请求,并在其量子结束之前返回到等待状态。

时钟间隔的长度根据硬件平台的不同而不同。 时钟中断的频率取决于HAL,而不是内核。 例如,大多数x86单处理器的时钟间隔约为10毫秒,对于大多数x86和x64多处理器,则约为15毫秒。 这个时钟间隔存储在内核变量 KeMaximumIncrement 中,为数百纳秒。

虽然线程以时钟间隔为单位运行,但系统不使用时钟Tick的计数作为线程运行多长时间及其量子是否过期的度量。 这是因为线程运行时的核算基于处理器周期。 当系统启动时,它将处理器速度(每秒CPU时钟周期)乘以一个时钟Tick触发所需的秒数(基于前面描述的KeMaximumIncrement),以计算每个量子等效的时钟周期数。 此值存储在内核变量 KiCyclesPerClockQuantum 中。

这种计算方法的结果是线程实际上不运行基于时钟Tick的量子数。 相反,它们运行于一个量子目标,它表示线程在停止运行时所消耗的CPU时钟周期的估计数。 这个目标应该等于相等数量的时钟间隔计时器Tick。 这是因为,每个量子的时钟周期的计算是基于时钟间隔计时器频率的。

量子计算

每个过程在过程控制块(KPROCESS)中都有一个量子重置值)。 此值在进程内创建新线程时使用,并在线程控制块(KTHREAD)中复制,然后在给线程一个新的量子目标时使用。 量子重置值以实际量子单元(我们将讨论这些意味着什么)存储,然后乘以每个量子的时钟周期数,从而产生量子目标。

当线程运行时,CPU时钟周期在不同的事件中(例如上下文切换、中断和某些调度决策)被减少。 如果在时钟间隔计时器中断时,已使用的CPU时钟周期数已达到(或已超过)量子目标,则触发量子结束处理。 如果有另一个线程处于相同的优先级等待运行,则对就绪队列中的下一个线程发生上下文切换。

在内部,量子单元表示为时钟Tick的三分之一。 也就是说,一个时钟Tick等于三个量子。 这意味着在客户端Windows系统中,线程的量子重置值为6(2*3),服务器系统的量子重置值默认为36(12*3)。 因此,在前面描述的计算结束时,KiCyclesPerClockQuantum除以3,因为原始值只描述每个时钟间隔计时器Tick的CPU时钟周期。

量子在内部存储为时钟Tick的一小部分而不是整个Tick的原因是,需要允许在等待完成时可以部分地减少量子(在Windows Vista之前的版本上是如此)。 以前的版本使用时钟间隔计时器来计算量子是否被用完。 如果没有进行这种调整,线程就有可能永远不会减少它们的量子。 例如,如果一个线程运行,进入等待状态,再次运行,并进入另一个等待状态,但在时钟间隔计时器激发的时候它总不是当前运行的线程,那么系统永远也不会从它的量子中扣除它运行的时间。 由于线程现在使用CPU时钟周期而不是量子来进行时间计算,并且量子值也不再以来时钟间隔计时器来定义,这些调整就没必要了。

⬆︎TOP