DPC在功能上可以理解为ISR(Interrupt Service Routine)的一部分。只是因为ISR为了尽量简单和返回控制权给操作系统,而将一部分功能剥离出来放入相应DPC中,延迟调用。因为DPC的IRQL仅在APC和Passive中断之上,所以系统可以从容地处理完高级别的中断后,再在DPC一级慢慢处理积累起来的相对并不那么紧急功能。
DPC在使用上可以理解为一个回调函数的封装对象。系统本身或者设备驱动程序,在合适的地方如设备驱动程序的AddDevice函数或DispatchPnP函数处理IRP_MN_START_DEVICE请求时,初始化一个DPC对象;在ISR中判断是否需要进一步处理中断,是则请求将DPC对象插入到系统DPC队列中;系统处理完高IRQL后,会在IRQL DISPATCH_LEVEL级别慢慢处理DPC队列中的DPC对象;每个DPC对象封装的回调函数,会使用同时封装的调用参数,被系统调用,完成在ISR中来不及完成的工作;如果需要进一步的工作,还可以继续请求插入DPC对象到DPC队列中。
DPC对象从最终用户角度有两种:DpcForIsr和CustomDPC。前者是与设备驱动对象(Device Object)绑定的;后者则由驱动自行维护。但从实现上来说,只有一种DPC对象存在,DpcForIsr所涉及的维护函数,实际上都是对CustomDPC的一个封装而已。
我们首先来看看初始化DPC对象的实现。KeInitializeDpc函数(ntoskedpcobj.c:39)完成具体的DPC对象的初始化,实际上就是填充一个内存结构KDPC(ntosinc tosdef.h:331)。
以下为引用:
//
// Deferred Procedure Call (DPC) object
//typedef struct _KDPC {
CSHORT Type;
UCHAR Number;
UCHAR Importance;
LIST_ENTRY DpcListEntry;
PKDEFERRED_ROUTINE DeferredRoutine;
PVOID DeferredContext;
PVOID SystemArgument1;
PVOID SystemArgument2;
PULONG_PTR Lock;
} KDPC, *PKDPC, *RESTRICTED_POINTER PRKDPC;
了解了KDPC对象的结构,实际上维护代码就非常简单了。KeInitializeDpc函数将KDPC对象结构初始化为初值;IoInitializeDpcRequest函数则只是对KeInitializeDpc函数的一个简单包装,如下
以下为引用:
#define IoInitializeDpcRequest( DeviceObject, DpcRoutine ) (
KeInitializeDpc( &(DeviceObject)->Dpc,
(PKDEFERRED_ROUTINE) (DpcRoutine),
(DeviceObject) ) )
KeInsertQueueDpc函数(ntoskedpcobj.c:89)实际上是系统对DPC队列维护的核心函数,其伪代码如下:
以下为引用:
BOOLEAN KeInsertQueueDpc (IN PRKDPC Dpc, IN PVOID SystemArgument1,IN PVOID SystemArgument2)
{
PKSPIN_LOCK Lock;
KIRQL OldIrql;KeRaiseIrql(HIGH_LEVEL, &OldIrql); // 提升当前IRQL到最高,屏蔽其它中断
PKPRCB = KeGetCurrentPrcb(); // 获取当前处理器控制块
// 通过比较Dpc->Lock是否为空,来判断此DPC对象是否已经被加入到DPC队列;
// 如果DPC对象可以被加入到队列,则将当前处理器控制块的DPC自旋锁复制到Dpc->Lock中
if ((Lock = InterlockedCompareExchangePointer(&Dpc->Lock, &Prcb->DpcLock, NULL)) == NULL)
{
// 更新当前处理器控制块的统计信息
Prcb->DpcCount += 1;
Prcb->DpcQueueDepth += 1;// 更新DPC对象的参数信息
Dpc->SystemArgument1 = SystemArgument1;
Dpc->SystemArgument2 = SystemArgument2;// 根据DPC对象优先级,决定将之加入到DPC队列的头部或尾部
if (Dpc->Importance == HighImportance)
InsertHeadList(&Prcb->DpcListHead, &Dpc->DpcListEntry);
else
InsertTailList(&Prcb->DpcListHead, &Dpc->DpcListEntry);// 如果当前处理器没有DPC对象活动或DPC中断请求,则进一步判断是否发出DPC中断请求
if (Prcb->DpcRoutineActive == FALSE && Prcb->DpcInterruptRequested == FALSE)
{
// 如果DPC对象优先级为中高;
// 或者DPC队列长度超过阈值MaximumDpcQueueDepth;
// 或者DPC请求速率小于阈值MinimumDpcRate
if ((Dpc->Importance != LowImportance) ||
(Prcb->DpcQueueDepth >= Prcb->MaximumDpcQueueDepth) ||
(Prcb->DpcRequestRate < Prcb->MinimumDpcRate))
{
// 满足触发条件,则发出DPC中断请求
Prcb->DpcInterruptRequested = TRUE;
KiRequestSoftwareInterrupt(DISPATCH_LEVEL);
}
}
}
KeLowerIrql(OldIrql);
return (Lock == NULL);
}
而处理与驱动绑定的DPC对象的IoRequestDpc函数只是KeInsertQueueDpc函数的一个简单包装。
以下为引用:
#define IoRequestDpc( DeviceObject, Irp, Context ) (
KeInsertQueueDpc( &(DeviceObject)->Dpc, (Irp), (Context) ) )
最后对DPC对象属性进行修改的KeSetImportanceDpc函数(ntoskedpcobj.c:367)和KeSetTargetProcessorDpc函数(ntoskedpcobj.c:401)实际上都是直接修改DPC对象结构的相应域。KDPC::Number大于MAXIMUM_PROCESSORS = 32时,用于指定DPC对象的目标CPU。如调用KeSetTargetProcessorDpc(pKDpc, 2)后,pKDpc = MAXIMUM_PROCESSORS + 2。
在了解了DPC对象和DPC队列的大致维护函数功能后,我们来看看稍微复杂一些的在多处理器下DPC队列的维护流程。
前面提到KDPC::Number指定了DPC对象所用的处理器号,因此在KeInsertQueueDpc函数开始获取处理器控制块时,需要判断Number是否指向一个处理器,并从全局处理器控制块列表中获取相应的处理器控制块,为代码如下:
以下为引用:
if (Dpc->Number >= MAXIMUM_PROCESSORS) // Number大于MAXIMUM_PROCESSORS时用于指定处理器
{
Processor = Dpc->Number - MAXIMUM_PROCESSORS;
Prcb = KiProcessorBlock[Processor]; // 全局唯一的处理器控制块列表}
else
{
Prcb = KeGetCurrentPrcb();
}KiAcquireSpinLock(&Prcb->DpcLock); // 使用自旋锁保护处理器控制块中的DPC队列
由此我们可以看到,实际上DPC队列是每个处理器一个的,我们完全可以将某个DPC对象绑定到某个处理器上,实现类似线程亲缘性(Thread Affinity)的效果,优化在多处理器环境下的性能。但这同时也带来一个问题,就是ISR程序可以和DPC回调函数同时被调用,某种程度上也造成了开发复杂度的增加,具体处理方法请参考DDK中相关文档。
Kernel-Mode Driver ArchitectureDesign GuideServicing InterruptsDPC Objects and DPCs