线程调度
===============
Windows实现了一个优先级驱动,抢占式的调度系统--最高优先级的可运行的线程会一直运行下去, 线程只能运行在允许它运行的某些处理器上, 这种现象叫做processor affinity. 默认的, 线程可以运行在任意一个available的处理器上, 但是你可以通过Windows scheduling function来设置存在于image header中的affinity mask, 从而修改processor affinity.
当一个线程被选中运行时, 它运行一段时间, 这段时间叫做quantum(配额). quantum是这样的一段时间, Windows允许线程先运行着, 在这段时间过了之后, Windows就可以中断线程去做一些其他的事情, 包括判断是否有跟当前线程同优先级的或更高优先级的线程在等待, 和判断当前线程的优先级是否需要降低. Quantum值在不同的线程中间是不同的, 甚至在操作系统不同的时候也是不同的. 然而,一个线程可能不会完成它的quantum. 因为Windows 实现了抢占式调度器(scheduler), 如果另一个线程拥有更高的优先级, 当它准备好可以运行的时候, 当前运行的iancheng可能会在他的时间片结束之前被夺去cpu. 事实上, 线程甚至可以在开始他的quantum之前, 就被确定稍后再运行, cpu被抢掉.
Windows的线程调度代码是实现在内核中的. 不存在调度器模块或者函数, 调度代码是散落在内核中调度相关事件发生的处理中的. 这些执行线程调度的函数合起来被称为内核调度器(Kernel's dispatcher). 线程分配发生在DPC/dispatch级, 是由以下的任何事件触发的.
- 一个线程进入可执行状态(ready to execute), 比如说一个线程刚刚被创建出来, 或者刚刚被从等待状态中释放出来.
- 线程离开了运行状态, 因为他的quantum结束了, 或者因为他terminate了, 或者因为进入了等待状态了.
- 线程的优先级更改了, 或者由于系统服务调用, 或者由于Windows自己修改了优先级的值.
- 运行中的processor affinity修改了
任何一种情形下, Windows都必须决定下一个应该运行的线程. 当Widnows选择了一个新的线程来运行的时候, 他执行context switch(上下文转换). 上下文转换就是一个过程, 该过程保存与运行中的线程相关的不稳定的机器状态, 加载另一个线程的这些状态, 并开始新线程的执行.
很明显, windows是在线程的粒度上进行分配的. 进程的概念是一个不运行的单位, 仅仅是为它其 中的线程提供上下文和资源, 而这样的调度安排是符合这样的概念的. 因为调度的确定是严格的建立在线程的基础上的, 所以调度起来就不需要考虑线程究竟属于哪个进程. 比如说, 进程A 有10个runnable的线程, 进程B有两个, 所有十二个线程有同样的优先级, 每个线程都会分配到1/12的CPU时间, 而并不会给A 50%, 给B 50%.
中断优先级 VS 进程优先级
======================
前面讲过, 线程通常运行在IRQL 0 或者1 上. Windows Debugging之三介绍了IRQL的信息.
用户态的线程永远是运行在IRQL0上的, 即最低等级, 根本就算不上中断等级, 也可以说没等级, 任何中断都可以打断用户态线程的执行, 任何用户态的线程, 不管他的优先级有多高, 都不可能干扰硬件的中断.
只有内核态的APC执行在IRQL1的等级上, 因为他们可以任意的打断用户态的线程.
内核态运行的线程可以提高它的IRQL等级来进行一些特殊处理, 比如当执行一个涉及到线程分配的系统调用的时候(IRQL 2).
线程状态
详细介绍如下:
- Ready---dispatcher在寻找一个线程来执行的时候, 它只考虑线程池中处于ready状态的线程. 它们仅仅是在等待被执行.
- Standby---处于standby状态的线程已经被某一个处理器选作下一个被处理的对象了. 当正确的条件达到的时候, dispatcher为这个线程执行上下文转换. 对系统中的每一个处理器而言, 只有一个线程能处于standby的状态.
- Running---一旦dispatcher为某个线程执行了上下文转换, 那么这个线程就进入了running的状态. 线程的执行回持续下去, 直到以下的情况发生, 1. 内核抢占了它,以便把资源给更高等级的线程来使用. 2. 他的quantum结束了. 3. 他运行结束了(terminate). 4. 线程自己自愿的转入等待状态.
- Waiting---一个线程通过以下的几种途径进入等待状态: 1. 自愿进入等待状态, 去等待一个对象来同步化他的执行. 2. 操作系统(比如IO系统)或者环境子系统指导线程来挂起自己. 当线程的等待结束时, 再看他的优先级来决定该线程要么进入运行状态, 要么回到ready状态.
- Transition--- 当线程已经准备好,可以被执行的时候, 正巧内核栈的内存页被换出了内存, 该线程进入到transition状态. 当内核栈被带回内存的时候,线程进入到ready状态.
- Terminated---当一个线程结束了执行, 他就进入到一个结束的状态(terminated). 一旦结束了, 线程对象也许会被删掉,也许不会(对象管理器会设置这项政策的).
- Initialized--- 线程创建的时候, 在内部使用的状态(used internally)
配额(Quantum)
====================
quantum是一段线程运行的一段时间, 在这段时间之后, 操作系统检查是否有另外的据有同样优先级的线程需要进入运行状态. 如果一个线程完成了他的quantum, 并且没有其他的线程有跟它一样高的优先级, Windows会重新分配给这个线程一个quantum.
每一个线程都有一个quantum值, 该quantum值代表在quantum过期之前, 线程可以运行多久. 这个值并不是时间的长短, 而是一个整形值, 我们称它为quantum units.
配额的计算
------------------------
线程启动时,windows xp和windows 2000 professional默认的quantum值是6. 终极目标是最大程度的减小上下文切换的时间. 通过一个更长的quantum, 服务器应用程序有更好的机会来完成客户端发来的请求, 情切在配额结束之前返回到等待状态. Windows Server 2003的quantum默认值是36, windows 2000 server是12.
在时钟中断的任意时刻, 时钟中断处理程序从县城的quantum中扣除一个定值3. 如果线程的quantum已经没有了, 那么线程配额用光的处理程序被激活, 同时另一线程可能被选中,运行. 因为时钟中断每次扣除3, 所以每个线程会默认的运行两个时钟周期(XP).
即使系统在DPC/dispatch等级或更高(比如说一个DPC或者一个终端服务程序在执行的时候), 当时钟中断发生, 即使线程没有运行完毕一整个的时钟周期, 当前的线程还是会减少他的quantum. 如果减少quantum的动作还没完成, 设备就中断了; 或者DPC发生在时钟周期计时器中断之前, 线程可能永远不会减少它的quantum.
时钟周期的长度根据不同的硬件系统平台的不同而不同. 时钟中断的频率取决于HAL, 而不是内核. 比如说, 多数的x86单个处理器的时钟周期是10毫秒, 而多处理器的x86平台是15毫秒.
时钟tick一下, 我们用3个quantum来表达, 而不是一个的原因是, 这样可以允许quantum在等待结束的时候衰退掉一部分. 当一个线程拥有一个基础优先级低于14, 执行等待函数的时候(WaitForSingleObject or WaitForMultipleObjects), 他的quantum就减少1 quantum单位(优先级高于14或者就是14的线程,在等待结束之后会重新设置它们的优先级 reset).
这样的部分的减少解决如下的情况: 一个线程在时钟计时器触发之前就进入了等待状态. 如果没有调整的话, 他就可能永远不会减少他的quantum了. 比如说, 一个线程在运行, 进入了等待状态, 又运行了, 又进入了等待状态, 但是每当在时钟计时器触发的时候, 都不属于当前运行的线程, 所以这个线程的quantum就永远不会减少了.
自愿转换
=============
场景
Windows在面临问题"谁该得到CPU"的时候, 依靠线程优先级; 但是实际运作中这是如何做到的呢? 下面的部分就详细描述基于优先级的抢占式多任务系统式如何在线程级别上工作的.
自愿转换
首先, 一个线程可以通过进入等待某个对象的方式, 自愿的放弃对于处理器的使用. 比如说事件, 互斥量, 旗语信号, IO完成端口, 进程, 线程, 窗口消息等等. 进入等待某对象的方式是通过调用某些win32等待函数完成的, 比如WaitForSingleObject 或者WaitForMultipleObjects
自愿转换大致相当于一个线程在快餐桌前点还没做好的菜一般. 与其占着点餐的队伍, 不如站到一边去, 在大厨做它的汉堡的时候, 让其他的线程去执行他们的请求. 当汉堡做好了, 第一个线程排到跟它优先级相同的ready的队伍的最后面. 无论如何, 多数的等待操作都会导致暂时的优先级增加, 这样线程可以在它的汉堡准备好了之后快速的拿走它并开始进食.
下图中, 最上面的线程自愿的放弃了处理器, 所以下一个ready队列中的线程(脑袋上有光圈儿的那个方块)可以运行了. 尽管从这张图上看去, 觉得自愿放弃的线程的优先级降低了, 其实它没有. 它仅仅是被移到等待的队列中去了. 那这个线程剩下的quantum怎么办呢? quantum值在进入等待状态的时候并不会被重设, 事实上, 正如早些时候讲过的, 当等待满足的时候, 线程的quantum会减少一个quantum的单位, 等于一个时钟周期的三分之一(除了优先级为14或14以上的线程, 他们的quantum在等待后会被重设.)
抢占
================
在这个场景下, 一个低等级的线程会被高等级的刚刚进入ready状态的线程给抢掉cpu. 有可能基于如下的原因发生这种情况:
1. 更高优先级的线程的等待结束了.(这个事件发生在其他线程等待已经发生的时候)
2. 一个线程的优先级要不增加了,要不减少了.
两者中的任何一个, 都可以导致高优先级的线程抢占CPU.
注意: 运行在内核态的线程可以被用户态的线程抢占的. 线程是否运行在内核态跟抢占无关. 线程的优先级是做抢占决定的指标.
当一个线程被抢占, 他被放在它的优先级的那个ready队列的头部, 这样当它再次运行的时候, 它就可以结束掉他的quantum.
下图中, 一个拥有优先级18的线程从等待状态恢复过来, 重新获得了cpu, 引起了一个优先级为16的线程被打到ready队列的队首 注意,是队首, 而不是队尾.
配额结束
=================================
当一个线程用光了他的cpu时间片配额, windows必须决定是否减小这个线程的优先级, 还有是否另个线程应该被安排到处理器上.
如果线程优先级降低了, Windows寻找一个更合适的线程来安排.(比如说, 一个更适合的线程应该是在ready队列里的, 比当前运行的线程的新优先级拥有更高的优先级的线程). 如果线程优先级没有降低, Widnows选择ready队列里的下一个线程, 然后把先前的线程放到那个队列的尾部(给它一个新的quantum, 修改它的状态为ready). 这样的情形在下图中有表示出来. 如果没有其他的相同优先级的线程处在ready的状态, 那么当前线程就再运行一个quantum.
事实上, 线程拥有quantum并不意味着它一定要运行结束那个quantum. 一个线程可能在他的时间片结束之前自愿的放弃对于CPU的占用, 要么它进入等待状态, 要么它被更高优先级的线程打到ready队列的头上去. 如同前面讨论的, 如果自愿放弃控制CPU, 线程会被在再次运行的时候, 被分配一个新的quantum值. 如果线程被抢占了, 不管怎样, 他被挪到相应优先级的ready队列的头上, 晚些时候, 当它再被安排上的时候, 线程就会运行至时间片结束.