Deferred Procedure Call (DPC)

At any given moment, execution of kernel-mode code is subject to the current processor’s current Interrupt Request Level (IRQL). Execution can be interrupted by code that runs at a higher IRQL. However, the higher the IRQL the more restricted is the execution. Most kernel functions are available for use only at the lowest IRQL, i.e., PASSIVE_LEVEL (0). The available functions are greatly reduced even for APC_LEVEL (1). Very few can be called at DISPATCH_LEVEL (2) and hardly any beyond that. Where code at this high an IRQL wants to do anything substantial, it must arrange for companion code to get executed at a lower IRQL when the kernel can get around to it. This deferring of execution to gentler circumstances is the essence of the Deferred Procedure Call. The blessing is relative, though, since the DPC executes at DISPATCH_LEVEL, which is still too high for such seemingly slight things as executing even a single instruction that touches pageable memory.

Kernel-mode code that expects to schedule a DPC, whether once or recurringly, first allocates a KDPC in non-paged memory and passes its address to KeInitializeDpc. Other arguments associate the KDPC with both the procedure that is to be called back and an arbitrary context that will be passed to the callback procedure should it ever be called. The way to schedule a call back is to feed the KDPC to the KeInsertQueueDpc function. This puts the KDPC into a per-processor queue. The kernel, in its own good time, removes the KDPC from that queue and the Deferred Procedure Call finally happens. Among the callback procedure’s arguments are the context that was specified when the KDPC was initialised and two more arguments that were specified when the KDPC was inserted. The DPC can be rescheduled simply by reinserting it. This can be done over and over, and typically is. If a DPC is scheduled but the call back is then not wanted, it can be removed from the queue by calling KeRemoveQueueDpc. Some control over the insertion and the processing is available through other functions, which are better called while the KDPC is not inserted. To specify the processor that the DPC is to execute on, use KeSetTargetProcessorDpc or, in version 6.1 and higher, KeSetTargetProcessorDpcEx. To influence the order and circumstances of DPC processing, use KeSetImportanceDpc.

Threaded DPCs

In version 5.2 and higher, a KDPC can represent either a normal DPC, as described above, or a Threaded DPC. In the latter variant, the scheduled procedure is called back at PASSIVE_LEVEL from a highest-priority thread. There’s a catch, however. Support can be disabled via the registry:

Key: HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Kernel
Value: ThreadDpcEnable
Default: 1

The kernel reads this registry value while starting. Whatever it finds applies thereafter to all processors. If this value is present and its data is zero, every threaded DPC is called at DISPATCH_LEVEL much as if it had been a normal DPC all along.