Deferred Procedure Call Details(延迟过程调用详解)

来源:互联网 发布:淘宝公积金代缴可靠吗 编辑:程序博客网 时间:2024/05/30 04:47

Deferred Procedure Call Details(延迟过程调用详解)               (2011-02-26 14:03:53)转载▼

标签: dpc 延迟过程调用 软件中断 it 分类: 我翻译滴文章^_^

Deferred Procedure Calls (DPCs) are a commonly used feature of Windows. Their uses are wide and varied, but they are most commonly used for what we typically refer to as "ISR completion" and are the underlying technology of timers in Windows.

 

延迟过程调用是windows系统中一种常见的特征,它们的使用广泛且多样化,但是它们最常见使用地方时我们通常所谓的“ISR完成“和隐含的被使用在系统定时器中

 

If they're so commonly used, then why are we bothering to write an entire article on them? Well, what we've found is that most people don't really understand the underlying implementation details of how DPCs work. And, as it turns out, a solid understanding is important in choosing the options available to you when creating DPCs and is also a life saver in some debug scenarios.

 

如果它们被如此频繁使用,那我们为何要为写一篇关于它们的全部细节的文章而感到麻烦? 好了,进入正题,我们发现大多数人并没有真正理解延迟过程调用的底层细节。你将发现,对它一个完整的理解将有助于你在使用DPC时做出正确选择,甚至在某些调试环境下将成为你的救命稻草

 

Introduction

介绍:

 

This article is not meant to be a comprehensive review of why or how DPCs are used. It is assumed that the reader already knows what a DPC is or, even better, has used them in a driver. If you do not fall into this category, information at that level is readily available on MSDN.

 

这篇文章没有打算完全完全涉及DPCs为什么被使用,如何被使用。现在我们假设读者已经知道DPCs是什么,或者更好点,你已经在你的驱动里使用过它们。如果你还不属于前两者,你可以访问MSDN,这个级别的资料随手可得

 

In addition, Threaded DPCs, which are a special type of DPC available on Windows Vista and later, will not be covered in any detail.

另外,Threaded DPCs(线程DPCs),作为一种特殊的DPCs在vista及更新版本的系统中被使用,我们将不会涉及关于它的任何细节

 

As a basis of our discussion, let's briefly review some basic DPC concepts.

作为我们讨论的基础,让我们简单复习下DPCs的基本概念

 

A working definition of DPCs is that they are a method by which a driver can request a callback to an arbitrary thread context at IRQL DISPATCH_LEVEL. The DPC object itself is nothing more than a data structure with a LIST_ENTRY, a callback pointer, some context for the callback, and a bit of control data:

DPCs实际上定义为:驱动可以在DISPATCH_LEVEL上向任何线程上下文请求一个回调函数。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;

 

 

 

You initialize a DPC Object with KeInitializeDpc and queue the DPC Object with KeInsertQueueDpc. Drivers that use DPCs to perform more extensive work than is appropriate for an Interrupt Service Routine typically use the DPC Object that's embedded in the Device Object, and cause this DPC Object to be queued by calling the function IoRequestDpc (which internally calls KeInsertQueueDpc). Once queued, at some point in the future your DPC routine is invoked from an arbitrary thread context at IRQL DISPATCH_LEVEL.

 

你可以用KeInitializeDpc初始化一个DPC对象,用KeInsertQueueDpc排队一个DPC对象。驱动使用DPCs可以完成广泛的工作,其中就包括ISR(中断服务例程)使用设备对象中的DPC对象。假设你的一个DPC对象已经调用IoRequestDpc(内部调用KeInsertQueueDpc)排队,一旦排队成功,在接下来的某个时间点你的DPC例程将会在DISPATCH_LEVEL上且在任意线程上下文中被调用

 

With that basic info in hand, we can now cover the gory details of both the queuing and delivery mechanisms that are used for DPCs. That will lead us to discussing what options we have for controlling the behavior of DPCs and what impact those options have.

 

掌握了基本信息后,我们现在可以涉及关于DPC排队和投送机制的底层细节了。我们将不得不讨论控制DPCs的行为的可选方法以及这些方法对它有什么影响

 

DPC Queuing

DPC队列

 

As mentioned previously, DPCs are queued (directly or indirectly) via the KeInsertQueueDpc DDI:

前面提到,DPCs是通过KeInsertQueueDpc排队的

 

NTKERNELAPI

BOOLEAN

KeInsertQueueDpc (

    __inout PRKDPC Dpc,

    __in_opt PVOID SystemArgument1,

    __in_opt PVOID SystemArgument2

    );

 

