Using Automatic Synchronization
Almost all of the code in a framework-based driver resides in event callback functions. The framework automatically synchronizes most of a driver's callback functions, as follows:
The framework always synchronizes general device object, functional device object (FDO), and physical device object (PDO) event callback functions with each other so that only one of the callback functions (except EvtDeviceSurpriseRemoval, EvtDeviceQueryRemove, and EvtDeviceQueryStop) can be called at a time for each device. These callback functions support Plug and Play (PnP) and power management events and are called at IRQL = PASSIVE_LEVEL.
Optionally, the framework can synchronize the execution of the callback functions that handle a driver's I/O requests, so that these callback functions run one at a time. Specifically, the framework can synchronize the callback functions for queue, interrupt, deferred procedure call (DPC), timer, work-item, and file objects, along with the request object's EvtRequestCancel callback function. The framework calls most of these callback functions at IRQL = DISPATCH_LEVEL, but you can force the queue and file object callback functions to run at IRQL = PASSIVE_LEVEL. (Work-item callback functions always run at PASSIVE_LEVEL.)
The framework implements this automatic synchronization by using a set of internal synchronization locks. The framework ensures that two or more threads cannot call the same callback function at the same time, because each thread must wait until it can acquire a synchronization lock before calling a callback function. (Optionally, drivers can also acquire these synchronization locks when necessary. For more information, see Using Framework Locks.)
Your driver should store object-specific data in object context space. If your driver uses only framework-defined interfaces, only callback functions that receive a handle to the object can access this data. If the framework is synchronizing calls to the driver's callback functions, only one callback function will be called at a time and the object's context space will be accessible to only one callback function at a time.
Unless your driver implements passive-level interrupt handling, code that services interrupts and accesses interrupt data must run at the device's IRQL (DIRQL) and requires additional synchronization. For more information, see Synchronizing Interrupt Code.
If your driver enables automatic synchronization of the callback functions that handle I/O requests, the framework synchronizes these callback functions so that they run one at a time. The following table lists the callback functions that the framework synchronizes.
Object type | Synchronized Callback Functions |
---|---|
Queue object |
|
File object |
|
Request object |
Optionally, the framework can also synchronize these callback functions with any interrupt, DPC, work-item, and timer object callback functions that your driver provides for the device (excluding the interrupt object's EvtInterruptIsr callback function). To enable this additional synchronization, the driver must set the AutomaticSerialization member of these objects' configuration structures to TRUE.
In summary, the framework's automatic synchronization capability provides the following features:
The framework always synchronizes each device's PnP and power management callback functions.
Optionally, the framework can synchronize an I/O queue's request handlers, and a few additional callback functions (see the previous table).
A driver can ask the framework to synchronize callback functions for interrupt, DPC, work-item, and timer objects.
Drivers must synchronize code that services interrupts and accesses interrupt data by using the techniques that are described in Synchronizing Interrupt Code.
The framework does not synchronize a driver's other callback functions, such as the driver's CompletionRoutine callback function, or the callback functions that the I/O target object defines. Instead, the framework provides additional locks that drivers can use to synchronize these callback functions.
Choosing a Synchronization Scope
You can choose to have the framework synchronize all of the callback functions that are associated with all of a device's I/O queues. Alternatively, you can choose to have the framework separately synchronize the callback functions for each of a device's I/O queues. The synchronization options that are available to your driver are as follows:
Device-level synchronization
The framework synchronizes the callback functions that the previous table contains, for all of the device's I/O queues, so that they run one at a time. The framework achieves this synchronization by acquiring the device's synchronization lock before calling a callback function.
Queue-level synchronization
The framework synchronizes the callback functions that the previous table contains, for each individual I/O queue, so that they run one at a time. The framework achieves this synchronization by acquiring the queue's synchronization lock before calling a callback function.
No synchronization
The framework does not synchronize the execution of the callback functions that the previous table contains and does not acquire a synchronization lock before calling the callback functions. If synchronization is required, the driver must provide it.
To specify whether you want the framework to provide device-level synchronization, queue-level synchronization, or no synchronization for your driver, you can specify a synchronization scope for your driver object, device objects, or queue objects. The SynchronizationScope member of an object's WDF_OBJECT_ATTRIBUTES structure identifies the object's synchronization scope. The synchronization scope values that your driver can specify are:
WdfSynchronizationScopeDevice
The framework synchronizes by obtaining a device object's synchronization lock.
WdfSynchronizationScopeQueue
The framework synchronizes by obtaining a queue object's synchronization lock.
WdfSynchronizationScopeNone
The framework does not synchronize and does not obtain a synchronization lock.
WdfSynchronizationScopeInheritFromParent
The framework obtains the object's SynchronizationScope value from the object's parent object.
In general, we do not recommend using device-level synchronization.
For more information about the synchronization scope values, see WDF_SYNCHRONIZATION_SCOPE.
The default synchronization scope for driver objects is WdfSynchronizationScopeNone. The default synchronization scope for device and queue objects is WdfSynchronizationScopeInheritFromParent.
If you want the framework to provide device-level synchronization for all devices, you can use the following steps:
Set SynchronizationScope to WdfSynchronizationScopeDevice in the WDF_OBJECT_ATTRIBUTES structure of the driver's driver object.
Use the default WdfSynchronizationScopeInheritFromParent value for each device object.
Alternatively, to provide device-level synchronization for individual devices, you can use the following steps:
Use the default WdfSynchronizationScopeNone value for the driver object.
Set SynchronizationScope to WdfSynchronizationScopeDevice in the WDF_OBJECT_ATTRIBUTES structure of individual device objects.
If you want the framework to provide queue-level synchronization for a device, the following techniques are available:
For framework versions 1.9 and later, you should enable queue-level synchronization for individual queues by setting WdfSynchronizationScopeQueue in the WDF_OBJECT_ATTRIBUTES structure of the queue object. This is the preferred technique.
Alternatively, you can use the following steps in all framework versions:
- Set SynchronizationScope to WdfSynchronizationScopeQueue in the WDF_OBJECT_ATTRIBUTES structure of the device object.
- Use the default WdfSynchronizationScopeInheritFromParent value for each device's queue objects.
If you do not want the framework to synchronize the callback functions that handle your driver's I/O requests, use the default SynchronizationScope value for your driver's driver, device, and queue objects. In this case, the framework does not automatically synchronize the driver's I/O request-related callback functions, and the callback functions can be called at IRQL <= DISPATCH_LEVEL.
Note that setting a SynchronizationScope value synchronizes only the callback functions that the previous table contains. If you want the framework to also synchronize the driver's interrupt, DPC, work-item, and timer object callback functions, the driver must set the AutomaticSerialization member of these objects' configuration structures to TRUE.
However, you can set AutomaticSerialization to TRUE only if all of the callback functions that you want to synchronize run at the same IRQL. Choosing an execution level, which is described next, might result in incompatible IRQL levels. In such a situation, the driver must use framework locks instead of setting AutomaticSerialization. For more information about the configuration structures for interrupt, DPC, work-item, and timer objects, and for more information about restrictions that apply to setting AutomaticSerialization in these structures, see WDF_INTERRUPT_CONFIG, WDF_DPC_CONFIG, WDF_WORKITEM_CONFIG, and WDF_TIMER_CONFIG.
If you set AutomaticSerialization to TRUE, you should select queue-level synchronization.
Choosing an Execution Level
When a driver creates some types of framework objects, it can specify an execution level for the object. The execution level specifies the IRQL at which the framework will call the object's event callback functions that handle a driver's I/O requests.
If a driver supplies an execution level, the supplied level affects the callback functions for queue and file objects. Ordinarily, if the driver is using automatic synchronization, the framework calls these callback functions at IRQL = DISPATCH_LEVEL. By specifying an execution level, the driver can force the framework to call these callback functions at IRQL = PASSIVE_LEVEL. The framework uses the following rules when setting the IRQL at which queue and file object callback functions are called:
If a driver uses automatic synchronization, its queue and file object callback functions are called at IRQL = DISPATCH_LEVEL unless the driver asks the framework to call its callback functions at IRQL = PASSIVE_LEVEL.
If a driver is not using automatic synchronization and does not specify an execution level, the driver's queue and file object callback functions can be called at IRQL <= DISPATCH_LEVEL.
Note that if your driver provides file object callback functions, you will most likely want the framework to call these callback functions at IRQL = PASSIVE_LEVEL because some file data, such as the file name, is pageable.
To supply an execution level, your driver must specify a value for the ExecutionLevel member of an object's WDF_OBJECT_ATTRIBUTES structure. The execution level values that your driver can specify are:
WdfExecutionLevelPassive
The framework calls the object's callback functions at IRQL = PASSIVE_LEVEL.
WdfExecutionLevelDispatch
The framework can call the object's callback functions at IRQL <= DISPATCH_LEVEL. (If the driver is using automatic synchronization, the framework always calls the callback functions at IRQL = DISPATCH_LEVEL.)
WdfExecutionLevelInheritFromParent
The framework obtains the object's ExecutionLevel value from the object's parent.
The default execution level for driver objects is WdfExecutionLevelDispatch. The default execution level for all other objects is WdfExecutionLevelInheritFromParent.
For more information about the execution level values, see WDF_EXECUTION_LEVEL.
The following table shows the IRQL level at which the framework can call a driver's callback functions for queue objects and file objects.
Synchronization Scope | Execution Level | IRQL of Queue and File Callback Functions |
---|---|---|
WdfSynchronizationScopeDevice |
WdfExecutionLevelPassive |
PASSIVE_LEVEL |
WdfSynchronizationScopeDevice |
WdfExecutionLevelDispatch |
DISPATCH_LEVEL |
WdfSynchronizationScopeQueue |
WdfExecutionLevelPassive |
PASSIVE_LEVEL |
WdfSynchronizationScopeQueue |
WdfExecutionLevelDispatch |
DISPATCH_LEVEL |
WdfSynchronizationScopeNone |
WdfExecutionLevelPassive |
PASSIVE_LEVEL |
WdfSynchronizationScopeNone |
WdfExecutionLevelDispatch |
<= DISPATCH_LEVEL |
You can set the execution level to WdfExecutionLevelPassive or WdfExecutionLevelDispatch for driver, device, file, queue, timer, and general objects. For other objects, only WdfExecutionLevelInheritFromParent is allowed.
You should specify WdfExecutionLevelPassive if:
Your driver's callback functions must call framework methods or Windows Driver Model (WDM) routines that can be called only at IRQL = PASSIVE_LEVEL.
Your driver's callback functions must access pageable code or data. (For example, file object callback functions typically access pageable data.)
Instead of setting WdfExecutionLevelPassive, your driver can set WdfExecutionLevelDispatch and provide a callback function that creates work items if it must handle some operations at IRQL = PASSIVE_LEVEL.
Before you decide whether your driver should set an object's execution level to WdfExecutionLevelPassive, you should determine the IRQL at which your driver and other drivers in the driver stack are called. Consider the following situations:
If your driver is at the top of the kernel-mode driver stack, the system typically calls the driver at IRQL = PASSIVE_LEVEL. The client of such a driver might be a UMDF-based driver or a user-mode application. Specifying WdfExecutionLevelPassive does not adversely affect the driver's performance, because the framework does not have to queue your driver's calls to work items that are called at IRQL = PASSIVE_LEVEL.
If your driver is not at the top of the stack, the system will likely not call your driver at IRQL = PASSIVE_LEVEL. Therefore, the framework must queue your driver's calls to work items, which are later called at IRQL = PASSIVE_LEVEL. This process can cause poor driver performance, compared to allowing your driver's callback functions to be called at IRQL <= DISPATCH_LEVEL.
For DPC objects, and for timer objects that do not represent passive-level timers, note that you cannot set the AutomaticSerialization member of the configuration structure to TRUE if you have set the parent device's execution level to WdfExecutionLevelPassive. This is because the framework will acquire the device object's callback synchronization locks at IRQL = PASSIVE_LEVEL and therefore the locks cannot be used to synchronize the DPC or timer object callback functions, which must execute at IRQL = DISPATCH_LEVEL. In such a case, your driver should use framework spin locks in any device, DPC, or timer object callback functions that must be synchronized with each other.
Also note that for timer objects that do represent passive-level timers, you can set the AutomaticSerialization member of the configuration structure to TRUE only if the parent device's execution level is set to WdfExecutionLevelPassive.