Deferred Procedure Call (DPC)

A Deferred Procedure Call (DPC) is a mechanism used in Windows kernel-mode drivers to execute code at a lower interrupt request level (IRQL) than the interrupt service routine (ISR) that requested it.

DPCS are essential for managing hardware interrupts efficiently. When an interrupt occurs, the ISR is executed immediately at a high IRQL. However, ISRs should be kept as short as possible to avoid delaying other critical system operations. DPCS allow the bulk of the interrupt handling logic to be deferred to a later time when the system is less busy and can execute code at a lower IRQL.

Why Use DPCS?

DPC Objects and Functions

DPCS are managed using the DPC object. Drivers typically create and initialize a DPC object, then associate a DPC routine with it.

Key Structures and Functions:

DPC Routine

The DPC routine is the function that contains the code to be executed asynchronously after the ISR completes. It runs at `DISPATCH_LEVEL` IRQL.

Signature of a DPC Routine:

VOID
  MyDpcRoutine(
    _In_ PKDPC  Dpc,
    _In_opt_ PVOID DeferredContext,
    _In_opt_ PVOID SystemArgument1,
    _In_opt_ PVOID SystemArgument2
  );

DPC Execution Flow

  1. An interrupt occurs, and the ISR is executed at a high IRQL.
  2. The ISR performs minimal necessary hardware acknowledgment and then queues a DPC for execution.
  3. The ISR returns, and the system's interrupt handling mechanism is invoked.
  4. When the system's IRQL drops to or below `DISPATCH_LEVEL`, and the DPC is at the head of the DPC queue, the associated DPC routine is called.
  5. The DPC routine executes its deferred processing logic.
Note: DPC routines must be mindful of their IRQL. They can only safely access memory and system components that are available at `DISPATCH_LEVEL`. Calling functions that require `PASSIVE_LEVEL` (like most user-mode accessible APIs) is not allowed.

Example: Queuing a DPC from an ISR

Here's a simplified conceptual example of how a DPC might be queued from an ISR:

C++

// Assume these are global or driver-defined
KDPC MyDpcObject;
PVOID MyDeviceContext; // Context for the DPC routine

// ISR routine
BOOLEAN
MyInterruptServiceRoutine(
    _In_ PKINTERRUPT Interrupt,
    _In_opt_ PVOID ServiceContext
)
{
    UNREFERENCED_PARAMETER(Interrupt);
    UNREFERENCED_PARAMETER(ServiceContext);

    // Acknowledge hardware interrupt
    // ...

    // Queue the DPC for deferred processing
    if (KeInsertQueueDpc(&MyDpcObject, MyDeviceContext, NULL)) {
        // DPC successfully queued
        return TRUE; // Interrupt was handled
    } else {
        // DPC was already queued or couldn't be queued
        return FALSE; // Interrupt might need further handling or was spurious
    }
}

// DPC routine
VOID
MyDpcRoutine(
    _In_ PKDPC  Dpc,
    _In_opt_ PVOID DeferredContext,
    _In_opt_ PVOID SystemArgument1,
    _In_opt_ PVOID SystemArgument2
)
{
    UNREFERENCED_PARAMETER(Dpc);
    UNREFERENCED_PARAMETER(SystemArgument1);
    UNREFERENCED_PARAMETER(SystemArgument2);

    PVOID deviceContext = DeferredContext;

    // Perform deferred processing using deviceContext
    // This code runs at DISPATCH_LEVEL
    // ...

    // e.g., update device status, signal completion, etc.
}

// In DriverEntry or AddDevice:
// Initialize DPC object
// KeInitializeDpc(&MyDpcObject, MyDpcRoutine, DriverObject);
// Set context
// MyDpcObject.DeferredContext = &MyDeviceContext;
// ... then set up the interrupt object to call MyInterruptServiceRoutine
            

DPC Considerations

Tip: Use the debugger to inspect the DPC queue and understand when and where DPCS are being executed.
Important: Never attempt to call `KeInsertQueueDpc` from within a DPC routine itself. This can lead to deadlocks or infinite loops. If a DPC needs to trigger another deferred action, it should typically signal an event or post to a work queue.

Understanding and correctly implementing DPCS is fundamental for writing efficient and stable Windows kernel-mode drivers.