DPCs are actually queued to a particular processor, which is accomplished by linking the DPC Object into the DPC List that?s located in the Processor Control Block (PRCB) of the target processor. Determining the processor to which the DPC Object is queued is fairly easy for the O/S. By default, the DPC is queued to the processor from which KeInsertQueueDpc is called (the "current processor"). However, a driver writer can indicate that a given processor be used for a particular DPC object, using the function KeSetTargetProcessorDpc.

 

DPCs实际上是在特定的处理器上排队的,并且插入目标处理器的处理器控制域(PRCB-Processor Control Block)的DPC队列中。操作系统确定一个DPC对象在哪个处理器上排队是非常容易的,默认情况下,DPC被排队到调用KeInsertQueueDpc例程的处理器(被称为“当前处理器”)中,然而驱动开发者可以使用KeSetTargetProcessorDpc为特殊的DPC对象指定处理器

 

Viewing the DPC List on a particular processor is easy using WinDBG. While the DPC List is actually contained within the PRCB, the PRCB is an extension of the Processor Control Region (PCR). By viewing the PCR with the !pcr command we can see any DPCs currently on the queue for that processor:

 

使用windbg可以轻松查看一个处理器的DPC队列,然而DPC队列实际上包含在PRCB中,PRCB是一个处理器域的扩展。可以在windbg中使用 !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

 

One aspect of DPCs to note is that once a DPC Object has been queued to a processor, subsequent attempts to queue the same DPC Object are ignored until the DPC Object has been dequeued (by Windows for execution of its callback). This is what the BOOLEAN return value of KeInsertQueueDpc indicates: TRUE means that Windows queued the DPC to the target processor and FALSE means that the DPC Object is already queued to some processor. This makes sense from a programming perspective, as the DPC data structure only has a single LIST_ENTRY field and thus can only appear on a single queue at a time.

 

关于DPCs需要注意的一点就是一旦一个DPC对象被排队到一个处理器,其后插入的相同的DPC对象都将被忽略直到这个DPC对象被抽出(通过系统调用其回调函数)。这也是KeInsertQueueDpc的boolean型返回值表明的意义:TRUE意味着系统已成功排队一个DPC到目标CPU,FALSE意味着DPC对象已经在一些处理器的DPC链中。从编程的角度来看,因为DPC对象的数据结构只有一个单链表,这样一来在某个时间点上它只能出现一条队列中

 

 

What About Priority?

什么是优先级

 

Where the DPC is placed on the target processor?s DPC List is an interesting question. Whether a DPC Object is inserted at the beggining or end of the target processor?s DPC List is one aspect of the priority feature of DPCs. You can set the importance of a given DPC Object by using the function KeSetImportanceDpc. This DDI lets you indicate that the DPC object is low, medium, or high importance. Also, in Vista and later you can set the importance to "medium high." Low, medium, and medium high importance DPCs are placed at the end of the DPC queue, while high importance DPCs are placed at the front of the queue. You may ask yourself at this point, "then what's the difference between low, medium, and medium high?" We'll answer that question shortly.

DPC被放到目标CPU的哪里?DPC队列是个有趣的问题,是不是DPC对象被插入到目标CPU的DPC队列的头部或者尾部?DPC队列是DPC对象优先级特征的一种表现。你可以用KeSetImportanceDpc设置一个给定的DPC对象的优先级,这个设备驱动程序接口(DDI=Device-Driver Interface)可以让你设置DPC的优先级为低优先级,中等优先级,高优先级。而且在vista及其以后的系统,你还可以设置优先级为“中高级” 。低,中等和中高的DPC对象被放在DPC队列尾部,然而高优先级的DPC对象被放在队列头部。在这点上你可能会问自己“低,中,中高优先级的DPC对象有什么区别?”。我将简单的回答这个问题

 

The DISPATCH_LEVEL Software Interrupt

DISPATCH_LEVEL 软件中断

 

Once the DPC has been queued to the target processor, a DISPATCH_LEVEL software interrupt is typically generated on the processor. The choice of whether or not to request the DISPATCH_LEVEL software interrupt when the DPC Object is queued is largely based on four factors: the importance of the DPC, the target processor of the DPC, the depth of the DPC List on the target processor, and "drain rate" of the DPC List on the target processor.

一旦DPC对象被成功插入到目标CPU,这个CPU上一般将生成一个DISPATCH_LEVEL的软件中断(译注:不是CPU生成,别搞混了,事实上是系统请求一个DISPATCH_LEVEL软件中断)。对于是否产生一个DISPATCH_LEVEL的软中断很大部分取决于以下四种因素:DPC对象的优先级,DPC对象指定的目标CPU,目标CPU的DPC队列百分比和目标CPU的DPC队列的“抽空比例”(译注:抽空比例就是一个阀值,表示DPC队列抽空动作的一个触发条件)

 

