3.9.1 Timer Objects

Any NT driver can set up a timer object with KeInitializeTimer or KeInitializeTimerEx that it can use within a nonarbitrary thread context to time out operations within the driver’s other routines or perform a periodic operation.

A timer can be a notification timer or a synchronization timer. When a notification timer is signaled, all waiting threads have their wait satisfied and the state of the timer remains signaled until it is explicitly reset. When a synchronization timer expires, its state is set to Signaled until a single waiting thread is released and then the timer is reset to the Not-Signaled state. KeInitializeTimer always creates notification timers. KeInitializeTimerEx accepts a Type parameter which can be NotificationTimer or SynchronizationTimer. Both notification and synchronization timers can optionally have an associated CustomTimerDpc routine.

A timer can expire just once or it can be set to expire repeatedly at a given interval. KeSetTimer always sets a timer that will expire just once. KeSetTimerEx accepts an optional Period parameter for specifying a recurring interval for the timer.

Figure 3.23 illustrates using a notification timer to set up a time-out interval for an operation and then wait while other driver routines process an I/O request.

Figure 3.23 Waiting on a Timer Object

As Figure 3.23 shows, a driver must provide storage for the timer object, which must be initialized in the DriverEntry or Reinitialize routine by calling KeInitializeTimer with a pointer to this storage.

Within the context of a particular thread, such as a driver-created thread or a thread requesting a synchronous I/O operation, the driver can wait on its timer object as shown in Figure 3.23:

  1. The thread calls KeSetTimer with a pointer to the timer object and a given DueTime, expressed in units of 100 nanoseconds. A positive value for DueTime specifies an absolute time at which the timer object should be removed from the Kernel’s timer queue and set to the Signaled state. A negative value for DueTime specifies an interval relative to the current system time.

    Note that the thread (or driver routine running in a system thread) passes a NULL pointer for the DPC object previously shown in Figure 3.20 when it calls KeSetTimer if it waits on the timer object instead of queueing a CustomTimerDpc routine.

  2. The thread calls KeWaitForSingleObject with a pointer to the timer object, which puts the thread into a wait state while the timer object is in the Kernel’s timer queue.

  3. The given DueTime expires.

  4. The Kernel dequeues the timer object, sets it to the Signaled state, and changes the thread’s state from waiting to ready.

  5. 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).

NT driver routines that run at raised IRQL can time out requests by using a timer object with an associated DPC object, as already described in Section 3.6, to queue a driver-supplied CustomTimerDpc routine. Only driver routines that run within a nonarbitrary thread context can wait for a nonzero interval on a timer object, as shown in Figure 3.23.

Like every other thread, a driver-created thread is represented by a Kernel thread object, which is also a dispatcher object. Consequently, a driver need not have its driver-created thread use a timer object to voluntarily put itself into a wait state for a given interval. Instead, the thread can call KeDelayExecutionThread with a caller-supplied interval. For more information about this technique, see the section on polling a device in Chapter 16. See also the Kernel-Mode Driver Reference for the specifics of calling KeDelayExecutionThread.

NT drivers’ DriverEntry, Reinitialize, and Unload routines also run in a system thread context, so NT drivers can call KeWaitForSingleObject with a driver-initialized timer object or KeDelayExecutionThread while they are initializing or unloading. A device driver can call KeStallExecutionProcessor for a very short interval (preferably something less than 50 microseconds) if it must wait for the device to update state during its initialization.

However, higher-level NT drivers generally use another synchronization mechanism in their DriverEntry and/or Reinitialize routines instead of using a timer object. Higher-level NT drivers should always be designed to layer themselves over any lower-level driver of a particular type or types of device. Therefore, a higher-level driver tends to become slow-to-load if it waits on a timer object or calls KeDelayExecutionThread because such a driver must wait for an interval long enough to accommodate the slowest possible device supporting it. Note also that a “safe” but minimum interval for such a wait is very difficult to determine.

For more information about the FILE_DEVICE_XXX that NT drivers set in their device objects, see Section 3.2, and see also the Kernel-Mode Driver Reference.

If the system time changes before a timer expires, relative timers are not affected but the system adjusts absolute timers. A relative timer always expires after the specified number of time units elapse, regardless of the absolute system time. An absolute timer expires at a specific system time, so a change in the system time changes the wait duration of an absolute timer.