《Windows核心编程》——七 线程调度、优先级和关联性

时间:2020-12-29 19:26:04

前言

    每个线程都有一个上下文(CONTEXT),后者保存在线程的内核对象中。这个上下文反映了线程上一次执行时CPU寄存器的状态。大约每隔20ms,Windows都会查看所有当前存在的线程内核对象。在这些对象中,只有一些被认为是可调度的。Windows在可调度的线程内核对象中选择一个,并将上次保存在线程上下文的值载入CPU寄存器。这一操作被称为上下文切换。Windows实际上会记录每个线程运行的次数。

    Windows之所以被称为抢占式多线程操作系统,是因为系统可以在任何时刻停止一个线程而另行调度另一个线程。系统只调度可调度的线程,但是事实上,系统中大多数线程都不是可调度的。除了被挂起的线程之外,还有其他很多线程无法调度,因为它们都在等待某种事件发生。系统不会给没有任务的线程分配任何CPU时间。

7.1 线程的挂起和恢复

在线程内核对象中有一个值表示线程的挂起此时。调用CreateProcess或CreateThread时,系统将创建线程内核对象,并把挂起次数初始化为1.这样,就不会给这个线程调度CPU了。这正是我们所希望的,因为线程初始化需要时间,我们当然不想再线程准备好之前就开始执行它了。在线程初始化之后,CreateProcess或CreateThread函数将查看是否有CREATE_SUSPENDED标志传入。如果有,函数会返回并让新的线程处于挂起状态。如果没有,函数会将线程的挂起次数递减为0.当线程的挂起次数为0时,线程就变成可调度的了,除非它还在等待某个事件发生。


就内核模式下面执行情况而言,SuspendThread是异步的,但在线程恢复之前,它是无法在用户模式下执行的。实际开发过程中,在调用SuspendThread时必须特别小心,因为试图挂起一个线程时,我们不知道线程在做什么。例如,如果线程正在分配堆中的内存,线程将锁定堆。当其他线程要访问堆的时候,它们的执行将被中止,直到第一个线程恢复。

    可以通过在创建线程时使用CREATE_SUSPENDED标志,或者通过调用SuspendThread来挂起线程。任何线程都可以调用这个挂起函数挂起另外一个线程(只要有线程的句柄),显然,线程可以将自己挂起,但是它无法自己恢复。该函数返回线程之前的挂起次数。就内核模式下面执行情况而言,SuspendThread是异步的,但在线程恢复之前,它是无法在用户模式下执行的。一般不使用SuspendThread这个函数,因为用户不知道线程在做什么。

    可以通过ResumeThread来恢复一个线程,该函数返回线程线程的前一个挂起次数。


7.2 进程的挂起和恢复

    Windows中不存在挂起和恢复进程的概念,因为系统从来不会给进程调度CPU时间。只能说挂起进程中的所有的线程。

7.3 睡眠

    线程还可以告诉系统,在一段时间内自己不需要调度了,这可以通过调用Sleep来实现。《Windows核心编程》——七 线程调度、优先级和关联性

7.4 切换到另一线程(SwitchToThread)

    调用该函数时,系统查看是否存在正急需CPU时间的饥饿线程。如果没有,该函数立即返回。如果存在,该函数将调度线程(其优先级可能比SwitchToThread的主调线程低)。通过这个函数,需要某个资源的线程可以强制一个可能拥有该资源的低优先级的线程放弃资源。SwitchToThread允许执行低优先级线程,sleep则不能,Sleep会立即重新调度主调线程,即使低优先级线程还处于饥饿状态。。

 

7.5 在超线程CPU上切换到另一线程

    超线程处理器芯片有多个“逻辑”CPU,每个都可以运行一个线程。每个线程都有自己的体系结构状态(一组寄存器),但是所有线程共享主要的执行资源,比如CPU高速缓存。当一个线程中止时,CPU自动执行另一个线程,无需操作系统干预。只有在缓存未命中、分支预测错误和需要等待前一个指令的结果等情况下,CPU才会暂停。

 

7.6 线程的执行时间(GetThreadTimes)

    GetProcessTimes返回的时间适用于一个指定进程中的所有线程(即使线程已终止)。例如所返回的内核时间是所有线程在内核模式下所耗时间的总和。

 

7.7 在实际上下文中谈CONTEXT结构

    系统使用CONTEXT结构记住线程的状态,这样线程在下一次获得CPU可以运行时,就可以从上此停止处继续。事实上,在Windows定义的所有结构中,CONTEXT结构是唯一一个特定于CPU的。

    CONTEXT结构分为几个部分。CONTEXT_CONTROL包含CPU的控制寄存器,比如指令指针、栈指针、标志和函数返回地址。CONTEXT_INTEGER标识CPU的整数寄存器;CONTEXT_FLOATING_POINT标识CPU的浮点寄存器;CONTEXT_SEGMENTS标识CPU的段寄存器;CONTEXT_DEBUG_REGISTERS标识CPU的调试寄存器;CONTEXT_EXTENDED_REGISTERS标识CPU的扩展寄存器。Windows实际上允许我们查看线程的内核对象的内部,并获取当前CPU寄存器状态的集合,只需调用GetThreadContexts。在使用该函数前应该先调用SuspendThread;否则,系统可能正好获得调度此线程,这样一来,线程的上下文与所获取的信息就不一致了。一个线程实际上有两个上下文:用户模式和内核模式。该函数只能返回线程的用户模式上下文。GetThreadContext只能返回线程的用户模式上下文。如果调用SuspendThread暂停一个线程,但是该线程正在内核模式下执行,那么它的用户模式上下文保持不变,即使SuspendThread实际还没有暂停线程。线程恢复之前,不能再执行任何用户模式的代码。

    Windows还允许我们通过调用SetThreadContext来改变结构中的成员,并把新的寄存器值放回到线程的内核对象中。同样如果要改变哪个线程的上下文,应该先暂停该线程,在调用该函数前,必须再次初始化CONTEXT的ContextFlags成员。

 

