-DPC(延迟过程调用)的细节
NTINSIDER,16卷,1期,1至2月2009
延迟过程调用(DPC)是一种Windows常用功能。用途是广泛和多样的,但最常用的是我们通常所说的“ISR完成”和WindowsTimer底层技术。
如果DPC常用,为什么还要写此篇?我们发现,大多数人并不真正了解DPC工作的底层实现细节。并且,事实证明,一个深入的理解,在选择选项创建DPC的调试方案中也至关重要。
简介
本文并不意味着是一个全面的DPC应用文档。它假定读者已经知道DPC是什么,更甚,在驱动中已作应用。如果你不属于这一类,查阅MSDN吧。
此外,ThreadedDPCs,这是一种特殊类型的DPC。可用于WindowsVista和以后,将不包括任何细节。
作为我们讨论的基础,让我们简要回顾一些基本的DPC概念。
DPC的定义是一种可以请求一个回调到任意线程上下文,在dispatch级别的IRQL。DPC对象本身仅是一个list_entry数据结构,一个回调指针,一些回调上下文,和一位的控制数据:
typedef struct _KDPC {
UCHAR Type;
UCHAR Importance;
USHORT Number;
LIST_ENTRY DpcListEntry;
PKDEFERRED_ROUTINE DeferredRoutine;
PVOID DeferredContext;
PVOID SystemArgument1;
PVOID SystemArgument2;
__volatile PVOID DpcData;
} KDPC, *PKDPC, *PRKDPC;
你来初始化一个DPC对象采用keinitializedpc和队列keinsertqueuedpc此DPC对象。驱动程序使用DPC进行更多的工作比是适当的一个中断服务程序,DPC的对象嵌入在设备对象。使DPC对象被排队通过调用函数IoRequestDpc(在内部调用keinsertqueuedpc)。一旦排到队列,在将来的某个时候你的DPC例程将被任意线程上下文IRQL级别 dispatch_level调用。
有了这些基本的信息,我们现在可以覆盖DPC排队列细节都和传输机制。这将导致我们讨论如何控制DPCs行为,这些选项有什么影响。
DPC队列
如前所述,DPCs排列(直接或间接)通过KeInsertQueueDpc DDI:
NTKERNELAPI
BOOLEAN
keinsertqueuedpc(
__inout prkdpc DPC,
__in_opt PVOID systemargument1,
__in_opt PVOID systemargument2
);
DPCs实际上是排队到一个特定的处理器,这是通过将DPC对象为DPC连接到DPC队列,位于目标处理器控制块(PRCB)。对OS而言确定该DPC对象队列,对处理器是相当容易的。默认情况下,DPC是排队列到处理器,KeInsertQueueDpc被调用(“当前处理器”)。然而,驱动可以指示一个给定的处理器被用于一个特定的DPC对象,使用例程KesetTargetProcessorDpc。
在一个特定的处理器用WinDbg查看DPC列表很容易。当DPC列表实际上是包含在PRCB,PRCB是处理器的控制区域的扩展(PCR)。通过观察PCR!PCR命令我们能看到的任何DPC目前处理器的队列:
0:kd> !pcr 0
KPCR for Processor 0 at ffdff000:
Major 1 Minor 1
NtTib.ExceptionList: 8054f624
NtTib.StackBase: 805504f0
NtTib.StackLimit: 8054d700
NtTib.SubSystemTib: 00000000
NtTib.Version: 00000000
NtTib.UserPointer: 00000000
NtTib.SelfTib: 00000000
SelfPcr: ffdff000
Prcb: ffdff120
Irql: 00000000
IRR: 00000000
IDR: ffffffff
InterruptMode: 00000000
IDT: 8003f400
GDT: 8003f000
TSS: 80042000
CurrentThread: 8055ae40
NextThread: 81bc0a90
IdleThread: 8055ae40
DpcQueue: 0x8055b4a0 0x805015ae [Normal]nt!KiTimerExpiration
0x81b690a4 0xf9806990 [Normal] atapi!IdePortCompletionDpc
0x818a12cc 0xf96c5ee0 [Normal] NDIS!ndisMDpcX
要注意的是,一旦一个DPC对象已排队列到一个处理器,再试图对此DPC排队列被忽略,直到DPC对象已出队(Windows的回调函数的执行)。这就是keinsertqueuedpc返回的布尔值的意义。
TRUE意味着Windows已排队此DPC到目标处理器,FALSE意味DPC对象已排队到另一些处理器。从编程的角度来看,作为数据结构DPC只有一个LIST_ENTRY域,因此一次只能出现在一个单一的队列。
关于优先权?
在DPC在那里被放置于目标处理器? DPC List是一个有趣的问题。一个DPC对象插入到目标处理器的开始或结束? DPC List优先功能的一个方面。你可以设置一个给定的DPC对象重要性,使用函数kesetimportancedpc。这个DDI让你表明DPC对象是低,中,或高重要性。同时,在Vista中,以后你可以设置的重要性“中高”。低,中,中高重要性和被放置在DPC队列的末尾,而高的重要性,被放置在队列的前面。在这一点上,问你自己,“在低,中和中高重要性有什么区别?”,我们会很快回答这个问题。
Dispatch_level软件中断
一旦DPC排队到目标处理器,一个dispatch_level软件中断通常在处理器生成的。选择是否请求dispatch_level软件中断时,DPC对象是排队在很大程度上是基于四个因素:重要性的DPC,DPC处理器的目标,在目标处理器的DPC列表的深度,和在目标处理器的DPC列表“流失率”。
如果DPC对象的目标处理器是目前的处理器,该dispatch_level软件中断请求发生,如果DPC对象是除了low之外的重要性。对low重要性DPC,软件中断发生,如果O / S认为处理器没有服务DPCs足够快,要么因为DPC队列已成为大或不以足够快的速度。如果这些都是真的,中断请求即使DPC是low重要性。
如果对DPC的目标处理器不是当前的处理器,其决策过程是不同的。因为请求中断的处理器将涉及昂贵的处理器间中断(IPI)的情况下,它是所要求的限制。在Vista中,IPI请求只会如果DPC是高重要性或如果在目标处理器的DPC队列已变得太深。Vista添加中高重要性DPCs检查和进一步降低IPIS数量要求目标处理器被闲置的dispatch_level软件中断请求(见一个高层次的分解表1)。
DPC传递:
一旦DPC已排队到处理器,在某种程度上它必须出队和回调执行。记住,有两种情况,在DPC排队到处理器的出现,无论是否dispatch_level软件中断。
从软件断服务程序传递
为了让事情简单,我们将限制讨论,排队DPC对象到当前的处理器的情况下。从IRQL级别 dispatch_level软件中断请求开始。当时keinsertqueuedpc调用,有两种情况下的系统可能是:首先将运行在一个IRQL小于dispatch_level,在这种情况下,dispatch_level中断会立即交付。第二个案例将如果当前处理器的IRQL > = dispatch_level,在这种情况下,中断将保持等待直到IRQL即将回到
在任一情况下,曾经为dispatch_level服务程序中断开始执行,它会检查是否有排队和当前处理器。如果DPC队列非空,Windows将在从服务例程返回循环和完全运行DPC列表流。
在排放前DPC列表,要确保它的DPC例程运行在一个新的执行堆栈。这可能会降低发生堆栈溢出的情况下,当前堆栈没有多少剩余空间。因此,每一个PRCB还包含一个指针指向先前分配的DPC堆栈,Windows切换到在调用任何DPCs之前:
0: kd> dtnt!_KPRCB DpcStack
+0x868 DpcStack : Ptr32 Void
如果我们在一个DPC程序设置断点,将看到调试器中的开关的证据。在这里,我们选择了从ATAPI驱动一个DPC:
0: kd> bp atapi!IdePortCompletionDpc
0: kd> g
Breakpoint 1 hit
atapi!IdePortCompletionDpc:
f9806990 8bff mov edi,edi
0: kd> k
ChildEBP RetAddr
f9dc7fcc 80544e5fatapi!IdePortCompletionDpc
f9dc7ff4 805449cbnt!KiRetireDpcList+0x61
f9dc7ff8 f9a2b9e0nt!KiDispatchInterrupt+0x2b
WARNING: Frame IP not in any known module. Following frames may be wrong.
805449cb 00000000 0xf9a2b9e0
注意到奇怪的调用堆栈,似乎在KiDispatchInterrupt调用之后消失。问题是,WinDBG已不再能够清晰显示调用堆栈,由于堆栈交换。我们在这里看到的是DPC堆栈的调用堆栈。如果我们尝试匹配的EBP地址显示与当前堆栈限制,我们将看到差异:
0: kd> !thread
THREAD 81964770 Cid 028c.02b8 Teb:7ffd8000 Win32Thread: e1873008 RUNNING on processor 0
IRP List:
8195b870: (0006,0190) Flags:00000970 Mdl: 00000000
819128b0: (0006,0190) Flags:00000970 Mdl: 00000000
Not impersonating
DeviceMap e1001980
OwningProcess 818d5978 Image: csrss.exe
AttachedProcess N/A Image: N/A
Wait Start TickCount 6779 Ticks: 0
Context Switch Count 4104 LargeStack
UserTime 00:00:00.000
KernelTime 00:00:00.265
Start Address 0x75b67cd7
Stack Init f9a2c000 Current f9a2ba58 Base f9a2c000 Limit f9a29000 Call0
从空闲(Idle)线程传递:但对于那些低重要性和或有目标DPC不要求dispatch_level软件中断?谁处理这些?嗯,实际上有两种方式,他们会处理的。另一个DPC会沿着这将要求dispatch_level中断,DPC将被随后的流水队列拾起,或空闲循环会出现,请注意,DPC队列是非空的。
部分空闲循环的工作是检查DPC队列和确定它是否为空。如果发现队列不是空的,它开始流水队列采用出队头并调用回调函数。我们可以在不同的调用堆栈看到这些。但使用前例相同的DPC程序:
Breakpoint 1 hit
atapi!IdePortCompletionDpc:
f98069908bff mov edi,edi
0:kd> k
ChildEBPRetAddr
8055042880544e5f atapi!IdePortCompletionDpc
8055045080544d44 nt!KiRetireDpcList+0x61
8055045400000000 nt!KiIdleLoop+0x28
Idle 循环自己使用很少的线程堆栈,未用到很多地俄Swappingstacks。
结论:希望澄清了一些DPC误解,它们是如何被系统处理。