戏说IRQL(3)
在这个信息爆炸的时代,为何还要看书呢?举个例子吧,前几日翻看案头的旧书时看到这么一句话:
IRQL
avoids destructive preemption on a single CPU, while spin locks forestall
interference among CPUs.
(IRQL用于避免在单个CPU上发生有害的抢占,而自旋锁可以阻止多个CPU的相互干扰。)
多么精炼的概括啊,语出Walter Oney的Programming
WDM,这是几乎所有写Windows驱动程序的人都看的一本书。
书接上回,继续讲IRQL中的1,2两个级别,也就是APC和DPC。它们的全称分别是异步过程调用(asynchronous procedure call)和延迟过程调用(deferred procedure call)。 两个名字中都包含了PC(过程调用),这代表了它们的一个共同特征,那就是做一种特殊的函数调用。进一步说,就是先把要调用的函数和参数插入到一个队列中,等待时机成熟时再执行。
以DPC为例,通常是硬件设备的中断处理函数在较高的IRQL级别调用KeInsertQueueDpc增加一个DPC调用,然后就宣布中断处理结束让IRQL降下来,防止CPU在高IRQL上停留时间太长,没有机会处理低IRQL的中断。
APC的情况更复杂些,可以在内核态注册APC调用,也可以在用户态注册,比如调用下面的API就可以在应用程序中增加一个APC调用:
DWORD WINAPI QueueUserAPC(
_In_ PAPCFUNC pfnAPC,
_In_ HANDLE hThread,
_In_ ULONG_PTR dwData
);
除了APC可以在用户态增加外,DPC和APC的另一个区别就是队列的组织方式有很大不同。DPC队列是CPU相关的,每个CPU有一个DPC队列,比如,下面是一个单CPU的系统发生系统挂死时,系统中唯一一个CPU的DPC队列:
DpcQueue: 0x80552380 0x804ff5b8 [Normal] nt!KiTimerExpiration
0x8abcfed4 0xba7843e8 [Normal] ACPI!ACPIInterruptServiceRoutineDPC
0x8a6560cc 0xba50fdf0 [Normal] NDIS!ndisMDpcX
0x8a899eec 0xbaa28650 [Normal] i8042prt!I8042KeyboardIsrDpc
0x8a899e90 0xbaa2b0ef [Normal] i8042prt!I8xKeyboardSysButtonEventDpc
0x8a899dd0 0xbaa2a163 [Normal] i8042prt!I8042ErrorLogDpc
0xb87ffa4c 0xb87e8020 [Normal] SynTP
0xb87ffa24 0xb87e7f60 [Normal] SynTP
进一步讲,描述DPC队列的信息记录在每个CPU的处理器控制块上:
kd> dt _KPRCB ffdff120 -y DPC
nt!_KPRCB
+0x4b0 DpcTime : 0x4f40
+0x860 DpcListHead : _LIST_ENTRY [ 0x80552384 - 0xb87ffa28 ]
+0x868 DpcStack : 0xbacd8000 Void
+0x86c DpcCount : 0x256ae9
+0x870 DpcQueueDepth : 8
+0x874 DpcRoutineActive : 0x80549244
+0x878 DpcInterruptRequested : 0
+0x87c DpcLastCount : 0x256ae9
+0x880 DpcRequestRate : 0
+0x8a0 DpcLock : 0
而APC队列是线程相关的,也就是每个线程都可以有一个APC队列。比如,执行扩展命令!apc便可以逐一检查每个进程的每个线程的APC队列,如果发现有APC的话,就会列出来:
kd> !apc
*** Enumerating APCs in all processes
Process 8abf2490 System
...
Process 897a4020 ICSRV.EXE
Process 897a3da0 inetinfo.exe
Thread 883feb80 ApcStateIndex 0 ApcListHead 883febbc [USER]
KAPC @ 896edeb0
Type 12
KernelRoutine 80573206 nt!IopUserCompletion+0
RundownRoutine 80573220 nt!IopUserRundown+0
...
APC的记录方式与DPC也有所不同,因为APC有来自内核态的,又有来自用户态的,所以在KTHREAD中,有个KAPC_STATE结构,然后在这个结构体里有两个队列,一个是用户态的,一个是内核态的。
kd> dt _KAPC_STATE
nt!_KAPC_STATE
+0x000 ApcListHead : [2] _LIST_ENTRY
+0x010 Process : Ptr32 _KPROCESS
+0x014 KernelApcInProgress : UChar
+0x015 KernelApcPending : UChar
+0x016 UserApcPending : UCharv
观察上面的iis进程:
kd> dt _KAPC_STATE 883feb80+34
nt!_KAPC_STATE
+0x000 ApcListHead : [2] _LIST_ENTRY [ 0x883febb4 - 0x883febb4 ]
+0x010 Process : 0x897a3da0 _KPROCESS
+0x014 KernelApcInProgress : 0 ''
+0x015 KernelApcPending : 0 ''
+0x016 UserApcPending : 0 ''
此外,KTHREAD结构还有一些与APC右关的字段:
kd> dt _KTHREAD -y APC 883feb80 -r
nt!_KTHREAD
+0x034 ApcState : _KAPC_STATE
+0x0e8 ApcQueueLock : 0
+0x138 ApcStatePointer : [2] 0x883febb4 _KAPC_STATE
+0x165 ApcStateIndex : 0 ''
+0x166 ApcQueueable : 0x1 ''
观察了这些数据结构之后,我们应该可以对DPC和APC的“藏身之处”有了个比较深刻的印象。接下来的问题就是如何执行这些保存在DPC队列或者APC队列中的过程呢?简单说是通过所谓的软中断机制,但这个软中断不是像int 13那样基于IDT表的,而是基于一张纯软件的表,位于HAL模块中的SWInterruptHandlerTable全局变量记录着这样表:
kd>
dds hal!SWInterruptHandlerTable
8301a0a4 8301310e hal!KiUnexpectedInterrupt
8301a0a8 83018510 hal!HalpApcInterrupt
8301a0ac 83018374 hal!HalpDispatchInterrupt
8301a0b0 8301310e hal!KiUnexpectedInterrupt
8301a0b4 830185aa hal!HalpApcInterrupt2ndEntry
8301a0b8 8301840e hal!HalpDispatchInterrupt2ndEntry
简单点说,在增加DPC或者APC时,增加函数(KiInsertQueueApcv)会请求软件中断:
#if defined(NT_UP)
#define KiRequestApcInterrupt(Processor) KiRequestSoftwareInterrupt(APC_LEVEL)
#else
#define KiRequestApcInterrupt(Processor) \
if (KeGetCurrentProcessorNumber() == Processor) { \
KiRequestSoftwareInterrupt(APC_LEVEL); \
} else { \
KiSendSoftwareInterrupt(AFFINITY_MASK(Processor), APC_LEVEL); \
}
#endif
#if defined(NT_UP)
#define KiRequestDispatchInterrupt(Processor)
#else
#define KiRequestDispatchInterrupt(Processor) \
if (KeGetCurrentProcessorNumber() != Processor) { \
KiSendSoftwareInterrupt(AFFINITY_MASK(Processor), DISPATCH_LEVEL); \
}
#endif
当IRQL降至DPC或者APC时,CPU就会检查是否有DPC/APC需要执行,详细过程,下一讲通过实验继续讲。