VOID IoInitializeDpcRequest( IN PDEVICE_OBJECT DeviceObject,IN PIO_DPC_ROUTINE DpcRoutine) { KeInitializeDpc( &DeviceObject->Dpc,(PKDEFERRED_ROUTINE) DpcRoutine,DeviceObject ); }
APC更多是和对应的线程关联的,而DPC则是和设备对象关联的。设备对象本身包含一个DPC结构体,如果在驱动开发过程中需要DPC支持,则可以通过将设备对象的DPC指针进行初始化,并填充相应的DPC处理例程。正因为DPC是在驱动程序中使用的,所以它的运行上下文是不确定的。也正因为这个原因,所以DPC的运行需要在DISPATCH_LEVEL级别进行,同时也就要求DPC运行的时候所涉及到的数据必须是非换页的。
VOID NTAPI KeInitializeDpc(IN PKDPC Dpc,IN PKDEFERRED_ROUTINE DeferredRoutine,IN PVOID DeferredContext) { KiInitializeDpc(Dpc,DeferredRoutine,DeferredContext,DpcObject); } VOID NTAPI KeInitializeThreadedDpc(IN PKDPC Dpc,ThreadedDpcObject); }上面的两个函数很简单,只是根据不同类型的DPC调用内部的实现函数。不过需要注意的是,后者虽然带thread,但是这种DPC的运行上下文同样也是任意的。这一点可以从下面的代码中得到佐证。内部函数的实现很简单,只是根据传递的信息进行结构体的成员变量填充而已。其中有一个很关键的数据变量DpcData填充为NULL。DpcData的原型定义如下所示:
typedef struct _KDPC_DATA { LIST_ENTRY DpcListHead; ULONG_PTR DpcLock; volatile ULONG DpcQueueDepth; ULONG DpcCount; } KDPC_DATA,*PKDPC_DATA;其中,DpcData会根据DPC的cpu亲和性找到对应的PRCB,根据这个PRCB结构体中的KDPC_DATA数据结构指针,填充DpcData。其中DpcListHead作为整个DPC的链表表头指针存在,而DpcLock则作为访问DpcData的互斥锁。DpcQueueDepth为队列中包含的DPC的个数,而DpcCount则用于统计DPC的综述,作为性能分析之用。
BOOLEAN NTAPI KeInsertQueueDpc(IN PKDPC Dpc,IN PVOID SystemArgument1,IN PVOID SystemArgument2) { KIRQL OldIrql; PKPRCB Prcb,CurrentPrcb; ULONG cpu; PKDPC_DATA DpcData; BOOLEAN DpcConfigured = FALSE,DpcInserted = FALSE; KeRaiseIrql(HIGH_LEVEL,&OldIrql); CurrentPrcb = KeGetCurrentPrcb(); if (Dpc->Number >= MAXIMUM_PROCESSORS) { cpu = Dpc->Number - MAXIMUM_PROCESSORS; Prcb = KiProcessorBlock[cpu]; } else { Prcb = CurrentPrcb; cpu = Prcb->Number; } if ((Dpc->Type == ThreadedDpcObject) && (Prcb->ThreadDpcEnable)) { DpcData = &Prcb->DpcData[DPC_THREADED]; } else { DpcData = &Prcb->DpcData[DPC_NORMAL]; } KiAcquireSpinLock(&DpcData->DpcLock); if (!InterlockedCompareExchangePointer(&Dpc->DpcData,DpcData,NULL)) { Dpc->SystemArgument1 = SystemArgument1; Dpc->SystemArgument2 = SystemArgument2; DpcData->DpcQueueDepth++; DpcData->DpcCount++; DpcConfigured = TRUE; if (Dpc->Importance == HighImportance) { InsertHeadList(&DpcData->DpcListHead,&Dpc->DpcListEntry); } else { InsertTailList(&DpcData->DpcListHead,&Dpc->DpcListEntry); } if (&Prcb->DpcData[DPC_THREADED] == DpcData) { if (!(Prcb->DpcThreadActive) && !(Prcb->DpcThreadRequested)) { while (TRUE); } } else { if (!(Prcb->DpcRoutineActive) && !(Prcb->DpcInterruptRequested)) { if (Prcb != CurrentPrcb) { if (((Dpc->Importance == HighImportance) || (DpcData->DpcQueueDepth >= Prcb->MaximumDpcQueueDepth)) && (!(AFFINITY_MASK(cpu) & KiIdleSummary) || (Prcb->Sleeping))) { Prcb->DpcInterruptRequested = TRUE; DpcInserted = TRUE; } } else { if ((Dpc->Importance != LowImportance) || (DpcData->DpcQueueDepth >= Prcb->MaximumDpcQueueDepth) || (Prcb->DpcRequestRate < Prcb->MinimumDpcRate)) { Prcb->DpcInterruptRequested = TRUE; DpcInserted = TRUE; } } } } } KiReleaseSpinLock(&DpcData->DpcLock); if (DpcInserted) { if (Prcb != CurrentPrcb) { KiIpiSend(AFFINITY_MASK(cpu),IPI_DPC); } else { HalRequestSoftwareInterrupt(DISPATCH_LEVEL); } } KeLowerIrql(OldIrql); return DpcConfigured; }
通过上面对KDPC_DATA结构体成员变量的解释,可以很容易理解整个函数的执行流程。首先,将IRQL提升到最高级,也就是屏蔽了所有的中断,之所以这样,DPC的操作实质是相对系统的全局的操作,所以需要将IRQL提升到最高级,以保证执行的原子性。然后,根据DPC的cpu亲和性选择相应的PRCB,以及由相应的DPC类型寻找相应的KDPC_DATA数据结构的地址。在将DPC挂载到队列的过程中存在多个判断,首先会根据重要级别选择DPC挂载到列表中的位置。如果当前没有对DPC进行处理,则需要根据相应的条件判断是否进行DPC处理。前者,当需要设置的DPC的目标处理器不是当前处理器时,根据两个条件判断是否请求DPC。第一个是当前的DPC级别是最高级或者DPC已经超过了一定的阈值。因为DPC的所处的IRQL高于线程所处的IRQL,所以需要抑制DPC的处理,先设定一个DPC队列的阈值,当队列中的DPC数量超过阈值时,则一次性将所有的DPC都请求执行。第二个条件是当前的处理器正处于空闲或者休眠状态。如果DPC请求执行的处理器恰好是当前处理器,那么处理器不可能处于空闲或者睡眠状态,则需要根据其他的条件判断是否进行DPC的请求执行。看起来是三个或操作,实际是同一性质的判断——即当前的DPC是否紧急(紧急级别不能使最低或者已经到了需要清空DPC队列的时候),或者就算不紧急至少是可以忍受的(当前DPC的请求率低于阈值)。最后调用的跳转,见中断相关分析的博客。
VOID NTAPI KiDispatchInterrupt(VOID) { PKIPCR Pcr = (PKIPCR)KeGetPcr(); PKPRCB Prcb = &Pcr->PrcbData; PVOID OldHandler; PKTHREAD NewThread,OldThread; _disable(); if ((Prcb->DpcData[0].DpcQueueDepth) || (Prcb->TimerRequest) || (Prcb->DeferredReadyListHead.Next)) { OldHandler = Pcr->NtTib.ExceptionList; Pcr->NtTib.ExceptionList = EXCEPTION_CHAIN_END; KiRetireDpcListInDpcStack(Prcb,Prcb->DpcStack); Pcr->NtTib.ExceptionList = OldHandler; } _enable(); if (Prcb->QuantumEnd) { Prcb->QuantumEnd = FALSE; KiQuantumEnd(); } else if (Prcb->NextThread) { OldThread = Prcb->CurrentThread; NewThread = Prcb->NextThread; Prcb->NextThread = NULL; Prcb->CurrentThread = NewThread; NewThread->State = Running; OldThread->WaitReason = WrDispatchInt; KxQueueReadyThread(OldThread,Prcb); KiSwapContext(APC_LEVEL,OldThread); } }函数在开中断的情况下获得与处理器相关的控制单元,然后根据控制单元得到处理器控制块。之所以可以在开中断的情况下获取处理器相关的控制单元,是因为这一部分只能是可读而不能修改的数据,所以无需提供互斥访问。之后判断三种情况下的DPC是否存在(三种情况中的第一种情况很好理解,后面两种稍后介绍),如果存在,则需要调整堆栈。
PUBLIC @KiRetireDpcListInDpcStack@8 @KiRetireDpcListInDpcStack@8: mov eax,esp mov esp,edx push eax call @KiRetireDpcList@4 pop esp ret
这就是在上面的调用的KiRetireDpcListDpcStack的汇编实现,注意KiRetireDpcListStack的调用方式是fastcall。所以第一个参数是Dpc,直接传递到内部的KiRetireDpcList函数中,而edx传递的函数则被设置为当前堆栈,也就是DPC结构体中的堆栈指针。而原来的堆栈指针则被压缩到调整后的DPC结构体中的堆栈中,注意函数调用返回后的那句pop实际上是恢复对阵指针。
VOID FASTCALL KiRetireDpcList(IN PKPRCB Prcb) { PKDPC_DATA DpcData; PLIST_ENTRY ListHead,DpcEntry; PKDPC Dpc; PKDEFERRED_ROUTINE DeferredRoutine; PVOID DeferredContext,SystemArgument1,SystemArgument2; ULONG_PTR TimerHand; KIRQL OldIrql; DpcData = &Prcb->DpcData[DPC_NORMAL]; ListHead = &DpcData->DpcListHead; do { Prcb->DpcRoutineActive = TRUE; if (Prcb->TimerRequest) { TimerHand = Prcb->TimerHand; Prcb->TimerRequest = 0; _enable(); KiTimerExpiration(NULL,NULL,(PVOID)TimerHand,NULL); _disable(); } while (DpcData->DpcQueueDepth != 0) { KeAcquireSpinLockAtDpcLevel(&DpcData->DpcLock); DpcEntry = ListHead->Flink; if (DpcEntry != ListHead) { RemoveEntryList(DpcEntry); Dpc = CONTAINING_RECORD(DpcEntry,KDPC,DpcListEntry); Dpc->DpcData = NULL; DeferredRoutine = Dpc->DeferredRoutine; DeferredContext = Dpc->DeferredContext; SystemArgument1 = Dpc->SystemArgument1; SystemArgument2 = Dpc->SystemArgument2; DpcData->DpcQueueDepth--; Prcb->DebugDpcTime = 0; KeReleaseSpinLockFromDpcLevel(&DpcData->DpcLock); _enable(); DeferredRoutine(Dpc,SystemArgument2); _disable(); } else { KeReleaseSpinLockFromDpcLevel(&DpcData->DpcLock); } } Prcb->DpcRoutineActive = FALSE; Prcb->DpcInterruptRequested = FALSE; if (Prcb->DeferredReadyListHead.Next) { _enable(); OldIrql = KeRaiseIrqlToSynchLevel(); KiProcessDeferredReadyList(Prcb); KeLowerIrql(OldIrql); _disable(); } } while (DpcData->DpcQueueDepth != 0); }上面根据三种使用DPC的属性进行DPC调用,其中,第一种是内核定时器的DPC调用过程;第二种是普通驱动程序中使用的DPC调用;第三种是延迟就绪的线程使用的DPC调用过程,延迟就绪线程是指线程已经就绪,但是不确定被调度到那个处理器上运行的就绪线程。之所以这样排序,是因为前两个部分可能激活等待的数据,这样第三步对线程进行状态进行调整的时候就可以尽量将这次条件满足的线程激活而不要等到下一次。