一如既往的开心。一直在,就好。

来自Windows核心编程 – 第七章。

这篇文章会讲到Windows的进程线程调度,线程的优先级以及线程和CPU的关联性。

首先列一个大纲目录把:

7.1 线程的挂起和恢复

7.2 进程的挂起和恢复

7.3 睡眠

7.4 切换到另一个线程

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

7.6 线程的执行时间

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

7.8 线程优先级

7.9 从抽象角度看优先级

7.10 优先级编程

7.11 关联性

 

Windows系统是一种抢占式操作系统,它必须使用某种算法确定何时调用哪些线程,时间又是多长。

每一个线程都有一个 CONTEXT 结构,保存在线程的内核对象中,它反映了线程上一次执行时CPU寄存器的状态。每隔一段时间(大约20ms),Windows都会查看当前存在的线程内核对象,在这些对象中选择一个,并将上次保存在线程内核中的 CONTEXT 中的值载入CPU寄存器。

上面的操作被称为 上下文切换 (context switch) – 上下文切换;环境切换;关联转换;

一个线程被调度后…大约再过20ms,Windows将CPU寄存器的值存回线程的上下文,线程不再运行。然后再次检查剩下的可调度线程内核对象,再选择一个载入。

Windows之所以被称为抢占式多线程操作系统,是因为系统可以在任何时刻停止一个线程而另行调度另一个线程。

系统只调度可以被调度的线程,实际上,大多数线程都是不可以调用的。例如:某个线程对象的挂起计数大于0,或者它正在等待某个事件发生等待…

 

7.1 线程的挂起与恢复

在线程内核对象中,有一个值表示线程的挂起计数。

调用 CreateProcess(创建进程) 或者 CreateThread(创建线程)时,系统都会创建线程内核对象,并将挂起计数初始化为1。这样就不会调度这个线程了。因为:线程初始化需要时间,所以希望在线程准备好后再开始执行它。

在线程初始化后,CreateProcess 或者 CreateThread 函数将查看是否有 CREATE_SUSPENDED 标志传入。如果有,函数会返回并让线程保持挂起状态。如果没有,函数会将线程的挂起计数递减为0,这样就变为一个可调度线程了。

当然如果线程一开始就在等待某事件的发生的话,那么它还是不可调度的。

 

创建一个处于挂起状态的线程,我们可以在线程执行任何代码之前,改变它的环境(比如优先级等)。改变了线程的环境后,就可以使其变为可调度的。

调用下面的函数,传入CreateThread时所返回的线程句柄 或者 CreateProcess 时的 ppiProcInfo 参数指向的结构中的线程句柄来实现:

如果调用成功,会返回线程的前一个挂起计数,否则会返回 0xFFFFFFFF .

一个线程可以被多次挂起,如果一个线程被挂起三次,则需要恢复三次。

除了在创建的时候传入参数,还可以用下面的函数来挂起线程:

函数的返回值也是返回之前的挂起计数。

任何线程都可以调用上面的函数来挂起另一个线程(只要有线程句柄)。

很显然,线程可以挂起自己,但是无法恢复自己。

一个线程最多可以挂起 127次 (MAXIMUM_SUSPEND_COUNT)

就内核模式下执行而言,SuspendThread 是异步的,但是线程恢复之前是无法在用户模式下执行的。

 

7.2 进程的挂起和恢复

一般来说 Windows里面是不存在挂起和回复进程的概念,因为系统从来不会给进程调度CPU的…

但是怎样挂起一个进程中的所有线程?

枚举系统中的线程列表,一旦找到属于某个进程的线程,便调用OpenThread。这个函数将找到线程ID匹配的线程内核对象,并使内核对象的使用计数增加1,然后返回对象句柄。有了这个句柄之后,我们就可以调用 SuspendThread函数了。

同样的我们可以依次枚举,然后调用ResumeThread函数。

要注意的是:在代码运行的过程中,可能有新的线程被创建,可能有旧的线程被删除…

都可能导致程序的失败…

 

7.3 睡眠

线程还可以告诉系统,在一段时间内自己不需要调度了。

通过Sleep函数实现:

这个函数将使线程自己挂起 dwMilliseconds 长的时间。

