52964.fb2
This chapter shows how the WdmIo and PHDIo drivers handle interrupts, use Deferred Procedure Calls (DPCs), and catch time-outs. It shows how Read and Write interrupt-driven I/O is started and demonstrates how interrupt handling routines should work.
A driver's interrupt handler must service a hardware device's immediate needs. Any major processing must wait until a driver's DPC routine runs. Eventually, the DPC completes the read or write request and starts the next queued IRP.
The chapter also covers two types of timer. A basic "one-second interval" timer is best used to implement a time-out for interrupt-driven operations. However, custom timers and their associated Custom DPCs can be used for finer grain timing.
First, let me be clear exactly what a hardware interrupt is. Devices that generate interrupts use an electrical signal to indicate that some special condition has occurred. The interrupt system is designed to stop the processor (or one of the processors) in its tracks — as soon as possible — and start some code to service the interrupt.
When an interrupt occurs, the processor saves a small amount of information on the kernel stack: the processor registers and the instruction pointer before the interrupt. This is just enough to restore the context when the interrupt service routine has completed.
Interrupts are usually used when something important has happened, or when a driver ought to do something important to its hardware device. Following are some example interrupt events.
• From the modem: I have just received a character. Come and get it! Another might arrive soon and overwrite this one.
• From the disk controller: I have just finished a DMA transfer of a sector of data. What shall I do now?
• From the printer: I have just printed a character. Give me another!
• From the printer: I have just run out of paper. Please tell the user to buy some more!
As you can see, not all interrupts are equally important. Of the previous four situations, the modem and disk controller interrupts are the most important.
x86 processors have a NonMaskable Interrupt (NMI) pin input and an Interrupt (INTR) pin input, with all normal device interrupts sharing the INTR input. External 8259A controllers are used to provide several interrupt lines to devices (i.e., IRQ0-IRQ15). IRQ0-IRQ15 share the INTR pin. An interrupt vector specifies the memory location that contains the address of the interrupt service routine. The 8259A controllers provide a different vector for each IRQ number. Therefore, IRQ0-IRQ15 each have their own interrupt service routines inside the Windows kernel.
x86 processors prioritize their interrupts in a simple way. Processor exceptions have the highest priority. NMI interrupts come next and then INTR interrupts. The INTR interrupts are maskable, as they can be disabled by setting a bit in the processor status register.
If an INTR interrupt occurs, its service routine will run until completion, stopping other INTR interrupts from occurring. However, an NMI interrupt could still butt in half-way through the INTR service routine. An interrupt service routine should do its job quickly, as it could stop other equally important INTR interrupts from being serviced. The interrupt latency is the time between a device asserting its interrupt signal and its service routine starting. Help keep the interrupt latency down for all drivers by making your interrupt service routine run quickly.
As described previously, Windows provides the initial interrupt handler for IRQ0-IRQ15. If your driver has connected to one of these interrupts (IRQ7 in the default case for WdmIo), it calls your interrupt handling routine to service the interrupt. More than one device can share the same interrupt line, so your first job is to determine if it really was your device that interrupted the processor.
Most hardware has a mind of its own, especially when riddled with by users, and so is likely to generate interrupts at the most awkward time. In particular, you can guarantee that the interrupts will not occur in the context of the user mode thread that should receive its data. Note that an interrupt may occur at any point in your driver code, so you have to be especially careful that you can cope.
Naturally, the Windows kernel helps considerably. In particular, Critical Section routines forcibly increase the IRQ level (IRQL) to the appropriate Device IRQL (DIRQL) so that an interrupt cannot intervene while your Critical Section routine is running. In an x86 system, it sets the appropriate bit in the processor status register that disables INTR interrupts.
Most devices that interrupt have some similar sort of interrupt disabling facility. At power up, a device's interrupts are usually disabled. However, it is best to disable your device's interrupts at the first possible opportunity. Once your driver has installed its interrupt handler, you can enable interrupts in the device. Even now, you should note that interrupts may not conveniently arrive after you have started processing your read request. From the word go, a modem might start interrupting with incoming data. Your driver may well not have received a Read IRP yet, so you will have to decide what to do with these incoming bytes. You could buffer them somewhere in the expectation of a Read IRP coming round the corner. Or you could just dump them on the floor.
A WDM driver receives its interrupt resource assignments in its Plug and Play IRP StartDevice handler. Each interrupt has these details:
• Interrupt level[42]
• Interrupt vector
• Interrupt affinity: a set of bits indicating which processors it can use in a multiprocessor system
• Interrupt mode: latched or level sensitive
Usually, you just need to store these details and pass them on to IoConnectInterrupt when you are ready to handle interrupts.
First, declare a PKINTERRUPT object that will be initialized by IoConnectInterrupt. Like most drivers, WdmIo declares this in its device extension. However, if all your devices use the same interrupt, this object may well need to be a driver global; you would need to connect to the interrupt only once (e.g., in your DriverEntry routine).
Table 17.1 shows the parameters to pass to IoConnectInterrupt. Apart from the various assigned interrupt values, you must pass the name of your interrupt handling routine and a context to pass to it. The WdmIo driver uses the device extension pointer as this context parameter. If all your devices share an interrupt, you will need to use a different context pointer; the interrupt handler will have to work out for itself to which device the interrupt refers.
The only complication to IoConnectInterrupt is what happens when you use more than one interrupt. You need to call IoConnectInterrupt once for each interrupt, using the same initialized spin lock pointer in each call. This is used to resolve tensions between the various handlers so that only one is called at once. Pass the highest DIRQL value in the SynchronizeIrql parameter in each call.
Table 17.1 IoConnectInterrupt function
NTSTATUS IoConnectInterrupt | (IRQL==PASSIVE_LEVEL) |
---|---|
Parameter | Description |
OUT PKINTERRUPT *InterruptObject | Interrupt object pointer |
IN PKSERVICE_ROUTINE ServiceRoutine | Name of interrupt handling routine |
IN PVOID ServiceContext | Context to pass to the interrupt handler, usually the device extension pointer |
IN PKSPIN_LOCK SpinLock | Optional spin lock parameter used when a driver uses more than one interrupt; NULL, otherwise |
IN ULONG Vector | Assigned interrupt vector |
IN KIRQL Irql | Assigned interrupt IRQL |
IN KIRQL SynchronizeIrql | The highest IRQL of the interrupts that a driver uses (i.e., usually the same as IRQL) |
IN KINTERRUPT_MODE InterruptMode | Assigned LevelSensitive or Latched value |
IN BOOLEAN ShareVector | TRUE if the interrupt vector is shareable |
IN KAFFINITY ProcessorEnableMask | Assigned interrupt affinity |
IN BOOLEAN FloatingSave | TRUE if the floating-point stack should be saved For x86 systems, this value must be FALSE |
Do not forget to disconnect your interrupt handler using IoDisconnectInterrupt before your device disappears. And you must disconnect from the interrupt if the Plug and Play system stops your device. The WdmIo driver always stops its device when the device is about to be removed, so the StopDevice routine in DeviceIo.cpp is the only place where WdmIo disconnects its interrupt handler.
The WdmIo driver connects to its interrupt handler when it processes a PHDIO_IRQ_CONNECT command in its ProcessCmds routine. It remembers if it connected successfully in the device extension ConnectedToInterrupt field. If the device is stopped for Plug and Play resource reallocation, this flag is left true after IoDisconnectInterrupt is called. When the device is restarted, WdmIo reconnects to the new interrupt if ConnectedToInterrupt is true.
The actual call to IoConnectInterrupt is done in a system worker thread as this function must be called at PASSIVE_LEVEL IRQL. In this case, it is the work item that completes the IRP and starts the next IRP using IoStartNextPacket.
Before I look at its interrupt handler, let's see how WdmIo starts and runs its interrupt-driven reads and writes.
Listing 17.1 shows the mass of extra fields that are needed in the device extension. The ConnectedToInterrupt field, as mentioned before, indicates whether WdmIo has connected to the interrupt. InterruptReg, InterruptRegMask, and InterruptRegValue are the values given by the controlling Win32 application in its PHDIO_IRCLCONNECT command.
Remember that the controlling application uses three IOCTLs, such as IOCTL_PHDIO_CMDS_FOR_WRITE, to store the commands that are used to process read and write transfers. WdmIoStartIo calls StoreCmds in each case to allocate some memory from the nonpaged pool to store a copy of the passed commands. The device extension WriteCmds, StartReadCmds, and ReadCmds fields store the pointers to this memory, with WriteCmdsLen, StartReadCmdsLen, and ReadCmdsLen storing the lengths. WdmIo frees the allocated memory when the device is removed.
The three time-out variables are used to detect time-outs, as described later. If Timeout is –1, no read or write is in progress. The final new group of variables are used to keep track of where WdmIo is in the read or write transfer. The next sections describe how these fields are used.
Listing 17.1 Interrupt handling fields in the device extension
// Interrupt handling support
bool ConnectedToInterrupt;
UCHAR InterruptReg;
UCHAR InterruptRegMask;
UCHAR InterruptRegValue;
ULONG CmdOutputCount; // Count of bytes output from commands
PUCHAR WriteCmds; // Stored commands for write IRP
ULONG WriteCmdsLen; // length
PUCHAR StartReadCmds; // Stored commands for start read IRP
ULONG StartReadCmdsLen; // length
PUCHAR ReadCmds; // Stored commands for read IRP
ULONG ReadCmdsLen; // length
UCHAR SetTimeout; // Timeout stored from script
int Timeout; // Seconds left to go. –1 if not in force
bool StopTimer; // Set to stop timer
ULONG TxTotal; // R/W total transfer size in bytes
ULONG TxLeft; // R/W bytes left to transfer
PUCHAR TxBuffer; // R/W buffer. Moves through current IRP SystemBuffer
bool TxIsWrite; // R/W direction
NTSTATUS TxStatus; // R/W status return
UCHAR TxResult[5]; // R/W output buffer (2 Failcode, 2 Offset, 1 user)
UCHAR TxLastIntReg; // R/W last interrupt register value
ULONG TxCmdOutputCount; // R/W Copy of last CmdOutputCount
Listing 17.2 shows how WdmIoStartIo initiates a write request. The device extension TxIsWrite field is set true for writes, and false for reads. TxTotal contains the total number of bytes to transfer. TxLeft stores the number of bytes left to transfer, and so counts from TxTotal down to zero. TxBuffer points to the next byte to transfer in the IRP buffer, so it moves through the buffer as each byte is written. The IRP buffer is always accessible, as long as the IRP is being processed, so there is no need to make a copy of it. The TxStatus field contains the IRP's eventual completion status, which is initially assumed to be successful.
The TxResult array is used to contain the output from running the stored write commands. This 5-byte array is, therefore, zeroed before the write begins in earnest. TxLastIntReg stores the last value read by the interrupt handler from its status register. The contents of TxResult and TxLastIntReg can eventually be obtained using the IOCTL_WDMIO_GET_RW_ RESULTS call, as shown in Listing 15.4.
The "one-second interval" timer is now started to detect time-outs.
WdmIo is now finally ready to output the first data byte by running the stored write data commands. As interrupts have been enabled, they must be run in the context of a Critical Section routine to avoid being interrupted. Listing 17.2 shows how RunWriteCmdsSynch does this job, calling ProcessCmds to run the commands in dx->WriteCmds with the output going to dx->TxResult. If ProcessCmds fails or if there are bytes left to transfer, RunWriteCmdsSynch returns TRUE and WdmIoStartIo completes the Write IRP straight away with status STATUS_UNSUCCESSFUL.
Listing 17.2 also shows the code in ProcessCmds that handles the PHDIO_WRITE_NEXT command. Basically, it retrieves the next byte from TxBuffer and writes it to the Data register. It increments the TxBuffer pointer and decrements the count of bytes left to process, TxLeft.
Listing 17.2 How WdmIoStartIo starts write requests
case IRP_MJ_WRITE:
if (dx->WriteCmds==NULL || !dx->ConnectedToInterrupt) {
status = STATUS_INVALID_DEVICE_REQUEST;
break;
}
// Store transfer details dx->TxIsWrite = true;
dx->TxTotal = IrpStack->Parameters.Write.Length;
dx->TxLeft = dx->TxTotal;
dx->TxBuffer = (PUCHAR)Buffer;
dx->TxStatus = STATUS_SUCCESS;
RtlZeroMemory(dx->TxResult, sizeof(dx->TxResult));
DebugPrint("WdmIoStartIo: Write %d bytes: %*s", dx->TxTotal, dx->TxTotal, dx->TxBuffer);
// Start timeout timer
dx->Timeout = dx->SetTimeout+1;
IoStartTimer(fdo);
// Send first value
if (KeSynchronizeExecution(dx->InterruptObject, (PKSYNCHRONIZE_ROUTINE)RunWriteCmdsSynch, (PVOID)dx)) {
status = STATUS_UNSUCCESSFUL;
break;
}
return;
// …
BOOLEAN RunWriteCmdsSynch(IN PWDMIO_DEVICE_EXTENSION dx) {
if (dx->TxLeft==0) return TRUE;
dx->CmdOutputCount = 0;
BOOLEAN rv = ProcessCmds(dx, dx->WriteCmds, dx->WriteCmds_en, dx->TxResult, sizeof(dx->TxResult), false);
dx->TxCmdOutputCount = dx->CmdOutputCount;
if (!rv) {
dx->TxStatus = STATUS_UNSUCCESSFUL;
return TRUE;
}
return FALSE;
}
//In ProcessCmds…
case PHDIO_WRITE_NEXT:
{
if (dx->Timeout==-1) {
FailCode = PHDIO_CANNOT_RW_NEXT;
goto fail;
}
if (dx->TxLeft==0) {
FailCode = PHDIO_NO_DATA_LEFT_TO_TRANSFER;
goto fail;
}
GetUChar(reg);
WriteByte(dx, reg, *dx->TxBuffer++);
dx->TxLeft--;
break;
}
Read requests are processed in a very similar way. As Chapter 15 mentioned, one set of stored commands, in StartReadCmds, is used to start read requests. A different set, in ReadCmds, is used to process read interrupts. RunStartReadCmdsSynch and RunReadCmdsSynch are run as Critical Section routines to process these two sets of commands. The PHDIO_READ_NEXT command is run in a very similar way to PHDIO_WRITE_NEXT, except that it stores the byte that it reads in the next location in the IRP buffer.
Listing 17.3 shows the interrupt service routine for WdmIo, InterruptHandler. Remember that this is a general-purpose service routine. If the user mode controlling application has set up its control fields or commands wrongly, things could go horribly wrong.
An interrupt handler should complete its job as quickly as possible. It is run at Device IRQL (DIRQL), so do not make DebugPrint calls. Remember that your device could interrupt at any time, not just when you have started a write request.
The first job of an interrupt handler is to see if the interrupt was generated by the correct device. If it was not, the routine should return FALSE as quickly as possible to let any other chained interrupt service routines have their go. Otherwise it should process the interrupt (at the very least, stop the device from interrupting) and return TRUE.
InterruptHandler reads the device register at InterruptReg. As described in Chapter 15, it then ANDs this value with InterruptRegMask and compares it with InterruptRegValue. If they are equal, the interrupt is valid and the handler can continue.
If the device extension Timeout flag is –1, no transfer is in progress and so TRUE is returned straight away. If your device requires further processing to cancel such "spurious" interrupts, you need to amend WdmIo so that it does what you want.
Next, WdmIo's interrupt handler gets the current IRP for this device. I double-check that there is a current IRP and then see if the I/O Manager has signalled that it should be cancelled by setting its Cancel flag.
If the IRP is still in progress, the TxIsWrite flag indicates whether the Read or Write stored commands should be run. The RunWriteCmdsSynch and RunReadCmdsSynch routines return TRUE if the transfer is now complete (i.e., if all the bytes have been transferred or there has been some error).
If the transfer is now complete, this is remembered by setting Timeout to –1. Interrupt routines run at Device IRQL and so cannot complete IRPs. Completing an IRP also takes some time, so this job ought not to be done in the interrupt handler, anyway. The driver model uses Deferred Procedure Calls (DPCs) to solve these problems, as described in the next section. InterruptHandler calls IoRequestDpc to request that the WdmIo DPC be run.
Listing 17.3 WdmIo Interrupt Handler
BOOLEAN InterruptHandler(IN PKINTERRUPT Interrupt, IN PWDMIO_DEVICE_EXTENSION dx) {
// See if interrupt is ours
dx->TxLastIntReg = ReadByte(dx, dx->InterruptReg);
if ((dx->TxLastIntReg&dx->InterruptRegMask) != dx->InterruptRegValue) return FALSE;
// If no transfer in progress then no further processing required
if (dx->Timeout==-1) return TRUE;
// See if current IRP being cancelled
PDEVICE_OBJECT fdo = dx->fdo;
PIRP Irp = fdo->CurrentIrp;
if (Irp==NULL) return TRUE;
BOOLEAN TxComplete = Irp->Cancel;
if (!TxComplete) {
// Run relevant set of commands
if (dx->TxIsWrite) TxComplete = RunWriteCmdsSynch(dx);
else TxComplete = RunReadCmdsSynch(dx);
}
// If all done, in error or being cancelled then call DPC to complete IRP
if (TxComplete) {
dx->Timeout = –1;
IoRequestDpc(fdo, Irp, dx);
}
return TRUE;
}
Code that runs at an elevated IRQL needs to run as quickly as possible. An elevated IRQL is any IRQL above DISPATCH_LEVEL (e.g., at Device IRQL in an interrupt service routine). Code that runs at an elevated IRQL cannot make most useful kernel calls.
The Windows kernel helps solve both these problems with Deferred Procedure Call (DPC) routines, that run at DISPATCH_LEVEL When an interrupt service routine has done all the jobs that must be performed, it should request that its DPC routine be run. This DPC routine should continue where the interrupt service routine left off.
A DPC typically either starts another transfer or completes an IRP.
In WdmIo, all the bytes are transferred in the interrupt handler. When the transfer is complete, WdmIo asks that its DPC be run to complete the IRP.
Other drivers may use their DPC routine to do data transfers. While WdmIo could use this technique, it would be slower. Processing the read or write commands in WdmIo is usually fairly quick, so it is simplest to get it over and done with[43].
Direct Memory Access (DMA) is a hardware facility for transferring many bytes of data from place to place without having to be handled by the processor. The DMA controller or the relevant device usually interrupts when the whole transfer has finished. The interrupt service routine usually runs a DPC routine to start its next transfer or complete the IRP. DMA transfers cannot be started at elevated IRQL, so the next transfer must be set up in a DPC.
An indeterminate amount of time elapses between a DPC being requested and when it is run. Do not defer any interrupt servicing to the DPC if data might be lost (e.g., read data being overwritten). If necessary, store any data in the device extension or some other preallocated nonpaged memory.
The Windows kernel makes it easy to use one DPC routine within a driver. If you want to use more than one DPC routine, check out the next section.
A standard device object contains the necessary KDPC object. However, you need to initialize it using IoInitializeDpcRequest, passing the name of your DPC routine. WdmIo makes this call in Pnp.cpp just after its FDO is created in its AddDevice routine, passing the name of its DPC routine, WdmIoDpcForIsr.
// Initialize our DPC for IRQ completion processing
IoInitializeDpcRequest(fdo, WdmIoDpcForIsr);
Asking for your DPC to be run is very easy — you just call IoRequestDpc, passing an IRP pointer and a context that you want passed to the DPC routine. As shown previously, WdmIo only asks for its DPC routine to be run when it has transferred all bytes or when an error has occurred. In both cases, the current IRP must be completed and the next queued IRP started.
As shown in Listing 17.4, WdmIoDpcForIsr starts by indicating that the transfer has stopped by setting the device extension Timeout field to –1. It then works out how many bytes have been transferred by subtracting the TxLeft field from TxTotal. If the I/O Manager wants the IRP cancelled, store STATUS_CANCELLED in the TxStatus return status field.
WdmIoDpcForIsr then removes the cancel routine, unlocks the device, completes the IRP, and starts the next queued IRP.
Listing 17.4 WdmIo Interrupt Deferred Procedure Call handler
VOID WdmIoDpcForIsr(IN PKDPC Dpc, IN PDEVICE_OBJECT fdo, IN PIRP Irp, IN PWDMIO_DEVICE_EXTENSION dx) {
dx->Timeout = –1;
ULONG BytesTxd = dx->TxTotal - dx->TxLeft;
if (Irp->Cancel) dx->TxStatus = STATUS_CANCELLED;
DebugPrint("WdmIoDpcForIsr: Status %x Info %d", dx->TxStatus, BytesTxd);
// Remove cancel routine
KIRQL OldIrql;
IoAcqiureCancelSpinLock(&OldIrql);
IoSetCancelRoutine(Irp, NULL);
IoReleaseCancelSpinLock(OldIrql);
// Unlock device and complete
IRP UnlockDevice(dx);
CompleteIrp(Irp, dx->TxStatus, BytesTxd);
IoStartNextPacket(fdo, TRUE);
// Stop timer calls
dx->StopTimer = true;
}
DPC Gotchas
Even with this simple DPC for deferred interrupt processing, there are some potential problems that you have to look out for.
The first point to note is that if two or more calls to IoRequestDpc are made before the DPC can be run, the DPC is only run once. Suppose two interrupts occur in quick succession. If each interrupt handler calls IoRequestDpc, you might expect that the DPC is run twice. However, if there is a long time before the DPC is run, then it is only run once. You must cope with this. In this situation, it might be that one interrupt wants a read call completed, while the next wants a write call completed. Use a separate flag in the device extension to indicate each condition. The DPC routine should be prepared to handle both situations. An easier alternative might be to use a different DPC routine for each condition.
For the WdmIo driver, this problem should almost never arise. The only situation I can envision is one in which an IRP is cancelled just after the interrupt handler calls IoRequestDpc.
However, in this case, the late-running DPC routine will find that the IRP has been cancelled, which is correct.
The other potential problems regarding DPCs will only occur in multiprocessor systems.
• A DPC routine is running on one processor as a device interrupt is handled on another.
• When an interrupt handler asks for a DPC to be run, it is run straight away on another processor before the interrupt service routine exits.
• Two or more DPC routines may be running at the same time on different processors.
The main solution to these problems is to use Critical Section routines whenever a DPC routine needs to access fields that an interrupt handler or another DPC use.
If you need to use more than one DPC, this is fairly straightforward. These "custom DPCs" are also used for Custom Timers with fine grain time-outs.
Declare a KDPC object in nonpaged memory (e.g., in the device extension). Initialize it using KeInitializeDpc. The DeferredContext parameter is eventually passed to the custom DPC routine.
To ask that your DPC routine be run, call KeInsertQueueDpc from within your interrupt handler. KeInsertQueueDpc returns TRUE if the request was successfully queued. It returns FALSE if the DPC is already in the queue. The SystemArgument1 and SystemArgument2 parameters to KeInsertQueueDpc are eventually passed to your DPC routine. You can use KeRemoveQueueDpc to remove your DPC request from the queue.
Table 17.2 shows the function prototype for your custom DPC routine. There are three context parameters. To be compatible with the basic DPC handler, these should be your FDO, the IRP, and usually the device extension. However, use these as you wish.
Table 17.2 CustomDpc prototype
VOID CustomDpc | (IRQL==DISPATCH_LEVEL) |
---|---|
Parameter | Description |
IN PKDPC Dpc | DPC |
IN PVOID Context | DeferredContext parameter given to KeInitializeDpc |
IN PVOID SystemArg1 | SystemArgument1 passed to KeInsertQueueDpc (NULL for custom timers) |
IN PVOID SystemArg2 | SystemArgument2 passed to KeInsertQueueDpc (NULL for custom timers) |
Two different types of timer can be used. A basic timer is called once every second; WdmIo uses this to detect device time-outs. Custom timers may be set up with resolutions starting from 100ns.
The kernel provides easy access to a device timer that calls you back every second. The tinier must be initialized at PASSIVE_LEVEL IRQL using IoInitializeTimer. WdmIo calls IoInitializeTimer as follows in its AddDevice routine just after the FDO has been created. The final parameter to IoInitializeTimer is passed to the timer callback.
status = IoInitializeTimer(fdo, (PIO_TIMER_ROUTINE)Timeout1s, dx);
if (!NT_SUCCESS(status)) {
IoDeleteDevice(fdo);
return status;
}
Use IoStartTimer to start timer calls and IoStopTimer to stop them. Do not call IoStopTimer from within your timer routine. The first timer call may occur after less than one second.
The timer routine is called at DISPATCH_LEVEL. You will usually need to use a Critical Section routine if you wish to coordinate your activities with interrupt handling routines.
Some drivers start their timer calls when a device is created and stop them when it is removed. WdmIo reduces the number of timer callbacks. It starts the timer whenever an interrupt driven I/O starts. When the transfer is completed, it sets a StopTimer flag to indicate that its timer should be stopped. The next call to WdmIoStartIo checks this flag and calls IoStopTimer, if necessary. The timer is also stopped when the device is removed.
Listing 17.5 shows the WdmIo timer callback, Timeout1s, and the Critical Section routine that it uses, called Timeout1sSynch.
The device extension Timeout field serves two purposes. If –1, it indicates that no interrupt-driven transfer is in progress. The WdmIo code in DeviceIo.sss checks this value in several places to ensure that a read or write is indeed in progress. If Timeout is zero or more, it indicates the number of seconds left before the current transfer times out. The first timer callback may occur after less than one second, so the code that starts reads and writes adds one to the given time-out to be on the safe side.
Timeout1s, therefore, first checks whether there is a transfer in progress. If not (i.e., if Timeout is –1), it returns immediately. If the IRP Cancel flag is set, it calls the DPC routine directly. Otherwise, it calls Timeout1sSynch as a Critical Section routine. Timeout1sSynch checks and decrements Timeout. If Timeout has reached zero, the read or write must be stopped. Timeout is set to –1, the IRP return status is set appropriately and TRUE is returned.
If Timeout1sSynch returns TRUE, Timeout1s calls the DPC routine directly. This call to WdmIoDpcForIsr uses NULL as the Dpc parameter. This fact could be used in DPC processing, if necessary. For example, for IRP cancelling and time-outs, there is no need to provide a priority boost for the IRP.
Listing 17.5 WdmIo time-out routines
VOID Timeout1s(IN PDEVICE_OBJECT fdo, IN PWDMIO_DEVICE_EXTENSION dx) {
if (dx->Timeout==-1) return;
DebugPrint("Timeout1s: Timeout is %d" ,dx->Timeout);
PIRP Irp = fdo->CurrentIrp;
if (Irp->Cancel ||
KeSynchronizeExecution(dx->InterruptObject, (PKSYNCHRONIZE_ROUTINE)Timeout1sSynch, dx))
WdmIoDpcForIsr(NULL, fdo, fdo->CurrentIrp, dx);
}
static BOOLEAN Timeout1sSync(IN PWDMIO_DEVICE_EXTENSION dx) {
if (dx->Timeout==-1 || –dx->Timeout>0) return FALSE;
dx->Timeout = –2;
dx->TxStatus = STATUS_NO_MEDIA_IN_DEVICE; // Win32: ERROR_NOT_READY
return TRUE;
}
Custom timers may be used if you want timer resolutions other than one second. You can detect when the timer goes off in two ways. Either use a Custom DPC callback, or wait for the timer object to become signalled.
NT 3.51 timers are one-shot only. NT 4, W2000, and WDM drivers can use periodic timers. Declare a KTIMER field in nonpaged memory (e.g., in your device extension), and initialize it with KeInitializeTimer or KeInitializeTimerEx. To start a one-shot timer, call KeSetTimer. If the DueTime LARGE_INTEGER parameter is positive, it represents an absolute time. If it is negative, it is a relative time in units of 100ns.
Use KeSetTimerEx if you want to specify a periodic timer. The DueTime parameter is used to specify the first time-out. The Period parameter specifies the period in milliseconds for subsequent time-outs.
You can cancel a timer using KeCancelTimer, and use KeReadStateTimer to find out if the timer has gone off.
A custom DPC routine may used as the timer callback. Initialize a custom DPC as described previously. Pass the KDPC pointer to KeSetTimer or KeSetTimerEx.
System threads can wait for a timer to go off using the KeWaitForSingleObject and KeWaitForMultipleObjects calls. KeInitializeTimerEx lets you specify whether a timer is a NotificationTimer or a SynchronizationTimer. A SynchronizationTimer timer releases only one waiting thread, while a NotificationTimer timer releases all waiting threads.
This chapter has looked at how to write interrupt service routines. Any nonessential processing is best done in a Deferred Procedure Call (DPC) routine at a lower IRQL. A basic one-second interval timer can be used to detect device time-outs. Custom timers can be used with Custom DPCs to receive notification of other time-out periods.
Note that the interrupt level is not the same as the IRQ number. IRQ7 has interrupt level 20.
Actually, the commands could take a long time to run if the Win32 application includes large delay commands. However, moving the command processing to the DPC would not help, as a Critical section routine would have to be run, bringing the IRQL back up the DIRQL.