If the target processor for the DPC Object is the current processor, the DISPATCH_LEVEL software interrupt is requested if the DPC Object is of any importance other than low. For low importance DPCs, the software interrupt is only requested if the O/S believes that the processor is not servicing DPCs fast enough, either because the DPC queue has become large or is not draining at a sufficiently fast rate. If either of those are true, the interrupt is requested even if the DPC is low importance.

如果DPC对象的目标CPU是当前处理器(译注:前面有提及),只要DPC对象优先级大于low,系统都将请求一个DISPATCH_LEVEL软中断,对于低优先级,只有当系统确定CPU处理DPC队列不够快时才会触发DISPATCH_LEVEL软件中断,根本原因是DPC队列已经足够大或者没有以一个足够快的速度在抽空。如果满足这些条件中的任意一个,即是DPC对象是低优先级,系统都将请求一个软件中断

 

 

If the target processor for the DPC Object is not the current processor, the decision process is different. Because requesting an interrupt on the other processor will involve a costly Inter Processor Interrupt (IPI), the situations under which it is requested are restricted. Prior to Vista, the IPI request would only be made if the DPC was high importance or if the DPC queue on the target processor had become too deep. Vista added the medium high importance DPCs to the check and went one step further to cut down the number of IPIs by requiring the target processor to be idle for the DISPATCH_LEVEL software interrupt to be requested (See Table 1 for a high-level breakdown).

如果DPC对象的目标CPU不是当前处理器,决定(是否产生一个DISPATCH_LEVEL软件中断的)过程将有所不同。因为请求一个中断将陷入一个代价很高的处理器间中断(IPI),此时CPU能被请求的情况是受限的。vista之前,IPI请求只能在DPC对象为高优先级或者DPC对象的目标CPU的DPC队列变得很臃肿的情况下发生。vista系统添加了中高优先级的DPC,是为了通过请求目标CPU切换为空闲状态以便DISPATCH_LEVEL软件中断得以被请求,如此就可以检查和进一步减少目标CPU的的IPIs的请求数量(见表1)

 

表 1

 

 

 

DPC Delivery

DPC投送

 

Once the DPC has been queued to the processor, at some point it must be dequeued and the callback executed. Remember that there were two scenarios that occurred after the DPC was queued to the processor, either the DISPATCH_LEVEL software interrupt was requested or it was not.

一旦一个DPC被成功排队到某个处理器,在某些时间点一定会被(从队列中)抽出并且执行其回调函数,记住DPC被排队到处理器的DPC队列之后有两种可能情况发生,产生DISPATCH_LEVEL软件中断或者不产生(译注:我无语了 = =)

 

Delivery from the Software Interrupt Service Routine

从中断服务例程中投递DPC

 

To keep things relatively simple, we?ll restrict our discussion here to the case of queuing the DPC Object to the current processor. Let's start with the case in which the IRQL DISPATCH_LEVEL software interrupt was requested. At the time KeInsertQueueDpc was called, there are two situations the system could be in: The first would be running at an IRQL < DISPATCH_LEVEL, in which case the DISPATCH_LEVEL interrupt would be delivered immediately. The second case would be if the current processor is at IRQL >= DISPATCH_LEVEL, in which case the interrupt would remain pending until the IRQL was about to return to an IRQL < DISPATCH_LEVEL.

为了保持这些概念之间的简单关系,我们限制我们的讨论范围仅仅限制为排队DPC对象到当前处理器这种情形。我们从DISPATCH_LEVEL的软件中断被请求的情形开始讨论。当KeInsertQueueDpc被调用,系统可能出于两种情况:第一种系统正运行在小于DISPATCH_LEVEL的中断级上,这种情况下,DISPATCH_LEVEL请求将会立即被处理。另一种情况是当前处理器中断级大于等于DISPATCH_LEVEL,这种情况下DISPATCH_LEVEL的请求中断将会被保留且排队,直到处理器降回到小于DISPATCH_LEVEL

 

In either case, once the service routine for the DISPATCH_LEVEL interrupt begins executing, it checks to see if any DPCs are queued to the current processor. If the DPC queue is not empty, Windows will loop and entirely drain the DPC List before returning from the service routine.

在任意一种情况下,一旦DISPATCH_LEVEL的服务例程被执行,它将检查是否有DPC在当前处理器排队。如果DPC队列不为空,系统将在服务例程返回前,循环直到彻底抽空DPC队列

 

