Windows NT assigns a priority to each hardware interrupt and a few software events, namely, the interrupt request level (interrupt request level-IRQL ). IRQL provides a synchronization method for activities on a single CPU Based on the following rules:
Once a CPU runs on IRQL higher than passive_level, the activity on the CPU can only be preemptible by the activity with higher IRQL.
Figure 4-1 shows the IRQL value range on the X86 platform. (Generally, this IRQL value depends on the platform you are facing.) The user mode program runs on passive_level and can be preemptible by any activity that runs above this IRQL. Many Device Driver routines are also executed on passive_level. Discussed in Chapter 2DriverEntryAndAdddeviceRoutines belong to this category, and most IRP dispatch routines also belong to this category.
Some common driver routines are executed on dispatch_level, while dispatch_level is higher than passive_level. These common routines includeStartioRoutine, DPC (deferred process call) routine, and other routines. The common feature of these routines is that they all need to access certain domains in the device object and the device extension, and they are not subject to interference or mutual interference from the dispatch routine. When any such routine runs, the rules stated above can ensure that they are not preemptible by the dispatch routine of any driver, because the dispatch routine itself runs on a lower-level IRQL. In addition, they will not be preemptible by similar routines because those routines run the same IRQL as their own. Only activities with higher IRQL can take the lead.
Note:
The dispatch routine (dispatch routine) is similar to the dispatch_level name. I/O manager sends I/O requests to these functions. The name of the dispatch level (dispatch_level) exists because the kernel thread dispatcher runs on this IRQL and determines which thread to execute next time. (Currently, the thread scheduler usually runs at the synch_level level)
Figure 4-1. Interrupt request level
There are various hardware interruptions between dispatch_level and profile_level. Generally, each device with the Interrupt Capability has an IRQL, which defines the interrupt priority level of the device. The WDM driver can determine the IRQL of its device only after it receives an irp_mj_pnp request with the secondary function code irp_mn_start_device. The configuration information of the device is passed to the request as a parameter, and the IRQL of the device is included in the configuration information. We usually call the device interrupt level device IRQL, or dirql.
Other IRQL-level meanings sometimes depend on the specific CPU structure. These IRQL are generally used only within the Windows NT kernel, so their meaning is not particularly relevant to writing the device driver program. For example, apc_level, which will be discussed in detail later in this chapter, will not be disturbed by other threads on the same CPU when the system schedules an APC (asynchronous process call) routine for a thread at this level. At the high_level level, the system can perform some special operations, such as memory snapshots before System sleep, handle bug check, and handle false interruptions.
IRQL changes
To demonstrate the importance of IRQL, see Figure 4-2, which shows a series of events that occur on a single CPU. At the beginning of the time series, the CPU runs at passive_level. At Moment T1, an interruption arrives, and its service routine runs on dirql1. This level is a dirql between dispatch_level and profile_level. At the T2 moment, another interrupt arrives, and its service routine runs on dirql2, which is a level lower than dirql1. We have discussed preemptive rules, so the CPU will continue to serve the first interruption. When the first interrupt service routine is completed at T3. The interrupt service program may request a DPC. The DPC routine is executed on dispatch_level. Therefore, the current unexecuted highest priority activity is the second interrupted service routine, so the system then executes the second interrupted service routine. This routine ends at T4. If no other interruptions occur after this, the CPU will be reduced to the dispatch_level level to execute the first interrupted DPC routine. After the DPC routine is completed at T5, IRQL returns to the original
Passive_level.
Figure 4-2. Interrupt priority in change
Basic synchronization rules
Follow the following rules to synchronize data using IRQL:
All accesses to shared data should be performed on the same (upgraded) IRQL.
In other words, whenever and wherever your code accesses data objects that are shared by other code, you should make your code run at a level higher than passive_level. Once the passive_level level is exceeded, the operating system will not allow the same IRQL activity to compete with each other, thus preventing potential conflicts. However, this rule is insufficient to protect data on a multi-processor machine. In a multi-processor machine, you also need another protection-spin lock ). If you only care about operations on a single CPU, you can use IRQL to solve all synchronization problems. But in fact, all WDM drivers must be designed to run on a multi-processor system.
IRQL and thread priority
Thread priority is a very different concept from IRQL. The thread priority controls the scheduling action of the thread scheduler and determines when to run the thread first and what thread to run next time. However, when the IRQL level is higher than or equal to the dispatch_level level, the thread switching stops. No matter what thread is currently active, it will remain active until IRQL falls below the dispatch_level level. At this time, the "Priority" only refers to IRQL, which controls which activity should be executed, rather than the context of which thread to switch.
IRQL and paging
One consequence of execution at the IRQL level is that the system will not be able to handle page faults (the system will handle page faults at the APC level ). This means:
Code executed above or equal to dispatch_level cannot cause page faults.
This also means that the Code executed above or equal to dispatch_level must exist in non-Paging memory. In addition, all the data to be accessed by the Code must also exist in non-Paging memory. Finally, as IRQL improves, you will be able to use fewer kernel-mode support routines.
The DDK documentation explicitly specifies the IRQL limits that support routines. For example,KewaitforsingleobjectThere are two limitations on the routine:
- The caller must run at a level lower than or equal to dispatch_level.
- If a non-0 timeout value is specified in the call, the caller must strictly run on IRQL lower than dispatch_level.
The above two lines want to explain: If kewaitforsingleobject is blocked for a specified long time (you specify a non-0 timeout), then you must run on IRQL lower than dispatch_level, this is because thread blocking is only allowed on IRQL. If everything you do is to check whether the event enters the signal state, it can be executed at the dispatch_level level. However, you cannot call kewaitforsingleobject in ISR or other routines that run above dispatch_level.
IRQL implicit Control
Most of the time, the system calls the routines in the driver on the correct IRQL. Although we have not discussed these routines in detail, I hope to give an example to express the meaning of this sentence. The first I/O request you encounter is that the I/O manager calls a dispatch routine to process an IRP. This call occurs at the passive_level level, because you need to block the caller thread and call other support routines. Of course, you cannot block a thread at a higher IRQL level, and passive_level is the only IRQL level that allows you to call any supported routines without limit.
If your dispatch routine is calledIostartpacketWhen the I/O manager calls your startio routine. This call occurs at the dispatch_level level because the system needs to access the I/O queue without interference from other routines (which can insert or delete IRPs in the queue. Recall the rules mentioned above: all access to shared data should be performed at the same (upgraded) IRQL level. Because every routine that can access the IRP queue is executed at the dispatch_level level, no routine can be interrupted during the queue operation (only in a single CPU system ).
The device may generate an interrupt, And the interrupt service routine (ISR) will be called at the dirql level. Some registers on the device may not be securely shared. However, if you only access those registers on dirql, you can ensure that no one on a single CPU computer can impede your ISR execution. If other driver code needs to access these key hardware registers, you should make the Code only run at the dirql level.KesynchronizeexecutionService functions can help you enforce this rule. I will discuss this function in section 7 "connection to interrupt processing.
Next, you should schedule a DPC call. DPC routines are executed at the dispatch_level level. They need to access your IRP queue, retrieve the next request from the queue, and then send the request to the startio routine. You can callIostartnextpacketThe service function extracts the next request from the queue, but must be called at the dispatch_level level. This function will call your startio routine before returning. Note that the IRQL here is quite clever: queue access, call iostartnextpacket, and call startio all need to happen at dispatch_level level, and the system also calls DPC at this IRQL level.
Routine.
Although it is also possible to explicitly control IRQL, there is almost no reason to do so, because the IRQL you need is always the same as the IRQL used when the system calls you. Therefore, you do not need to improve IRQL from time to time. The IRQL that the routine expects is almost always the correct one for the system.
Clear IRQL Control
If necessary, you can temporarily upgrade IRQL on the current processor and then downgrade it back to the original IRQL.KeraiseirqlAndKelowerirqlFunction. The following code runs at passive_level:
KIRQL oldirql; <--1ASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL); <--2KeRaiseIrql(DISPATCH_LEVEL, &oldirql); <--3...KeLowerIrql(oldirql); <--4 |
- Kirql defines the data type used to save IRQL values. We need a variable to save the current IRQL.
- This assert determines the necessary conditions for calling keraiseirql: The New IRQL must be greater than or equal to the current IRQL. If this relationship is not true, keraiseirql will cause a bug check. (A fatal error is reported on the death blue screen)
- Keraiseirql promotes the current IRQL to the IRQL level specified by the first parameter. It also saves the current IRQL value to the variable specified by the second parameter. In this example, we upgraded IRQL to dispatch_level and saved the original IRQL levelOldirqlVariable.
- After executing any code that needs to be executed on the upgraded IRQL, we call kelowerirql to lower IRQL to the level when keraiseirql is called.
As mentioned in the DDK document, you must call kelowerirql with the value returned by your recent keraiseirql call. This is right in the big aspect, because if you increase IRQL, you must lower it. However, the subsequent decisions may be incorrect due to the code you call or the assumptions made by calling your code. Therefore, this sentence in the document is strictly incorrect. The only rule applied to the kelowerirql function is that the new IRQL must be lower than or equal to the current IRQL.
When the system calls your driver routine, you downgrade IRQL (the IRQL used when the system calls your routine, or the IRQL that your routine wants to execute). This is an error, it is also a serious error, although you upgraded IRQL before the routine returns. As a result, some activities can take the lead in your routine and access data objects that your callers think cannot be shared.
There is a function dedicated to raising IRQL to the dispatch_level level:
KIRQL oldirql = KeRaiseIrqlToDpcLevel();...KeLowerIrql(oldirql) |
Note: This function is only declared in ntddk. h and is not declared in WDM. H. Therefore, the WDM driver should not use this function.