以下几项需要注意:

  1. 调用Sleep,将使线程自愿放弃属于它的时间片中的剩下的部分。
  2. 系统设置线程不可调度时间只是近似于设定的毫秒数。比如告诉系统睡眠100ms,可能大概是100ms就继续运行了,也可能几秒甚至数分钟都不运行。Windows不是实时操作系统。
  3. 调用Sleep传入 INFINITE 表示永远不要调度这个线程。恩,还不如直接退出呢
  4. 可以给Sleep函数传入0,这样表示当前时间片剩下的部分。它强制系统重新调度一个线程,当然系统可能调度刚刚调用了Sleep函数的那个线程。

 

7.4 切换到另一个线程

系统提供了一个名为SwitchToThread 的函数,如果存在另一个可调度线程,那么系统会让此线程运行。

调用这个函数时,系统查看是否存在急需 CPU 时间的饥饿线程。

如果没有,SwitchToThread 立即返回。如果存在,SwitchToThread将调度该线程(其优先级可能比SwitchToThread的主调线程低)。饥饿线程可以运行一个时间量,然后系统调度程序恢复正常运行。

通过这个函数,需要某个资源的线程可以强制一个可能拥有该资源的低优先级的线程放弃资源。如果在调用SwitchToThread 时没有其他线程可以运行,则函数将返回FALSE;否则函数将返回非0值。

其实调用SwitchToThread 和 Sleep(0) 是类似的,但是区别在于 SwitchToThread 运行执行低优先级线程,而Sleep 会重新调度调用Sleep的线程,即使低优先级的线程还处于饥饿状态。

 

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

超线程处理器芯片有多个“逻辑” CPU ,每个都可以运行一个线程。每个线程都有自己的体系机构状态(一组寄存器),但是所有的线程共享主要的执行资源。

当一个线程终止时,CPU会自动执行另一个线程,无需操作系统干预。

只有在缓存未命中、分支预测错误和需要等待前一个指令的结果的情况下,才会暂停。

 

7.6 线程的执行时间

GetThreadTimes 函数可以返回线程已获得的 CPU 时间量。

GetThreadTimes

以上四个返回值,都是用 100ns 为单位的。

这样我们是可以用这段代码来运行时间的。

还有一个函数是 GetProcessTimes ,返回的时间适用于一个指定进程的所以线程(即使线程已经终止)。

 

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

系统使用 CONTEXT 结构记住线程的状态。

Windows 允许我们查看线程的内核对象的内部,并获取当前 CPU 寄存器状态的集合。

为此我们需要调用:

为了调用这个函数,我们需要分配一个CONTEXT结构,初始化一些标志以表示要获取哪些寄存器。

需要注意的是,在获取信息之前应该先挂起线程,这样才能保证线程上下文获得信息是一样的。

线程由两个上下文(用户模式和内核模式),调用该函数只能得到用户模式的值,挂起后用户模式是暂停的,所以这时候调用该函数是安全的。

在调用这个函数之前,应该初始化 CONTEXT 结构的 ContextFlags 成员。

Context.ContextFlags = CONTEXT_FULL 表示获取所有重要寄存器。

还可以 SetThreadContext 来改变结构中的成员并把新的寄存器的值放回线程内核对象中。

 

7.8 线程优先级

每个线程都被赋予 0(最低) ~ 31(最高) 的优先级数。

当系统确定分配CPU时,会先查看优先级为31的线程,并以轮询的方式调度。

只要有优先级为31的线程可供调度,系统就不会给优先级为0~30的线程分配CPU,这种情况称为饥饿。即:较高优先级线程一直占用CPU致使低优先级线程无法运行。

另一个要注意的是:较高优先级总是会抢占较低优先级的线程,无论较低优先级的线程是否正在执行。

举个例子:比如优先级为5的线程正在运行,这时候有一个优先级为6的线程准备好了可以运行,它就会暂停优先级为5的那个线程,然后运行优先级为6的线程。

PS.系统启动时,会创建一个名为 页面清零线程(Zero Page Thread) 的特殊线程,这个线程的优先级定义为0,而且是整个系统中唯一一个优先级为0的线程。它的工作是在没有其他进程需要执行的时候,将系统内存中的所有闲置页面清零。

 

7.9 从抽象角度看优先级

Windows 支持6个优先级类(priority class) : idle, below, normal, above normal, high 和 real-time.

当然,normal是最常用的优先类,为99%的应用程序所使用。

