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(...) { ... IoInitializeTimer(fdo, (PIO_TIMER_ROUTINE) OnTimer, pdx); ... } |
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 on the companion disc illustrates one way of using the IO_TIMER as a watchdog. I put a timer member into the device extension for this fake device:
typedef struct _DEVICE_EXTENSION { ... LONG timer; ... } DEVICE_EXTENSION, *PDEVICE_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(...) { ... if (InterlockedIncrement(&pdx->handles == 1) { pdx->timer = -1; IoStartTimer(fdo); } ... } NTSTATUS DispatchClose(...) { ... if (InterlockedDecrement(&pdx->handles) == 0) IoStopTimer(fdo); ... } |
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. (See the sidebar "More About PIOFAKE" for an explanation of the way this nonexistent device works.) 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 DPC routines as shown in this excerpt:
1 2 3 4 5 6 |
VOID OnTimer(PDEVICE_OBJECT fdo, PDEVICE_EXTENSION pdx) { KeSynchronizeExecution(pdx->InterruptObject, (PKSYNCHRONIZE_ROUTINE) CheckTimer, pdx); } VOID CheckTimer(PDEVICE_EXTENSION pdx) { if (pdx->timer <= 0 || --pdx->timer > 0) return; PIRP Irp = GetCurrentIrp(&pdx->dqReadWrite); if (!Irp) return; Irp->IoStatus.Status = STATUS_IO_TIMEOUT; Irp->IoStatus.Information = 0; IoRequestDpc(pdx->DeviceObject, Irp, NULL); } BOOLEAN OnInterrupt(...) { ... if (pdx->timer <= 0) return TRUE; if (!pdx->nbytes) { Irp->IoStatus.Status = STATUS_SUCCESS; Irp->IoStatus.Information = pdx->numxfer; pdx->timer = -1; IoRequestDpc(pdx->DeviceObject, Irp, NULL); } ... pdx->timer = 10; } VOID DpcForIsr(...) { ... PIRP Irp = StartNextPacket(&pdx->dqReadWrite, fdo); IoCompleteRequest(Irp, IO_NO_INCREMENT); ... } |