Выбрать главу

Before KeInsertQueueDpc places a dpc on dpc queue it first checks the Lock field. If this field is non-null it indicates that the DPC object already resides on one of the system's DPC queues. When this is the case KeInsertQueueDpc returns immediately with a FALSE result. If the Lock field is null, KeInsertQueueDpc links the dpc object onto the target processor's DPC queue through the DpcListEntry links in the DPC object. It then sets the Lock value to non-null and returns TRUE, informing the caller that the DPC has been freshly queued.

The NT DDK states that a DISPATCH_LEVEL software interrupt is issued. This is also known as a DPC queue-drain interrupt, but it is also used for invocations of the scheduler. Under most circumstances it is generated as the DDK asserts. However, there are instances where it is not, and this affects the timeliness of DPC processing. What determines whether or not a software interrupt is generated is the combination of the DPC's target processor and the DPC's importance. The Importance field in the DPC object reflects the DPC's importance, which can be High, Medium, or Low. KeInitializeDpc sets this value to medium, so by default all DPCs are of medium importance. You can override this by calling KeSetImportanceDpc, another function that has its prototype in NTDDK.H, but that is undocumented. The values for High, Medium and Low are also in NTDDK.H (they are just an enumerated type). Before I describe how the importance affects when a DPC queue is drained, there is another effect priorities have on DPC queuing. DPCs that are of low or medium importance are placed at the end of the DPC queue they are targeted for, but high-importance DPCs are placed at the front of the queue!

The way that the combination of the target processor and the DPC importance affects queue draining is shown in Table 1. To summarize, when a high importance DPC is queued, a drain interrupt is generated regardless of whether the DPC is targeted or not targeted. Drain interrupts are generated upon the queuing of a medium importance DPC if the target processor is the current processor. If the target processor is a different processor, the target is sent a drain interrupt only if the number DPCs currently queued exceeds a global threshold. Low importance DPCs have the same drain-generating rule for DPCs that are remotely targeted as medium importance DPCs. Low importance DPCs that are targeted at the current processor cause a drain if the DPC queue length is above the threshold, or if the rate that DPCs have been queued on the processor is less than a global threshold.

DPC Importance Targeted at Current CPU or Non-Targeted Targeted at Different CPU
Low DPC queue length exceeds maximum DPC queue length or DPC request rate is less than minimum DPC request rate or System is idle DPC queue length exceeds maximum DPC queue length or System is idle
Medium Always DPC queue length exceeds maximum DPC queue length or System is idle
High Always Always

Table 1. Rules for Queue-Draining Interrupt Generation

The DDK states that the DPC queue is drained whenever the IRQL drops below DISPATCH_LEVEL. This is actually implemented as a call to an internal interrupt helper function. When an interrupt occurs, control is transferred into the interrupt object that is connected to the interrupt. The code in the interrupt object calls KiInterruptDispatch, which in-turn calls the ISR that is associated with the interrupt. When the ISR returns, KiInterruptDispatch calls the hal function HalEndSystemInterrupt, which is responsible for dropping the IRQL back to its pre-interrupt level. If the IRQL is dropping below DISPATCH_LEVEL and a queue-drain interrupt is pending, then the HAL calls into KiDispatchInterrupt (note that this is a different function from KiInterruptDispatch). KiDispatchInterrupt is the kernel's own internal ISR that handles DISPATCH_LEVEL software interrupts, and it executes DPC queue draining and context switches.

In addition, when a processor is idle it drains any DPCs that are in its queue. Which leads me to an interesting trivia point: the system idle thread executes at DISPATCH_LEVEL rather than at PASSIVE_LEVEL like other threads. Why? Because the idle thread's responsibility (other than to keep the processor doing something when there is no real work to do) is to drain the DPC queue in cases where a DPC object was queued to the processor, but no drain interrupt was generated (like when a low priority DPC is queued and neither drain condition held true). Since DPCs execute at DISPATCH_LEVEL the idle thread is simply set to DISPATCH_LEVEL execution, which means that it, rather than KiDispatchInterrupt, ends up draining the DPC queue even when a drain interrupt is generated. This is because HalEndSystemInterrupt will return the irql to its pre-interrupt level, which will be DISPATCH_LEVEL if the idle thread was running. Thus, KiDispatchInterrupt will not be called (since the IRQL is not dropping below DISPATCH_LEVEL) and the queue will be drained by the idle thread.

Remember that the default behavior of DPCs if you use them without targetting or setting their importance, is that they are of Medium importance and will not be targeted.

So What?

So how does the information on DPC queue drainage, DPC priorities and DPC targeting affect you as a driver writer? Like I said in the introduction, you may never have a need to call upon the advanced capabilities of DPCs. Only one driver shipped with NT, NDIS, uses DPC targeting and priorities. NDIS uses low importance DPCs that are targeted at a particular processor for its DPC processing.

Many devices require timely response by their interrupt handlers. So what benefit is there to using High-importance DPCs versus the default of Medium? Like most driver-related issues, it really depends on the other drivers that are in the system. However, on average there will be less latency between an ISR and a High-importance DPC than between an ISR and a Low-importance DPC.

полную версию книги