In this article (How to Trigger User-Mode Events from Interrupt Handling Functions), we mentioned notifying a user-mode event in the WDF driver’s interrupt handling function using the KeSetEvent function, waking up the user-mode program to read the Timer’s count value. However, since the kernel function KeSetEvent is required to run at an IRQL less than or equal to DISPATCH_LEVEL, and our WDF interrupt handling function runs at DIRQL (Device IRQL, above DISPATCH_LEVEL), we need to lower the IRQL of the WDF interrupt handling function. We achieve this by setting the PassiveHandling member of WDF_INTERRUPT_CONFIG to TRUE, which lowers the IRQL of the interrupt handling function to PASSIVE_LEVEL (the lowest IRQL), resolving this conflict. In this article, we will introduce two methods that can also solve the issue of the interrupt handling function having a high IRQL while the operations it performs require a lower IRQL.In the Windows kernel, there are two important asynchronous operation mechanisms: DPC (Deferred Procedure Call) and WorkItem. These mechanisms are typically used to execute some asynchronous delayed tasks.DPC runs at an IRQL of DISPATCH_LEVEL (2), and it is usually initiated by hardware interrupt handlers. When the interrupt handler quickly processes the most urgent tasks (such as saving device state, clearing interrupt status), it places tasks that require more time but are less urgent (such as data processing, starting data transfer) into DPC for processing. DPC is one of the highest priority software mechanisms in the kernel (second only to hardware interrupts), and it can preempt almost any thread, including user-mode threads and kernel system threads. Essentially, DPC is a piece of code scheduled for execution at DISPATCH_LEVEL, working at the same IRQL as the kernel process scheduler. If we compare it to the concept of soft interrupts in the Linux kernel, then DPC is the soft interrupt in the Windows kernel.WorkItem runs at an IRQL of PASSIVE_LEVEL (0), and it runs in system threads, scheduled by the process scheduler along with other user-mode threads. Clearly, WorkItem has a lower priority compared to DPC. However, WorkItem also has its advantages; since it runs at PASSIVE_LEVEL, it can be blocked, access pageable memory, and perform file I/O, which are not allowed in DPC because DPC runs at DISPATCH_LEVEL, where these operations are prohibited. Similarly, as an asynchronous operation, WorkItem can also be used to handle non-urgent tasks in interrupts, with fewer restrictions.The interrupt objects created by WDF drivers can be configured to use the DPC and WorkItem modes described above by simply specifying the corresponding callback functions in the WDF_INTERRUPT_CONFIG structure. However, it is important to note that these two methods are mutually exclusive, meaning an interrupt object can either use DPC or WorkItem, but not both simultaneously. The usage of DPC and WorkItem in WDF driver interrupt handling is quite similar; both involve scheduling a DPC or WorkItem in the interrupt handling function (ISR) to execute the corresponding callback function. This concept is akin to the Top-Half and Bottom-Half processing of interrupts in the Linux kernel, where urgent and short processing tasks are placed in the Top-Half, and a Bottom-Half is scheduled to handle non-urgent but longer processing tasks.Without further ado, let’s look at the example code. First, we will see how to use DPC to handle interrupts. When initializing the interrupt object configuration structure WDF_INTERRUPT_CONFIG, we specify the DPC callback function EvtInterruptDpc, and there is no need to set PassiveHandling to TRUE, as we intend to call KeSetEvent in EvtInterruptDpc, allowing the interrupt handling function to continue running at DIRQL.
Below is the implementation of the interrupt handling function EvtInterruptIsr and the DPC callback function EvtInterruptDpc. In EvtInterruptIsr, we can no longer call KeSetEvent; instead, we call the WDF API function WdfInterruptQueueDpcForIsr. When this function is called, the kernel will start a DPC to execute the DPC callback function EvtInterruptDpc. We can call KeSetEvent in EvtInterruptDpc because DPC runs at DISPATCH_LEVEL, and KeSetEvent can run at a maximum of DISPATCH_LEVEL, satisfying the IRQL requirement..
Here is the log output from the driver when using DPC to handle interrupts. It can be seen that the interrupt handling function EvtInterruptIsr is still running at DIRQL (IRQL = 6), while the DPC callback function EvtInterruptDpc runs at DISPATCH_LEVEL (IRQL = 2). Each time EvtInterruptIsr is called, it triggers EvtInterruptDpc to be called.
Using WorkItem to handle interrupts in WDF drivers is similar to using DPC; we simply specify the EvtInterruptWorkItem callback function in the WDF_INTERRUPT_CONFIG structure, and again, there is no need to set PassiveHandling to TRUE.
In EvtInterruptIsr, we can no longer call KeSetEvent; instead, we call the WDF API function WdfInterruptQueueWorkItemForIsr. When this function is called, the kernel will start a WorkItem to execute the specified WorkItem callback function EvtInterruptWorkItem. We can call KeSetEvent in EvtInterruptWorkItem because WorkItem runs at PASSIVE_LEVEL, which has much looser IRQL restrictions, and the function KeSetEvent can definitely be executed at this IRQL.
Here is the log output from the driver when using WorkItem to handle interrupts. It can be seen that the interrupt handling function EvtInterruptIsr is still running at DIRQL (IRQL = 6), while the WorkItem callback function EvtInterruptWorkItem runs at PASSIVE_LEVEL (IRQL = 0). Each time EvtInterruptIsr is called, it triggers EvtInterruptWorkItem to be called.
In the examples above, the tasks handled by the interrupt handling function, DPC, and WorkItem callback functions are relatively simple. We are just providing examples; in actual driver programs, there will certainly be more complex tasks to handle. However, the usage of these two interrupt handling methods will definitely be the same as described above.