Some devices don't generate interrupts with every significant state change. Even when the device does generate interrupts, a case can be made to drive an infrequently used, relatively slow device with a polled technique. Printer drivers are a common example of this technique. Since the printer device buffer is large in comparison to the physical print rate, a device driver has a considerable window in which it can occasionally check for the need to refill the buffer.
Working with Polled Devices
Once a device is started, it is generally unacceptable for driver code to hang in a tight loop waiting for the device to finish. In a multiprocessing and multitasking operating system such as Windows 2000, there are always useful chores that can or must occur in parallel with the device's operation.
By Microsoft edict, a driver is not allowed to stall in a tight polling loop for more than 50 microseconds. With today's extraordinary processor speeds, 50 microseconds is an eternity a period in which hundreds of instructions could have executed.
When polling is required, drivers can utilize one of four techniques based on the amount of required poll time and the context in which the stall occurs.
-
Driver code running at PASSIVE_LEVEL IRQL can call KeDelayExecutionThread, described in Table 11.3, to suspend a thread's execution for a specified interval. The thread is removed from the "ready to run" queue of threads and thus does not interfere with other "good to go" threads.
This technique is available only to kernel-mode threads started by other driver code or during the device's initialization or cleanup code.
-
Driver code can "busy wait" the processor using KeStallExecutionProcessor, described in Table 11.4. The function call is equivalent to code hanging in a tight countdown loop, except that the time stalled is processor speed-independent. Threads should not stall the processor for more than 50 microseconds.
-
Synchonization objects, such as kernel events and mutexes, can be utilized. The "stalling" code then waits on the synchronization object to enter the signaled state an act for which another nonstalled code path must take responsibility.
-
A driver can utilize CustomTimerDpc routines to gain the benefit of I/O Timer functionality with controllable timing granularity.
If a device needs to be polled repeatedly, and the delay interval between each polling operation is more than 50 microseconds, the driver design should incorporate system threads (discussed in Chapter 14).
Table 11.3. Function Prototype for KeDelayExecution Thread
NSTATUS KeDelayExecutionThread | IRQL == PASSIVE_LEVEL |
Parameter | Description |
IN KPROCESSOR_MODE waitMode | |
IN BOOLEAN bAlertable | TRUE - if wait is canceled upon receipt of Async Procedure Call |
| FALSE - in kernel mode |
IN PLARGE_INTEGER interval | Wait interval in 100 nanosecond units |
Return value | Success or failure code |
Table 11.4. Function Prototype for KeStallExecutionProcessor
VOID KeStallExecutionProcessor | IRQL == Any Level |
Parameter | Description |
IN ULONG interval | Wait interval in microseconds |
Return value | - void - |
How CustomTimerDpc Routines Work
A CustomTimerDpc routine is just a DPC routine that is associated with a kernel Timer object. The CustomTimerDpc routine runs after the timer's timeout value expires. The Kernel automatically queues the DPC routine for execution. When the Kernel's DPC dispatcher pulls the request from the queue, the CustomTimerDpc routine finally executes. Of course, there could be some delay between the moment the Timer object expires and the actual execution of the DPC routine.
Kernel Timer objects can be programmed to fire once or repeatedly. Thus, CustomTimerDpc routines can be scheduled to run at regular intervals. Like all other DPC routines, a CustomTimerDpc runs at DISPATCH_LEVEL IRQL. Table 11.5 shows the prototype for one of these routines. Notice the CustomTimerDpc routine always receives two reserved arguments from the system. The contents of these two system arguments are undefined. With CustomTimerDpc routines, there is a single context argument that is permanently associated with the DPC object.
CustomTimerDpc routines differ from I/O Timer routines in several ways.
Table 11.5. Function Prototype for a CustomTimerDpc Routine
VOID CustomTimerDpc | IRQL == Any Level |
Parameter | Description |
IN PKDPC pDpc | DPC object generating the request |
IN PVOID pContext | Context passed when DPC initialized |
IN PVOID SystemArg1 | Reserved |
IN PVOID SystemArg2 | Reserved |
Return value | - void - |
-
The minimum resolution of an I/O Timer is one second; the expiration time of a CustomTimerDpc is specified in units of 100 nanoseconds. In reality, the resolution is limited to about 10 milliseconds.
-
The I/O Timer always uses a one-second interval. The expiration interval for a CustomTimerDpc can be specified differently with each firing.
-
The storage for an I/O Timer object is automatically part of the Device object. To use a CustomTimerDpc, both a KDPC and a KTIMER object must be manually declared in nonpaged storage.
How to Set Up a CustomTimerDpc Routine
Working with CustomTimerDpc routines is very straightforward. A driver simply needs to follow these steps.
-
Allocate nonpaged storage (usually in a device or controller extension) for both a KDPC and a KTIMER object.
-
AddDevice calls KeInitializeDpc (Table 11.6) to associate a DPC routine and a context item with the DPC object. This context item is passed to the CustomTimerDpc routine when it fires. The address of the device or controller extension is a good choice for the context item.
-
AddDevice also calls KeInitializeTimer (Table 11.7) just once to set up the timer object.
-
To start a one-shot timer, call KeSetTimer (Table 11.8); to set up a repeating timer, use KeSetTimerEx instead. If these functions are used on a timer object that is currently active, the previous request is canceled and the new expiration time replaces the old one.
Table 11.6. Function Prototype for KeInitializeDpc
VOID KeInitializeDpc | IRQL == PASSIVE_LEVEL |
Parameter | Description |
IN PKDPC pDpc | Pointer to a DPC object for which the caller provides the storage |
IN PKDEFERRED_ROUTINE DeferredRoutine | Specifies the entry point for a routine to be called when the DPC object is removed from the DPC queue |
IN PVOID pContext | Pointer to a caller-supplied context to be passed to the DeferredRoutine when it is called |
Return value | - void - |
Table 11.7. Function Prototype for KeInitializeTimer
VOID KeInitializeTimer | RQL == PASSIVE_LEVEL |
Parameter | Description |
IN PKTIMER Timer | Pointer to a timer object for which the caller provides the storage |
Return value | - void - |
Table 11.8. Function Prototype for KeSetTimer
BOOLEAN KeSetTimer | IRQL <= DISPATCH_LEVEL |
Parameter | Description |
IN PKTIMER Timer | Pointer to timer object to set |
IN LARGE_INTEGER DueTime | Specifies the absolute or relative time at which the timer is to expire |
IN PKDPC Dpc | Pointer to a DPC object that was initialized by KeInitializeDpc |
Return value | |
To keep a timer from firing, call KeCancelTimer (Table 11.9) before the Timer object expires. This also cancels a repeating Timer. To find out whether a Timer has already expired, call KeReadStateTimer (Table 11.10).
To initialize the DPC and timer objects, code must be executing at PASSIVE_LEVEL IRQL. To set, cancel, or read the state of the timer, code must be running at or below DISPATCH_LEVEL IRQL. In general, the function KeInsertQueueDpc should be avoided with the DPC object used for CustomTimerDpc routine. It can lead to race conditions within the driver.
How to Specify Expiration Times
Internally, Windows 2000 maintains the current system time by counting the number of 100-nanosecond intervals since January 1, 1601. This being a very big number, 64 bits are required to hold it in a structure tagged LARGE_ INTEGER. Table 11.11 lists the functions drivers can use to work with time values.
Table 11.9. Function Prototype for KeCancelTimer
BOOLEAN KeCancelTimer | IRQL <= DISPATCH_LEVEL |
Parameter | Description |
IN PKTIMER Timer | Pointer to timer object to cancel |
Return value | |
Table 11.10. Function Prototype for KeReadStateTimer
BOOLEAN KeReadStateTimer | IRQL <= DISPATCH_LEVEL |
Parameter | Description |
IN PKTIMER Timer | Pointer to the timer object to query |
Return value | |
When using KeSetTimer to on a timer object, the expiration time can be specified in one of two ways.
-
A positive LARGE_INTEGER value represents an absolute system time at which the timer will expire. Absolute times correspond to some exact moment in the future, like December 28, 2020 at 6:42 PM.
-
A negative LARGE_INTEGER value represents the length of an interval measured from the current moment, like "10 seconds from now." Clearly, relative time intervals are more useful within driver work.
This fragment of code shows how to set a Timer object to expire after an interval of 75 microseconds. It assumes that pDevExt holds a pointer to a device extension, and that the Extension contains initialized Timer and DPC objects.
LARGE_INTEGER timeDue; timeDue = RtlConvertLongToLargeInteger( -75 * 10 ); KeSetTimer( &pDevExt->Timer, timeDue, &pDevExt->DPC );
Table 11.11. Functions That Operate on System Time Values
Time Functions |
Function | Description |
KeQuerySystemTime | Return 64-bit absolute system time |
RtlTimeToTimeFields | Break 64-bit time into date and time fields |
RtlTimeFieldsToTime | Convert date and time into 64-bit system time |
KeQueryTickCount | Return number of clock interrupts since boot |
KeQueryTimeIncrement | Return number of 100-nanosecond units added to system time for each clock interrupt |
RtlConvertLongToLargeInteger | Create a signed LARGE_INTEGER |
RtlConvertUlongToLargeInteger | Create a positive LARGE_INTEGER |
RtlLargeIntegerXxx | Perform various arithmetic and logical operations on LARGE_INTEGERs |
Since the value passed to KeSetTimer is negative, the system interprets it as a relative time value. Scaling the number by 10 is necessary because the basic unit of system time for the call is 100 nanoseconds, one-tenth of a microsecond.
Other Uses for CustomTimerDpc Routines
In the next section, an example of a driver that performs data transfers using the CustomTimerDpc is presented. Typically, this technique is used to manage devices that do not generate interrupts. However, a CustomTimerDpc can also be used in specialized device timeout situations.