7.8 线程优先级

每个线程都被赋予0(最低)~31(最高)的优先级。当系统确定给哪个线程分配CPU时,它会首先查看优先级为31的线程,并以循环的方式进行调度。只要有优先级为31的线程可供调度,系统就不会给优先级0~30的线程分配CPU。这种情况称为饥饿。当较高优先级的线程占用了CPU时间,致使低优先级的线程无法运行时,我们就称这种情况为饥饿。在多处理器机器上饥饿发生的可能性要小的多,因为这种机器上优先级为31和优先级为30的线程可以同时运行。系统总是保持各CPU处于忙碌状态,只有没有线程可供调度的时候,CPU才会空闲下来。

较高优先级的线程总是会抢占较低优先级的线程,无论较低优先级的线程是否正在执行。

系统启动时,将创建一个名为清零线程的t特殊线程。这个线程的优先级定为0,而且是整个系统中唯一一个优先级为0的线程。页面清零线程负责在没有其他进程需要执行的时候,将系统内存中的所有闲置页面清零。


 

7.9 从抽象角度看优先级

    Windows API在系统的调度程序之上提供了一个抽象层,因此我们不会直接调用调度程序。相反调用的是Windows函数,它们会根据底层操作系统的版本来“解释”参数。

    Windows支持6个优先级类:idle,below normal,normal,above normal,high和realtime。这是对于进程的优先级类。
《Windows核心编程》——七 线程调度、优先级和关联性

    real-time优先级别最高,可以影响操作系统的任务,因为大部分操作系统线程在执行时所用的优先级类都比它低。

    Windows支持7个相对线程优先级:idle,lowest,below normal,normal,above normal,highest和time-critical。这些优先级是相对于进程优先级的。

《Windows核心编程》——七 线程调度、优先级和关联性

 

    应用程序的开发人员无需处理优先级,而是由系统将进程的优先级类和线程的相对优先级映射到一个优先值。

《Windows核心编程》——七 线程调度、优先级和关联性

如上图所示,real-time优先级类的线程,其优先级值不能低于16;非real-time优先级线程的优先级值不能高于15。

 

7.10 优先级编程

    给进程指派优先级:通过调用CreateProcess时,在fdwCreate参数中传入需要的进程优先级;或者一旦进程运行,便可通过调用SetPriorityClass来改变自己的优先级。

    获取进程优先级:通过调用GetPriorityClass来获取一个进程的优先级。

   

    当线程最开始创建时,它的线程优先级总是设置为normal。CreateThread没有为调用者提供设置新线程相对优先级的办法。为了设置线程的相对优先级须在线程运行前调用SetThreadPriority。可以通过调用GetThreadPriority的相对优先级。Windows并没有提供返回线程优先级的函数。

7.10.1 动态提升线程优先级

    系统通过线程的相对优先级加上线程所属进程的优先级来确定线程的优先级值。有时候,这也被称为线程的基本优先级值。偶尔,系统也会提升一个线程的优先级——通常是为了响应某种I/O事件比如窗口消息或者磁盘读取。

    系统只提升优先级值在1~15的线程。事实上,正因为如此,这个范围被称为动态优先级范围。而且,系统不会把线程的优先级值提升到实时范围(高于15)。
    用户可以通过调用SetProcessPriorityBoost来决定允许或禁止提升一个进程中所有线程的优先级;通过SetThreadPriorityBoost来决定允许或禁止提升某个线程的优先级。可以通过GetProcessPriorityBoost或GetThreadPriorityBoost来判断当前是否启动优先级提升。

 

7.10.2 为前台进程微调调度程序

    这种微调只在前台进程是normal优先级才进行。如果处于其他优先级,则不会进行微调。

7.10.3 调度I/O请求优先级

    设置线程优先级将影响系统如何给线程分配CPU资源。但是,线程还要执行I/O请求,以对磁盘文件读写数据。如果一个低优先级线程获得CPU时间,它可以很轻易地在很短的时间内将成百上千个I/O请求入列。因为I/O请求一般都需要时间进行处理,可能低优先级线程会挂起高优先级的线程,使它们无法完成任务,从而显著影响系统的响应性。如在执行一些运行时间较长的低优先级服务比如磁盘碎片整理程序、病毒扫描程序、内容索引程序等的时候,机器的响应性会变得很差。

 

7.11 关联性

    默认情况下,Windows系统在给线程分配处理器时,使用软关联。意思是如果其他因素都一样,系统将使线程在上一次运行的处理器上运行。让线程始终在同一个处理器上运行有助于重用仍在处理器高速缓存中的数据。

    可以通过调用SetProcessAffinityMask来限制一个进程中的线程只在可用CPU的子集上运行。子进程会继承进程关联性。

    可以通过调用SetThreadAffinityMask来限制一个线程在某个CPU上执行。