优先级类 描述
Real-Time (实时) 此进程中的线程必须立即响应事件,执行实时任务。此进程中的线程还会抢占操作系统的组件的CPU时间。使用该优先级类需要极为小心。
high (高) 此进程中的线程必须立即响应事件,执行实时任务。任务管理器运行在这一级,因此用户可以通过它结束失控的进程。
above normal (高于标准) 此进程中的线程运行在 normal 和 high 优先级类之间
normal (标准) 此进程中的线程无需特殊的调度
below normal (低于标准) 此进程中的线程运行在 normal 和 dile 优先级类之间
idle (低) 此进程中的线程在系统空闲时运行。屏幕保护程序、后台实用程序 和 统计数据收集软件通常实用该进程。

优先级类 又可以叫做 进程优先级。

idle 优先级类 适用于在系统什么都不做的时候运行的应用程序。

只有在绝对必要的时候才使用 high 优先级类。

应该尽可能避免使用 real-time 优先级类。

 

选择了 优先级类(进程优先级) 之后,我们就不需要考虑应用程序与其他应用程序的关系了,而应该转而关注自己应用程序里的线程。Windows支持7个相对线程优先级: idle, lowest, below normal, normal, above normal, highest 和 time-critical。这些优先级是相对于 优先级类(进程优先级) 的。

同样的,大多数线程使用 normal 线程优先级。

相对线程优先级 描述
time-critical 对于 real-time 优先级类,线程运行在31上;所有其他优先级运行在 15
highest 线程运行在高于 normal 之上两个级别
above normal 线程运行在高于 normal 之上一个级别
normal 线程运行在进程 normal 级别上
below normal 线程运行在低于 normal 之下一个级别
lowest 线程运行在低于 normal 之下两个级别
idle 对于 real-tine 优先级类,线程运行在16;所有其他优先级运行在 1

总结上面就可以知道:

  • 进程都属于某个优先级类
  • 可以指定进程中线程的相对线程优先级

关于 进程优先级 和 线程优先级 与 优先级值的映射 书上有写出来,我就不列举了。

 

7.10 优先级编程

可以给进程指派优先级的。调用 CreateProcess 时,可以在 fdwCreate 参数中传入需要的优先级。

首先是优先级类 – 进程优先级:

优先级 标识符
real-time REALTIME_PRIORITY_CLASS
high HIGH_PRIORITY_CLASS
above normal ABOVE_NORMAL_PRIORITY_CLASS
normal NORMAL_PRIORITY_CLASS
below normal BELOW_NORMAL_PRIORITY_CLASS
idle IDLE_PRIORITY_CLASS

进程可以通过调用下面的函数来修改自己的优先级:

比如,可以这样修改自己进程的优先级为 idle:

也可以获取进程优先级:

 

接下来是线程优先级:

设置线程优先级函数:

同样的标识符如下:

 相对线程优先级 符号常数
time-critical THREAD_PRIORITY_TIME_CRITICAL
highest THREAD_PRIORITY_TIME_HIGHEST
above normal THREAD_PRIORITY_ABOVE_NORMAL
normal THREAD_PRIORITY_NORMAL
below normal THREAD_PRIORITY_BELOW_NORMAL
lowest THREAD_PRIORITY_LOWEST
idle THREAD_PRIORITY_IDLE

相应的,获取线程的相对优先级的函数是:

 

我们来看个例子吧,设置一个线程的优先级为 相对优先级 – idle

清晰明了…自己看~~哈哈

需要注意的是,CreateThread 总是创建相对线程优先级为 normal 的新线程。

 

7.10.1 动态提升线程优先级

系统通过线程的相对优先级加上线程所属进程的优先级来确定线程的优先级。

有时候,系统也会提示一个线程的优先级——通常是为了响应某种I/O事件比如窗口消息或者磁盘读取。

要注意的是,系统只提示优先级值低于16的线程——16以上就是实时了…

有两个函数可以用来禁止动态提升,这里不详细讲。

 

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

如果用户是使用某个进程的窗口,这个进程就成为前台进程,而此时所有其他的进程都被成为 后台进程。显然为了照顾用户体验,改进前台线程的响应性,Windows会为前台进程中的线程微调调度算法,获得更多时间片。

 

7.11 关联性

Cache在CPU上这种情况下,让线程始终在同一个 CPU 上运行有助于重用仍在Cache中的数据。

有函数可以设置闲置某些线程只能在某些指定CPU上运行。

 

大致就是这样了。

【Windows核心编程】线程调度、优先级和关联性
Tagged on:
5 1 投票
Article Rating
订阅评论
提醒

0 评论
内联反馈
查看所有评论