DPC(延迟过程调用)的技术细节

-DPC(延迟过程调用)的细节

NTINSIDER
16卷,1期,122009


延迟过程调用(DPC)是一种Windows常用功能。用途是广泛和多样的,但最常用的是我们通常所说的“ISR完成WindowsTimer底层技术。

  
如果DPC常用,为什么还要写此篇?我们发现,大多数人并不真正了解DPC工作的底层实现细节。并且,事实证明,一个深入的理解,在选择选项创建DPC的调试方案中也至关重要。

简介

本文并不意味着是一个全面的DPC应用文档。它假定读者已经知道DPC是什么,更甚,在驱动中已作应用。如果你不属于这一类,查阅MSDN吧。

此外,ThreadedDPCs,这是一种特殊类型的DPC。可用于WindowsVista和以后,将不包括任何细节。

作为我们讨论的基础,让我们简要回顾一些基本的DPC概念。

DPC
的定义是一种可以请求一个回调到任意线程上下文,在dispatch级别的IRQLDPC对象本身仅是一个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和队列keinsertqueuedpcDPC对象。驱动程序使用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列表实际上是包含在PRCBPRCB是处理器的控制区域的扩展(PCR)。通过观察PCRPCR命令我们能看到的任何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误解,它们是如何被系统处理。

 

你可能感兴趣的:(DPC(延迟过程调用)的技术细节)