As mentioned in Section 3.9, some of the system-supplied drivers use an event object in their DriverEntry routines to wait while the next-lower driver processes an IRP set up by the waiting driver. NT drivers that have driver-created threads or driver Dispatch routines that wait on the completion of a synchronous I/O request also can use an event object to synchronize operations between their threads and/or other driver routines.
Any NT driver that uses an event object must call KeInitializeEvent or IoCreateXxxEvent before it waits on, sets, clears, or resets the event. Figure 3.24 illustrates how a driver with a thread can use an event object for synchronization.
Figure 3.24 Waiting on an Event Object
As Figure 3.24 shows, such a driver must provide the storage for the event object, which must be resident. The driver can use the device extension of a driver-created device object (see Section 3.2), the controller extension if it uses a controller object (see Section 3.4), or nonpaged pool allocated by the driver.
When the DriverEntry or Reinitialize routine calls KeInitializeEvent, it must pass a pointer to the driver’s resident storage for the event object. In addition, the caller must specify the initial State (Signaled or Not-Signaled) for the event object, as shown in Figure 3.24. The caller also must specify the Type for its event object, which can be either of the following:
·SynchronizationEvent
When such an event is set to the Signaled state, a single thread that is waiting on the event becomes eligible for execution and the event’s state is automatically reset to Not-Signaled.
This type of event is sometimes called an autoclearing event, because its Signaled state is automatically reset each time a wait is satisfied.
·NotificationEvent
When such an event is set to the Signaled state, all threads that were waiting on the event become eligible for execution and the event remains in the Signaled state until an explicit reset to Not-Signaled occurs: that is, there is a call to KeClearEvent or KeResetEvent with the given Event pointer.
Very few NT device or intermediate drivers have a single driver-dedicated thread, let alone a set of threads that might synchronize their operations by waiting on an event that protects a shared resource.
Most NT drivers that use event objects to wait for the completion of an I/O operation set the input Type to NotificationEvent when they call KeInitializeEvent. An event object set up for IRPs that an NT driver creates with IoBuildSynchronousFsdRequest or IoBuildDeviceIoControlRequest is almost always initialized as a NotificationEvent because the caller will wait on the event for notification that its request has been satisfied by one or more lower-level drivers.
After the driver has initialized, its thread, if any, and other routines can synchronize their operations on the event. For example, a driver with a thread that manages the queueing of IRPs, such as the system floppy controller driver, might synchronize IRP processing on an event, as shown in Figure 3.24:
1.The thread, which has dequeued an IRP for processing on the device, calls KeWaitForSingleObject with a pointer to the driver-supplied storage for the initialized event object.
2.Other driver routines carry out device I/O operations necessary to satisfy the IRP, and, when these operations are complete, the driver’s DpcForIsr routine calls KeSetEvent with a pointer to the event object, a driver-determined priority boost for the thread (Increment, as shown in Figure 3.24), and a Boolean Wait set to FALSE. Calling KeSetEvent sets the event object to the Signaled state, thereby changing the waiting thread’s state to ready.
3.The Kernel dispatches the thread for execution as soon as a processor is available: that is, no other thread with a higher priority is currently in the ready state and there are no kernel-mode routines to be run at raised IRQL (greater than PASSIVE_LEVEL).
The thread now can complete the IRP if the DpcForIsr has not called IoCompleteRequest with the IRP already, and can dequeue another IRP to be processed on the device.
Calling KeSetEvent with the Wait parameter set to TRUE indicates the caller’s intention to immediately call a KeWait..Object(s) support routine on return from KeSetEvent.
NT driver writers should consider the following guidelines for setting the Wait parameter to KeSetEvent:
A pageable thread or pageable driver routine that runs at IRQL PASSIVE_LEVEL should never call KeSetEvent with the Wait parameter set to TRUE. Such a call causes a fatal page fault if the caller happens to be paged out between the calls to KeSetEvent and KeWait..Object(s).
Any standard driver routine that runs at an IRQL greater than PASSIVE_LEVEL cannot wait for a nonzero interval on any dispatcher object(s) without bringing down the system (see Section 3.9). However, such a routine can call KeSetEvent while running at an IRQL less than or equal to DISPATCH_LEVEL.
For a summary of the IRQLs at which standard NT driver routines run, see Chapter 16. For support-routine-specific IRQL requirements, see the Kernel-Mode Driver Reference.
KeResetEvent returns the previous state of a given Event: whether it was set to Signaled or not when the call to KeResetEvent occurred. KeClearEvent simply sets the state of the given Event to Not-Signaled.
Consider the following a guideline for when to call the preceding support routines:
For better performance, every NT driver should call KeClearEvent unless the caller needs the information returned by KeResetEvent to determine what to do next.