Programming the Microsoft Windows Driver Model
Watchdog Timers
Some devices won t notify you when something goes wrong they simply don t respond when you talk to them. Each device object has an associated IO_TIMER object that you can use to avoid indefinitely waiting for an operation to finish. While the timer is running, the I/O Manager will call a timer callback routine once a second. Within the timer callback routine, you can take steps to terminate any outstanding operations that should have finished but didn t.
You initialize the timer object at AddDevice time:
NTSTATUS AddDevice(...) {
where fdo is the address of your device object, OnTimer is the timer callback routine, and pdx is a context argument for the I/O Manager s calls to OnTimer.
You start the timer counting by calling IoStartTimer, and you stop it from counting by calling IoStopTimer. In between, your OnTimer routine is called once a second.
The PIOFAKE sample in the companion content illustrates one way of using the IO_TIMER as a watchdog. I put a timer member in the device extension for this fake device. I also defined a BOOLEAN flag to indicate when the driver is actually busy handling an IRP:
typedef struct _DEVICE_EXTENSION {
When I process an IRP_MJ_CREATE after a period with no handles open to the device, I start the timer counting. When I process the IRP_MJ_CLOSE that closes the last handle, I stop the timer:
NTSTATUS DispatchCreate(...) {
The timer cell begins life with the value -1. I set it to 10 (meaning 10 seconds) in the StartIo routine and again after each interrupt. Thus, I allow 10 seconds for the device to digest an output byte and to generate an interrupt that indicates readiness for the next byte. The work to be done by the OnTimer routine at each 1-second tick of the timer needs to be synchronized with the interrupt service routine (ISR). Consequently, I use KeSynchronizeExecution to call a helper routine (CheckTimer) at device IRQL (DIRQL) under protection of the interrupt spin lock. The timer-tick routines dovetail with the ISR and deferred procedure call (DPC) routines as shown in this excerpt:
VOID OnTimer(PDEVICE_OBJECT fdo, PDEVICE_EXTENSION pdx) { KeSynchronizeExecution(pdx->InterruptObject, (PKSYNCHRONIZE_ROUTINE) CheckTimer, pdx); } VOID CheckTimer(PDEVICE_EXTENSION pdx) {
-
The timer value -1 means that no request is currently pending. The value 0 means that the current request has timed out. In either case, we don t want or need to do any more work in this routine. The second part of the if expression decrements the timer. If it hasn t counted down to 0 yet, we return without doing anything else.
-
This driver uses a DEVQUEUE, so we call the DEVQUEUE routine GetCurrentIrp to get the address of the request we re currently processing. If this value is NULL, the device is currently idle.
-
At this point, we ve decided we want to terminate the current request because nothing has happened for 10 seconds. We request a DPC after filling in the IRP status fields. This particular status code (STATUS_IO_TIMEOUT) turns into a Win32 error code (ERROR_SEM_TIMEOUT) for which the standard error text ( The semaphore timeout period has expired ) doesn t really indicate what s gone wrong. If the application that has requested this operation is under your control, you should provide a more meaningful explanation.
-
If the busy flag is FALSE, this interrupt is not expected and will be ignored. The flag might be FALSE if the device has generated a spurious interrupt. (PIOFAKE is for fake hardware, so the device is really a dialog box with an Interrupt button that you can press at a time when no test program is trying to write a string to the device.) Or else a request might have timed out, and CheckTimer would have cleared the flag precisely to prevent the ISR from doing anything.
-
We allow 10 seconds between interrupts.
-
Whatever requested this DPC also filled in the IRP s status fields. We therefore need to call only IoCompleteRequest.
The busy flag plays an important role in guarding against a race between the interrupt service routine (OnInterrupt) and the timeout routine (CheckTimer). The StartIo routine sets busy. One or the other of OnInterrupt or CheckTimer will clear the flag before requesting the DPC that completes the current IRP. Once either of these routines sets the flag, the other will start returning immediately until StartIo starts a new IRP. To properly synchronize access, all routines that touch the busy flag must run in synchrony with the interrupt routine. Hence the use of KeSynchronizeExecution to call CheckTimer and to call the routine (not shown here in the text) that initially sets busy to TRUE.