Before draining the DPC List, Windows wants to ensure that it has a fresh execution stack for the DPC routines to run on. This will presumably cut down the incidents of stack overflows in the case where the current stack does not have much space remaining. Thus, every PRCB also contains a pointer to a previously allocated DPC stack that Windows switches to before calling any DPCs:

抽空DPC队列之前,系统需要确保为每个DPC例程的执行分配的新的堆栈。这大概是为了减少因为空间不足而导致堆栈溢出的可能性,因此,每个处理器控制域也也包含一个指向上一个DPC例程的堆栈,这个指针会被系统在执行任何一个DPC例程之前改变

 

0: kd> dt nt!_KPRCB DpcStack

   +0x868 DpcStack : Ptr32 Void

 

 

 

We can see evidence of the switch in the debugger if we set a breakpoint in a DPC routine. Here we chose a DPC from the ATAPI driver:

我们可以通过调试器对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 80544e5f atapi!IdePortCompletionDpc

f9dc7ff4 805449cb nt!KiRetireDpcList+0x61

f9dc7ff8 f9a2b9e0 nt!KiDispatchInterrupt+0x2b

WARNING: Frame IP not in any known module. Following frames may be wrong.

805449cb 00000000 0xf9a2b9e0

 

 

 

Notice the strange call stack - it seems to disappear after the call to KiDispatchInterrupt. The problem is that WinDBG has ceased to be able to unwind the stack due to the stack switch, and the call stack that we see here is the call stack for the DPC stack. If we try to match the EBP addresses shown with the stack limits of the current stack we will see the discrepancy:

注意这个奇怪的调用堆栈-它貌似在调用KiDispatchIntterupt后消失了,对于这个问题是因为由于堆栈改变导致windbg已经不能展开堆栈了。如果我们尝试去匹配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

Owning Process            818d5978       Image:         csrss.exe

Attached Process          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 Call 0

 

Note that the EBP addresses do not fall within the base and limit of the current thread's stack. Using the techniques outlined in last issue's Debugging Techniques: Take One...Give One article (September -

注意EBP的地址没有落入当前线程栈的的起始地址加堆栈大小的这段空间里,通过《Debugging Techniques》的技术概览:Take one...Give One article(译注:继续无语= =)

 

Delivery from the Idle Thread

从空闲线程投递

 

But what about those low importance DPCs or targeted DPCs that didn't request the DISPATCH_LEVEL software interrupt? Who processes those? Well, there are actually two ways in which they'll be processed. Either another DPC will come along that will request the DISPATCH_LEVEL interrupt and the DPC will be picked up on the subsequent drain, or the idle loop will come along and notice that the DPC queue is not empty.

但是如果对于那些不请求DISPATCH_LEVEL软件中断的低优先级的DPC或者目标DPC怎么办?谁来处理?好吧,事实上有可以通过两种途径处理它们。一是:系统将产生一个DPC并请求DISPATCH_LEVEL软件中断,并且随后将这个DPC从队列中踢掉。二是:系统将产生空闲循环和DPC队列不为空的消息

 

Part of the idle loop's work is to check the DPC queue and determine if it is empty or not. If it finds that the queue is not empty, it begins draining the queue by dequeuing entries and calling the callbacks. We can see this in a different call stack but using the same DPC routine as the previous example:

空循环的一部分工作是检查DPC队列并确定是否为空,如果发现它不为空,它将通过取出每个DPC并且调用DPC的回调函数来抽空DPC队列。我们可以看出这个使用和前一个例子相同的DPC例程却有着不同的调用堆栈

 

Breakpoint 1 hit

atapi!IdePortCompletionDpc:

f9806990 8bff            mov     edi,edi

0: kd> k

ChildEBP RetAddr 

80550428 80544e5f atapi!IdePortCompletionDpc

80550450 80544d44 nt!KiRetireDpcList+0x61

80550454 00000000 nt!KiIdleLoop+0x28

 

The difference here is that in this case the stack is not switched, thus the DPCs actually executed on the idle thread's stack. Because the idle loop uses so little thread stack itself, there is not much use in going through the effort of swapping stacks in this case.

不同之处在于此处的堆栈没有转换,如此一来DPC变确实执行在空线程的堆栈中。因为空循环本身只是用线程栈的很小一部分,这种情况下,转换堆栈并没有多少有益之处

 

Conclusion

结论

 

Hopefully this cleared up a few misconceptions about DPCs and how they are handled by the system.

希望这篇文章可以理清一些关于DPC和系统如何处理它们的错误概念(译注:I wanna  achieve this goal too,but....)