Windows XP 中断与异常(段内容补充二)
6 时钟中断与定时器
定时器(timer)是一种被广泛使用的软件机制,应用软件可以用定时器来实现动画和其他各种周期性任务等,设备驱动程序利用定时器来控制超时,操作系统利用定时器来执行线程调度、时间计最等。Windows 提供了一种定时器内核对象,本章后面5.4.2 节将会介绍内核提供的各种同步对象,其中就包括定时器对象。这一节我们先讨论 Windows 如何管理定时器和实现定时任务。
6.1 时钟中断时限处理
Windows 中最底层的定时器机制是通过时钟中断加上DPC对象来实现的。我们首先从时钟中断入手来检查最原始的时间控制点。时钟中断是由本地 APIC 来控制的,HAL模块负责设置时钟中断向量、中断发生的频率,具体的时钟间隔与 HAL 模块有关,通常为 10~15 ms。在 Intel x86 处理器的 Windows 系统中,时钟中断的 IRQL为28。时钟中断的服务例程由 HAL 提供,它调用了内核模块的 KeUpdateSystemTime
函数。
KeUpdateSystemTime
功能:
- 它首先更新中断时间和系统时间(位于用户共享数据区一个用户模式和内核模式代码均可访问的内存页面),以及滴答计数(全局变量_KeTickCount)。
- 然后,
KeUpdateSystemTime
判断定时器数组中是否有定时器到期,如果有,则设置 KPRCB 的TimerRequest
标志,并通过调用HalRequestSoftwarelnterrupt
函数请求DISPATCH_LEVEL
软件中断。最后,KeupdateSystemTime
调用KeUpdateRunTime
函数,更新线程和进程的运行时间。
KeUpdateRunTime
函数功能:
- 它首先更新内核模式的总时间、中断所花的时间(如果当前在中断例程中),然后更新当前线程和进程的内核模式时间或用户模式时间。
- 接着,
KeUpdateRunTime
函数刷新 DPC 请求率,如果当前 DPC 链表不为空,则有必要的话调用HalRequestSofiwarelnterrupt
函数请求DISPATCH_LEVEL
软件中断。 - 最后,
KeUpdateRunTime
扣除当前线程的时限,扣除的单位为 3(CLOCK_QUANTUM_DECREMENT
),参考 3.5.3 节中关于线程时限管理的介绍。 - 如果扣除以后线程的时限仍大于0,则函数返回,否则设置 KPRCB 的
QuantumEnd
标志(若QuantumEnd != 0
表示时限用完),并请求一个DISPAT_CHLEVEL
的软件中断。
值得一提的是,在多处理器系统中,第一个处理器(即 P0)上的时钟中断例程调用 KeUpdateSystemTime
函数,而其他处理器上的时钟中断例程调用 KeUpdateRunTime
函数。从上述两个函数的功能不难理解,系统全局的时间信息由第一个处理器来维护,而其他的处理器只负责处理该处理器上的 DPC 以及与线程调度有关的事项。
在 KeUpdateSystemTime
和 KeUpdateRunTime
两数中,有三件事情可以导致立即请求一个 DISPATCH_LEVEL
的软件中断:有 DPC 需要处理、定时器到期,以及当前线程的时限已结束。
根据上一节的介绍,DISPATCH_LEVEL
软件中断的处理函数 KiDispatchInterrupt
,以及它所调用的函数 KiRetireDpcList
,正好可以完成这些事情。
6.2 定时器处理
系统中所有的定时器构成了一个链表数组,他们都将会挂入全局数组 KiTimerTableListHlead
包含的 256 个定时器链表中,链表串着定时器 KTIMER 对象的 TimerListEntry
成员。相关的C定义如下:
1 |
|
DueTime
:任何一个定时器都有一个指定的到期时间,即 KTIMER 结构中的 DueTime 值。当一个定时器被插入到 KiTimerTableListHead
中时,它被插入到哪个链表是由 DueTime
决定的,即根据 DueTime
的值,计算得到一个下标 Index
值。KiComputeTimerTableIndex
函数完成这一计算。此计算式的基本思路是,**将 DueTime
除以每个滴答(tick)的最大间隔时间,即把 DueTime
转换成以滴答为单位,然后模上TIMER_TABLE_SIZE
**(在代码中使用了 and
指令,对于2的幂次来说,这是等价的)。
因此,每次时钟中断经过一个滴答,它只需检查 KiTimerTableListHead
数组中的一个链表,即符合当前滴答时间值作为到期时间的定时器链表,同时也判断属于下一个滴答的定时器链表。我们在 KeUpdateSystemTime
函数中可以看到,如果检测到当前滴答计数(全局变量 KeTickCount
)或下一个滴答计数所对应的链表中的 Time
域已经小于等于当前中断时间值,则设置当前处理器的 KPRCB 中的 TimerRequest
标志,并且设置 KPRCB 的 TimerHand
域指向刚刚到期的定时器链表的数组下标,然后调用 HalRequestSoftwarelInterrupt
函数请求 DISPATCH_LEVEL
软件中断。
1 | kd> dd KeTickCount |
⚠️注意:Windows XP中的没有 WRK 关于定时器这部分的变化:
- WRK 中说的数组
KiTimerTableListHead
的每一项是一个_KTIMER_TABLE_ENTRY
结构,XP中直接就是一个KTIMER
对象。 - 数组长度不同
- XP:
#define TIMER_TABLE_SIZE 256
- WRK:
#define TIMER_TABLE_SIZE 512
- XP:
Index
计算方法不同- XP:没有
hand
,Index = KiComputeTimerTableIndex(IN LARGE_INTEGER Interval,IN LARGE_INTEGER CurrentTime, IN PKTIMER Timer)
。 - WRK:
KiInsertTimerTable
数组中的每一个链表都有一个Hand
值,然后Index = Hand = KiComputeTimerTableIndex(DueTime)
。
- XP:没有
- 关于
KiTimerExpiration
函数- XP:
KiTimerExpiration
函数是在KiInitSystem
系统初始化时调用。 - WRK:如果 KPRCB 中的
TimerRequest
标志已被置上,则KiRetireDpcList
调用KiTimerExpiration
函数,并且将 KPRCB 中的TimerHand
传递给它。所以,KiTimerExpiration
函数知道该从KiTimerTableListHead
数组的哪个定时器链表开始处理。
- XP:
现在我们来看定时器被插入链表的做法。KiInsertTimerTable
函数定位到待插入的定时器链表,依据定时器的 DueTime
值的先后顺序,插入到链表中合适的位置上。
综上所述,我们可以知道,如果将定时器的到期时间 DueTime
换算成滴答计数,则 KiTimerTableListHlead
数组的每个链表中的定时器对模 TIMER_TABLE_SIZE
同余。
Windows 之所以使用一个二维数组来维护系统中所有的定时器对象,是从效率的角度考虑的,KiComputeTimerTableIndex
计算得到一个下标或索引,相当于做了一个优雅的散列处理。不同索引对应的链表的到期时间不一样。
与单个长链表相比,定时器的插入和到期处理都可以在相对较短的链表上进行,而且,在 KeUpdateSystemTime
函数中判断是否有到期定时器也可以非常快捷地实现。
从以上的介绍可以看出,定时器对象既具有普通同步对象的特征,也具有 DPC 对象的特征。本节仅仅介绍了定时器对象的到期处理,后面5.4.2 节还将进一步从同步对象的角度介绍定时器对象。
6.3 定时器对象
定时器对象既是一个像 DPC 这样的例程的包裝对象,也是一个分发器对象,并且分为定时器通知对象和定时器同步对象。
在前面5.2.5 节介绍定时器管理时,我们看到定时器数据结构 KTIMER 的第一个成员是 DISPATCHER_ HEADER 结构,因此,KTIMER 对象也可被用于等待函数,换句话说,一个线程可以等待一个定时器对象。有关定时器的操作的实现代码位于 baseintostketimerobj.c 和 timersup.c 文件中。下面是主要的定时器操作:
- KelnitializeTimer/KeInitializeTimerEx,初始化一个定时器对象。KelnitializeTimer 调用 KelnitializeTimerEx 函数来初始化一个通知类型的定时器。
- KeClearTimer,清除一个定时器对象的信号状态。
- KeCancelTimer,取消一个已经设定的定时器。如果该定时器对象尚未被设定,即DISPATCHER_ HEADER 中的 Inserted 域为 FALSE,则什么也不做,否则将它从系统的定时器链表中移除。
- KeSetTimer/KeSetTimerEx,设定一个定时器。若它已经被插人到了系统的定时器链表中,则首先将它从定时器链表中移除。然后,计算定时器的到期时间,若当前已到调用 KiSignalTimer,以唤醒那些正在等待该定时器的线程,并且以 DPC 方式交付定时器中的到期例程。若当前尚未到期,则设定定时器为无信号状态,并调用KilnsertOrSignalTimer 函数将它插人到系统的定时器链表中。
- KiSignalTimer,定时器对象变为有信号状态。对于定时器通知对象,调用KiWaitTestWithoutSideEffeets 函数唤醒所有的等待线程;对于定时器同步对象,则调用 KiWait TestSynchronizationObject,唤醒一个正在等待该对象的线程。对于周期性的定时器,即 KTIMER 的 Period 域不为0,则再次插入该定时器对象到系统的定时器链表中。最后,若定时器对象关联了一个 DPC 例程,则调用 KelnsertQueueDpc 函数,将其转化成一个真正的 DPC 对象。
- KilnsertOrSignalTimer,如果定时器对象尚未到期,则将它插入到系统的定时器链表中,否则,调用 KiComplete Timer 函数,KiCompleteTimer 进一步调用 KiSignalTimer函数完成定时器对象的到期处理。
- KiTimerExpiration,该函数是系统的定时器到期处理函数,如图 5.9 所示。它遍历特定的定时器链表,对于每个到期的定时器,使之变成有信号状态,并调用KiWaitTestWithoutSideEffects(针对定时器通知对象)或 KiWaitTestSynchronizationObject 函数(针对定时器同步对象)。
定时器对象是-种常用的分发器对象,内核和设备驱动程序都可以使用定时器对象来实现超时和定时处理。Windows 提供的定时器对象非常灵活:既可以通过到期例程来通知使用者,也允许使用者通过等待函数获得通知;既可以是一次性的,也可以是周期性的;既可以是通知类型,也可以是同步类型。值得指出的一点是,定时器对象的精度受限于系统处理器时钟中断的精度,根据 5.2.5 节的介绍,Windows仅在时钟中断或 DISPATCH_ LEVEL软件中断发生时,才会处理定时器链表中的到期定时器对象。
最后,我们注意到,每当由于一个分发器对象导致其他的线程被解除等待时,被唤醒的线程都会获得优先级的提升,不同的分发器对象带给等待线程的优先级提升值可能有所不同,事件、信号量和突变体对象的优先级提升值为1,定时器对象不会提升等待线程的优先级。有关线程的优先级和优先级提升的情形,参见 3.5.1节的介绍。
由于分发器对象的状态变化会直接影响到系统的线程调度,所以,在以上这些分发器对象的操作函数的代码中,我们常常会看到 RQL 提升至 DISPATCHL LEVEL 或 SYNCH LEVEL,并且要锁住调度器数据库,这表明,当一个线程调用这些操作时,它实际上将控制权暂时交给了线程调度器。事实上,系统的线程调度器也正是通过这种方式来工作的,它不仅从代码位置上散布在内核模块各处,而且从运行时间上,也插人在各个线程、中断或异常的执行过程中。绝大多数情况下,进出线程调度器是在同一个函数中完成的,也就是说,如果一个丽数进人了调度器,则该函数在返回以前,还会退出调度器。但也有例外,比如 KeSetEvent、 KeReleaseMutant、 KeReleaseSemaphore 等函数的 Wait 参数就指明了该函数调用以后是否要紧接着调用一个等待函数,若如此,则不退出调度器,而是把这一信息记录在线程对象 KTHREAD 的 WaitNext 和 Waitlrql 成员中,然后在等待函数中再判断线程的状态。
6.4 源码KeUpdateSystemTime
1 | .text:0046DFA4 // =============== S U B R O U T I N E ======================================= |
6.5 ia64版本KeUpdateSystemTime
1 | VOID KeUpdateSystemTime ( |
6.6 ROS:KeUpdateSystemTime
1 | /* |
6.7 KeInitializeTimer
1 | VOID __stdcall KeInitializeTimer ( |
6.8 KeInitializeTimerEx
1 | VOID __stdcall KeInitializeTimerEx ( |
6.9 KeSetTimer
1 | BOOLEAN __stdcall KeSetTimer ( |
6.a KeSetTimerEx
1 | BOOLEAN __stdcall KeSetTimerEx ( |
6.b KiTimerExpiration
1 | VOID __stdcall KiTimerExpiration ( |
7 异常分发
8 线程调度
9 注册表
10 文件系统
11 内核模式回用户模式
Windows内核原理与实现 8.1.2 P550
异常 驱动开发–常规/非常规驱动与3环通信
调试 x86内核CVE:1900、飞哥、0day第二版 内核部分
Windows回调机制