52964.fb2 Writing Windows WDM Device Drivers - читать онлайн бесплатно полную версию книги . Страница 17

Writing Windows WDM Device Drivers - читать онлайн бесплатно полную версию книги . Страница 17

Chapter 16Hardware I/O IRP Queuing

This chapter starts looking at the WdmIo and PHDIo drivers. Although the text only refers to the WdmIo driver, the techniques used in PHDIo are identical.

Any driver that talks to a real device will need to control access to the hardware. Chapter 9 mentioned that Critical section routines should be used to run routines that cannot be interrupted. Critical section routines are now used for real.

However, the main new requirement for a driver that accesses hardware is that it serializes all the IRPs that it processes. If more than one Win32 process has opened a device and is issuing IRPs, each IRP must not try to talk to the hardware at the same time[36]. If the IRPs can be processed very quickly, critical sections should be used. However, in most cases, the relevant IRPs need to be put into a queue and processed serially, one by one.

Processing IRPs serially will, of course, reduce performance. If jobs can be carried out in parallel, try to do it that way.

You may wish to impose further strictures on users of your device. You could insist that a certain IOCTL be used just before a read request. Although it is best to keep these restrictions to a minimum, do put in any relevant checks. You could use the FileObject pointer on the IRP stack to ensure that it is the same application that is issuing the IOCTL and read requests.

Processing IRPs serially is such a common requirement that the Windows Driver Model includes special support for this technique. Each device object contains a device queue of IRPs. The standard StartIo driver callback is used to process the queued IRPs one at a time. The device object and IRP structures have fields that are used by the I/O Manager to manage the queue, and cancel and cleanup queued IRPs safely.

This chapter, therefore, looks at the device IRP queue, StartIo routines, and how to cancel and cleanup IRPs. The next chapter looks at interrupt handling and all its associated topics.

Figure 16.1 illustrates the queuing of two Read IRPs and one IOCTL IRP. The main Read and IOCTL dispatch routines perform some initial checking of the IRPs before they are queued for processing by the StartIo routine. The initial processing of IRPs 2 and 3 can happen simultaneously. However, the StartIo routine definitely processes the IRPs serially, one after the other.

Figure 16.1 IRP queuing

Hardware Access

Before I start in earnest, note that the WdmIo driver is a standard WDM driver in a device stack. It is layered over the Unknown bus driver. It does not use the Unknown driver to access hardware. Instead, it talks to hardware directly.

The StartDevice, RetrieveResources, and StopDevice routines in DeviceIo.cpp have been altered slightly from their originals in Wdm2. They insist that one port or memory resource be allocated. If necessary a memory-mapped set of registers is mapped into memory, and unmapped when the device is stopped. Similarly, if WdmIo has connected to an interrupt, StopDevice disconnects and StartDevice connects again.

The WriteByte and ReadByte routines are shown in Listing 16.1. They both fail silently if the register offset is out of range. The DebugPrint trace calls should not be used in these routines, as they may be called at device IRQL (DIRQL) (i.e., above the maximum IRQL suitable for DebugPrint calls, DISPATCH_LEVEL).

If you need to delay for a short period, use the KeStallExecutionProcessor routine, specifying the stall period in microseconds. The DDK recommends that you keep the delay as short as possible, and definitely no more than 50µs. Longer delays usually mean writing code in a very different way. For example, you could use custom timers, as described in the next chapter. Alternatively, a system thread could use the KeDelayExecutionThread function for longer delays.

Listing 16.1 WriteByte and ReadByte

void WriteByte(IN PWDMIO_DEVICE_EXTENSION dx, IN ULONG offset, IN UCHAR byte) {

 if (offset>=dx->PortLength) return;

 PUCHAR Port = dx->PortBase+offset;

 if (dx->PortInIOSpace) WRITE_PORT_UCHAR(Port, byte);

 else WRITE_REGISTER_UCHAR(Port, byte);

}

UCHAR ReadByte(IN PWDMIO_DEVICE_EXTENSION dx, IN ULONG offset) {

 if (offset>=dx->PortLength) return 0;

 PUCHAR Port = dx->PortBase+offset;

 UCHAR b;

 if (dx->PortInIOSpace) b = READ_PORT_UCHAR(Port);

 else b = READ_REGISTER_UCHAR(Port);

 return b;

}

Finally, note that the WdmIo driver read and write dispatch routines and the IOCTLs all use Buffered I/O.

IRP Queuing

Device Queues

The DebugPrint in Chapter 14 used doubly-linked lists as a means of storing blocks of memory for later processing. The WdmIo driver uses the built-in IRP device queue to serialize the processing of its main IRPs. A device queue is a doubly-linked list with special features that tailor it for IRP processing. One of its special features is that it has a built-in spin lock. Thus, a driver does not need to provide its own spin lock to guard access to the queue, as DebugPrint had to do for its doubly-linked lists.

The WdmIo Functional Device Object (FDO) contains a device queue field that stores the queue's linked list head. Each IRP also contains a linked list entry that is used to store the pointers to its linked IRPs. For the moment, the names of these fields do not matter, as most ordinary queue actions are handled internally by the I/O Manager. However, to cleanup the device queue, these entries must be manipulated directly. The I/O Manager also initializes the device queue.

Inserting IRPs into the device queue is actually fairly straightforward. This except from the Read IRP dispatch handler in Dispatch.cpp shows the necessary steps. First, the IRP must be marked as pending using IoMarkIrpPending. Then, IoStartPacket is called to insert the IRP into the queue. If there are no queued IRPs, the IRP is sent for processing in the StartIo routine straightaway. Finally, the Read IRP returns STATUS_PENDING to confirm that the IRP is indeed pending.

IoMarkIrpPending(Irp);

IoStartPacket(fdo, Irp, 0, WdmIoCancelIrp);

return STATUS_PENDING;

The call to IoStartPacket passes a pointer to the cancel routine. I shall show later how this works. The third parameter to IoStartPacket is a key that can be used to sort the IRPs in the device queue. This feature is not used by WdmIo, so the key is always zero.

The WdmIo driver still uses the Plug and Play device locking technique to ensure that IRP processing is not interrupted by Plug and Play stop device requests. Note that the UnlockDevice routine is not called when an IRP is queued. This is deliberate, as the IRP has not been completed. I ensure that UnlockDevice is called on all paths that will later complete the IRP.

The main Read, Write, and IOCTL dispatch routines in WdmIo perform device locking and parameter checking. All valid IRPs of these types are put in the device queue for processing later serially in the WdmIo StartIo routine.

StartIo Routines

The WdmIoStartIo routine shown in Listing 16.2 processes IRPs for each WdmIo device one by one[37]. All queued IRPs, whatever their major function code, come through this same routine. Therefore, StartIo routines usually have a big switch statement at their heart. Listing 16.2 shows the complete code for IOCTL IRPs with major function code IRP_MJ_DEVICE_CONTROL However, the code for Read and Write IRPs is not shown for now. This is explained in the next chapter.

When an IRP is passed to the StartIo routine, it has just been removed from the device queue. The I/O Manager has also put the IRP pointer in the FDO CurrentIrp field. This is particularly useful for interrupt handlers as will be seen in the chapter, but is also used by IRP cancel and cleanup routines. The IRP Cancel field is set to TRUE when the IRP is cancelled. The IRP Cancel field may even be set before StartIo is called. Both WdmIoStartIo and the interrupt handling routines check the Cancel field at various points, as shown later.

StartIo routines are always called at DISPATCH_LEVEL IRQL. This means that all the code and the variables it accesses must be in nonpaged memory. It restricts the set of kernel calls that can be made. However, calls to the DebugPrint trace output routines can be made safely. WdmIoStartIo processes IRPs in two different ways. All the IOCTL IRPs are handled straightaway; the IRP is processed in the relevant way and is completed at the end of WdmIoStartIo. However, Read and Write IRPs use interrupt driven I/O to transfer one byte at a time. These IRPs are usually completed later, as described in the next chapter. In this case, WdmIoStartIo simply returns; the current IRP is still pending and is still being processed.

WdmIoStartIo is only called to process another IRP in the device queue when IoStartNextPacket is called (or IoStartNextPacketByKey). For IRPs that are processed entirely by WdmIoStartIo, IoStartNextPacket is called after the IRP is completed[38]. The TRUE second parameter to IoStartNextPacket indicates that cancel routines are being used.

WdmIoStartIo begins by zeroing the CmdOutputCount field in the device extension. This field stores the count of bytes transferred during immediate IRP processing. CmdOutputCount is passed to CompleteIrp at the end of WdmIoStartIo.

WdmIoStartIo then stops the device timer if the device extension StopTimer field is set to true. This timer is used to detect read and write time-outs and its use is described in the next chapter.

WdmIoStartIo now contains the obligatory huge switch statement, switching on the IRP stack major function code. As stated earlier, only IOCTL, read, and write requests should reach WdmIoStartIo. The IOCTL handler has a subsidiary large switch statement, this time switching on the IOCTL control code. All the IOCTLs are processed straightaway and eventually fall through to complete the IRP at the end of WdmIoStartIo.

WdmIoStartIo ends by completing IRPs that have finished their processing. The Cancel flag is checked and the cancel routine is removed. Finally, the device is unlocked using UnlockDevice, the IRP is completed, and IoStartNextPacket is called.

Listing 16.2 WdmIoStartIo routine

VOID WdmIoStartIo(IN PDEVICE_OBJECT fdo, IN PIRP Irp) {

 PWDMO_DEVICE_EXTENSION dx = (PWDMIO_DEVICE_EXTENSION)fdo->DeviceExtension;

 PIO_STACK_LOCATION IrpStack = IoGetCurrentIrpStackLocationIrp);

 PUCHAR Buffer = (PUCHAR)Irp->AssociatedIrp.SystemBuffer;

 // Zero the output count

 dx->CmdOutputCount = 0;

 DebugPrint("WdmIoStartIo: %I", Irp);

 // Stop the 1 second timer if necessary

 if (dx->StopTimer) {

  IoStopTimer(fdo);

  dx->StopTimer = false;

 }

 NTSTATUS status = STATUS_SUCCESS;

 // Switch on the IRP major function code

 switch(IrpStack->MajorFunction) {

 //////////////////////////////////////////////////////////////////////

 case IRP_MJ_DEVICE_CONTROL:

  {

   ULONG ControlCode = IrpStack->Parameters.DeviceIoControl.IoControlCode;

   ULONG InputLength = IrpStack->Parameters.DeviceIoControl.InputBufferLength;

   ULONG OutputLength = IrpStack->Parameters.DeviceIoControl.OutputBufferLength;

   switch(ControlCode) {

   ////////////////////////////////////////////////////////////////////

   case IOCTL_PHDIO_RUN_CMDS:

    DebugPrint( "WdmIoStartIo: Run Cmds %s", dx->ConnectedToInterrupt?"(synchronised)":"");

    // Run the commands, synchronized with interrupt if necessary

    if (dx->ConnectedToInterrupt) {

     if (!KeSynchronizeExecution(dx->InterruptObject, (PKSYNCHRONIZE_ROUTINE)RunCmdsSynch, (PVOID)fdo))

      status = STATUS_UNSUCCESSFUL;

    } else if (!RunCmds(fdo, true)) status = STATUS_UNSUCCESSFUL;

    break;

   ////////////////////////////////////////////////////////////////////

   case IOCTL_PHDIO_CMDS_FOR_READ:

    DebugPrintMsg( "WdmIoStartIo: Store cmds for read");

    status = StoreCmds(&dx->ReadCmds, &dx->ReadCmdsLen, InputLength, Buffer);

    break;

   ////////////////////////////////////////////////////////////////////

   case IOCTL_PHDIO_CMDS_FOR_READ_START:

    DebugPrintMsg("WdmIoStartIo: Store cmds for read start");

    status = StoreCmds(&dx->StartReadCmds, &dx->StartReadCmdsLen, InputLength, Buffer);

    break;

   ////////////////////////////////////////////////////////////////////

   case IOCTL_PHDIO_CMDS_FOR_WRITE:

    DebugPrintMsg("WdmIoStartIo: Store cmds for write");

    status = StoreCmds(&dx->WriteCmds, &dx->WriteCmdsLen, InputLength, Buffer);

    break;

   ////////////////////////////////////////////////////////////////////

   case IOCTL_PHDIO_GET_RW_RESULTS:

    // Copy cmd output first

    dx->CmdOutputCount = dx->TxCmdOutputCount;

    if (dx->CmdOutputCount>OutputLength) dx->CmdOutputCount = OutputLength;

    RtlCopyMemory(Buffer, dx->TxResult, dx->CmdOutputCount);

    // Then add on last interrupt reg value

    if (dx->CmdOutputCount+1<=OutputLength) Buffer[dx->CmdOutputCount++] = dx->TxLastIntReg;

    DebugPrint("WdmIoStartIo: Get RW Results: %d bytes", dx->CmdOutputCount);

    break;

   ////////////////////////////////////////////////////////////////////

   default:

    status = STATUS_NOT_SUPPORTED;

   }

   break;

  }

 //////////////////////////////////////////////////////////////////////

 case IRP_MJ_WRITE:

  // …

 case IRP_MJ_READ:

  // …

 default:

  status = STATUS_NOT_SUPPORTED;

  break;

 }

 //////////////////////////////////////////////////////////////////////

 // Complete this IRP

 if (Irp->Cancel) status = STATUS_CANCELLED;

 // Remove cancel routine KIRQL OldIrql;

 IoAcquireCancelSpinLock(&OldIrql);

 IoSetCancelRoutine(Irp, NULL);

 IoReleaseCancelSpinLock(OldIrql);

 // Unlock device, complete IRP and start next UnlockDevice(dx);

 DebugPrint("WdmIoStartIo: CmdOutputCount %d", dx->CmdOutputCount);

 CompleteIrp(Irp, status, dx->CmdOutputCount);

 IoStartNextPacket(fdo, TRUE);

}

Processing Commands

Critical Sections

The IOCTL_PHDIO_RUN_CMDS IOCTL is used to run a set of commands straightaway. Eventually the ProcessCmds routine is run to process the commands. ProcessCmds is fairly straightforward and so is not listed here. It is in DeviceIo.cpp on the book CD-ROM. Suffice to say, it is passed parameters to its input and output buffers, together with their length. It also has a CanTrace bool parameter that dictates whether its DebugPrint trace statements can be run safely.

However, there are a couple of hurdles to overcome before IOCTL_PHDIO_RUN_CMDS can call ProcessCmds.

The first hurdle is that the IOCTL input and output buffers use the same block of memory. While this is a jolly useful technique for saving memory in the first stage of IOCTL processing, it means that some nonpaged memory must be allocated for the output data. The output data is copied back to the shared buffer and the temporary memory freed. The RunCmds routine performs this task[39].

If WdmIo has not connected to a hardware interrupt, the IOCTL_PHDICLRUN_CMDS handler in Listing 16.2 simply calls RunCmds. However, if the device extension ConnectedToInterrupt field is true, an interrupt may occur that could scupper any command processing. This problem is overcome by calling RunCmds in the context of a Critical Section. To recap, a Critical Section routine runs at Device IRQL (DIRQL) and so cannot be interrupted (by our interrupt at least).

In this case, the IOCTL_PHDIO_RUN_CMDS handler calls KeSynchronizeExecution to run the RunCmdsSynch routine as a Critical Section. RunCmdsSynch just calls RunCmds with the CanTrace parameter set to false. Both RunCmdsSynch and RunCmds return FALSE if they can not allocate enough memory for the output buffer.

Listing 16.3 RunCmdsSynch and RunCmds

BOOLEAN RunCradsSynch(IN PDEVICE_OBJECT fdo) {

 return RunCmds(fdo, false);

}

BOOLEAN RunCmds(IN PDEVICE_OBJECT fdo, IN bool CanTrace) {

 PWDMIO_DEVICE_EXTENSION dx = (PWDMIO_DEVICE_EXTENSION)fdo->DeviceExtension;

 PIRP Irp = fdo->CurrentIrp;

 PIO_STACK_LOCATION IrpStack = IoGetCurrentIrpStackLocation(Irp);

 ULONG InputLength = IrpStack->Parameters.DeviceIoControl.InputBufferLength;

 ULONG OutputLength = IrpStack->Parameters.DeviceIoControl.OutputBufferLength;

 PUCHAR Buffer = (PUCHAR)Irp->AssociatedIrp.SystemBuffer;

 PUCHAR OutBuffer = NULL;

 if (OutputLength>0) {

  OutBuffer = (PUCHAR)ExAllocatePool(NonPagedPool.OutputLength);

  if (OutBuffer==NULL) return FALSE;

 }

 ProcessCmds(dx, Buffer, InputLength, OutBuffer, OutputLength, CanTrace);

 if (OutBuffer!=NULL) {

  RtlMoveMemory(Buffer, OutBuffer, OutputLength);

  ExFreePool(OutBuffer);

 }

 return TRUE;

}

Cancelling Queued IRPs

Chapter 14 explained when Windows tries to cancel IRPs using IRP cancel routines. In addition, Windows issues the Cleanup IRP to cancel IRPs in some circumstances. Chapter 14 also showed how the DebugPrint driver provides a cancel routine for its one queued IRP.

As WdmIo queues IRP, it must also provide cancel routines for these IRPs. In addition, it handles the Cleanup IRP correctly so that IRPs are cancelled if a user mode application closes its file handle with overlapped I/O requests pending.

Queued IRP Cancelling

When considering a strategy for cancelling IRPs, two cases must usually be considered. In the first case, the IRP is still being held in the device queue. The second case is when the IRP has been removed from the device queue and is being processed by StartIo.

The I/O Manager does not know whether an IRP is in the device queue[40]. It simply calls the cancel routine. The cancel routine must determine what to do. If the IRP pointer matches the FDO CurrentIrp field, the IRP is running in StartIo (or in the process of a transfer started by StartIo). Otherwise, the cancel routine must try to remove the IRP from the device queue.

The I/O Manager uses its Cancel spin lock to guard cancelling operations. An IRP's cancel routine is called at DISPATCH_LEVEL IRQL while holding the Cancel spin lock. The IRP CancelIrql field holds the old IRQL that should be passed to IoReleaseCancelSpinLock before the IRP is completed and the cancel routine exits.

I have already mentioned that a device queue includes a spin lock to ensure that all operations on the queue are handled safely in a multiprocessor environment. When cancel routines are involved, the Cancel spin lock must be held. This is to ensure that a Cancel routine is not called on one processor while the IRP is being dequeued on another processor.

WdmIo IRP Cancelling Strategy

The WdmIo driver cancels a queued IRP by removing it from the device queue and completing it with status STATLJS_CANCELLED.

If the IRP is being processed by StartIo, the cancel routine in effect does nothing. Before the cancel routine is called, the I/O Manager sets the IRP's Cancel flag. The code called by WdmIoStartIo checks this Cancel flag every now and then. If it is found to be set, the current operation is abandoned and the IRP is completed with status STATUS_CANCELLED.

The DDK documentation does not say that the Cancel routine has to complete the IRP. In WdmIo, if the IRP is being processed by StartIo, the Cancel routine does not cancel the IRP. The IRP is only cancelled later. This strategy seems to work. See the following section for an alternative technique.

Listing 16.4 shows the WdmIoCancelIrp routine. If the IRP to be cancelled matched the FDO CurrentIrp field, the IRP is being processed by StartIo (or its interrupt driven follow on code). In this case, all WdmIoCancelIrp does is to release the Cancel spin lock and exit.

If the IRP to be cancelled is not the current IRP, KeRemoveEntryDeviceQueue is called to try to remove the IRP from the device queue. The FDO DeviceQueue field holds the device queue list head. The IRP Tail.Overlay.DeviceQueueEntry field holds the list entry. The Cancel spin lock can now be released safely. If the IRP was removed from the queue, UnlockDevice is called and the IRP is completed with status STATUS_CANCELLED.

Listing 16.4 WdmIoCancelIrp routine

VOID WdmIoCancelIrp(IN PDEVICE_OBJECT fdo, IN PIRP Irp) {

 PWDMIO_DEVICE_EXTENSION dx = (PWDMIO_DEVICE_EXTENSION)fdo->DeviceExtension;

 DebugPrint("WdmIoCancelIrp: Cancelling %I", Irp);

 if (Irp==fdo->CurrentIrp) {

  DebugPrintMsg("WdmIoCancelIrp: IRP running in StartIo");

  // IRP is being processed by WdmIoStartIo.

  // Irp->Cancel flag already set.

  // WdmIoStartIo or timeout will detect Cancel flag

  // and cancel IRP in due course

  IoReleaseCancelSpinLock(Irp->CancelIrql);

 } else {

  DebugPrintMsg("WdmIoCancelIrp: IRP in StartIo queue");

  // IRP is still in StartIo device queue.

  // Just dequeue and cancel it. No need to start next IRP.

  BOOLEAN dequeued = KeRemoveEntryDeviceQueue(&fdo->DeviceQueue, &Irp->Tail.Overlay.DeviceQueueEntry);

  IoReleaseCancelSpinLock(Irp->CancelIrql);

  if (dequeued) {

   UnlockDevice(dx);

   CompleteIrp(Irp, STATUS_CANCELLED);

  }

 }

}

Cancel Checking

The code in DeviceIo.cpp makes various checks to see if the I/O Manager has requested that the IRP be cancelled.

At the end of WdmIoStartIo, as shown in Listing 16.2, the status is set to cancelled if the IRP Cancel field has been set. The code at the end of WdmIoStartIo also removes the cancel routine before completing the IRP. It acquires the I/O Manager Cancel spin lock before calling IoSetCancelRoutine with NULL for the cancel routine parameter. Remove a cancel routine before completing an IRP.

The ProcessCmds routine also checks the IRP Cancel field just before it gets the next command. If it is set, processing stops with error code PHDIO_CANCELLED in the output buffer and the IRP is cancelled.

The interrupt handling code also checks the IRP Cancel field, as shown in the next chapter.

Alternative Cancel Strategy

An alternative IRP cancelling strategy is to remove an IRP's cancel routine as soon as it starts being processed in StartIo. The advantage is that the cancel routine can complete the IRP straightaway. The downside is that no cancel routine is available while the IRP is being processed by StartIo or its follow on code.

The IRP cancel routine is changed for the case when the IRP is the current IRP. Listing 16.5 shows that in this case, the IRP is completed and IoStartNextPacket called.

Listing 16.5 Alternative IRP cancel routine

if (Irp==fdo->CurrentIrp) {

 DebugPrintMsg("WdmIoCancelIrp: IRP just dequeued for StartIo");

 // IRP has just been dequeued but StartIo has not had a chance

 // to remove this cancel routine yet. Irp->Cancel flag already set.

 IoReleaseCancelSpinLock(Irp->CancelIrql);

 // Cancel IRP and start next one

 CompleteIrp(Irp, STATUS_CANCELLED);

 IoStartNextPacket(fdo, TRUE);

} else

 …

The start of the StartIo routine must also be changed. The main job is to remove the cancel routine using IoSetCancelRoutine. However, there is a small chance that the IRP has already been cancelled. The code in Listing 16.6 checks the IRP Cancel field first. If it has been cancelled, StartIo simply exits.

In my mind, this technique has a potential race condition. When the Cancel routine completes, the IRP memory may disappear straightaway. However, the StartIo routine may be just about to run on another processor. Accessing the IRP Cancel field might, therefore, cause an access violation (or refer to the next IRP to use this IRP structure). Moving the CompleteIrp and IoStartNextPacket calls into the StartIo routine (if Irp->Cancel is set) would solve this problem, but it would mean that the cancel routine does not complete the IRP.

Listing 16.6 Alternative StartIo initial processing

// Check whether cancelled already KIRQL OldIrql;

IoAcquireCancelSpinLock(&OldIrql);

if (Irp->Cancel) {

 IoReleaseCancelSpinLock(OldIrql);

 // IoStartNextPacket called by cancel routine

 return;

}

// Remove cancel routine

IoSetCancelRoutine(Irp, NULL);

IoReleaseCancelSpinLock(OldIrql);

Cleanup IRP Handling

The Cleanup IRP is issued to cancel any IRPs that are outstanding when a file handle is closed. Even if the IRPs have cancel routines, they are not called.

To handle it correctly, the driver ought to cancel only IRPs that belong to the correct file handle. Only queued IRPs whose FileObject field matches the Cleanup IRP's stack FileObject field should be cancelled.

The WdmIo Cleanup IRP handler, WdmIoDispatchCleanup, shown in Listing 16.7, does not perform this FileObject check. This makes the code simpler.

WdmIoDispatchCleanup must hold the Cancel spin lock wherever it accesses the device queue. However, you must not be holding the Cancel spin lock when an IRP is completed.

The code keeps extracting IRPs using KeRemoveDeviceQueue until the device queue is empty. A pointer to the IRP is found using the usual CONTAINING_RECORD machinations. The IRP is marked for cancelling and its cancel routine is removed. The Cancel spin lock is released before UnlockDevice is called and the IRP is completed with status STATUS_CANCELLED.

The Cleanup IRP handler then goes on to try to cancel the IRP currently being processed by WdmIoStartIo or its follow-on code. CancelCurrentIrpSynch is called as a Critical Section routine. This returns TRUE if a transfer is in progress (i.e., if the device extension Timeout field is greater than or equal to zero). If a transfer is in progress, WdmIoDpcForIsr is called to complete the IRP with status STATUS_CANCELLED. The Timeout field and WdmIoDpcForIsr are described in the next chapter.

To handle the FileObject check correctly, you must still remove each IRP from the queue in turn. If the IRP's FileObject does not match the Cleanup FileObject, the IRP must be put in a temporary holding queue. At the end, all these IRPs must be reinserted in the main device queue.

Listing 16.7 WdmIo Cleanup IRP handling

NTSTATUS WdmIoDispatchCleanup(IN PDEVICE_OBJECT fdo, IN PIRP Irp) {

 PWDMIO_DEVICE_EXTENSION dx = (PWDMIO_DEVICE_EXTENSION)fdo->DeviceExtension;

 DebugPrintMsg("WdmIoDispatchCleanup");

 KIRQL OldIrql;

 IoAcquireCancelSpinLock(&OldIrql);

 // Cancel all IRPs in the I/O Manager maintained queue in device object

 PKDEVICE_QUEUE_ENTRY QueueEntry;

 while((QueueEntry=KeRemoveDeviceQueue(&fdo->DeviceQueue)) != NULL) {

  PIRP CancelIrp = CONTAINING_RECORD(QueueEntry, IRP, Tail.Overlay.DeviceQueueEntry);

  CancelIrp->Cancel = TRUE;

  CancelIrp->CancelIrql = OldIrql;

  CancelIrp->CancelRoutine = NULL;

  IoReleaseCancelSpinLock(OldIrql);

  DebugPrint("WdmIoDispatchCleanup: Cancelling %I", CancelIrp);

  UnlockDevice(dx);

  CompleteIrp(CancelIrp, STATUS_CANCELLED);

  IoAcquireCancelSpinLock(&OldIrql);

 }

 IoReleaseCancelSpinLock(OldIrql);

 // Forceably cancel any in-progress IRP

 if (dx->Timeout!=-1) {

  if (KeSynchronizeExecution(dx->InterruptObject, (PKSYNCHRONIZE_ROUTINE)CancelCurrentIrpSynch, dx)) {

   if (fdo->CurrentIrp!=NULL) {

    DebugPrint("WdmIoDispatchCleanup: Cancelled in-progress IRP %I", fdo->CurrentIrp);

    WdmIoDpcForIsr(NULL, fdo, fdo->CurrentIrp, dx);

   }

  }

 }

 return CompleteIrp(Irp, STATUS_SUCCESS);

}

static BOOLEAN CancelCurrentIrpSynch(IN PWDMIO_DEVICE_EXTENSION dx) {

 if (dx->Timeout==-l) return FALSE;

 dx->Timeout = –1;

 dx->TxStatus = STATUS_CANCELLED;

 return TRUE;

}

Testing, Cancelling, and Cleanup

I amended WdmIoTest so that I could test the WdmIo driver's IRP cancelling and Cleanup IRP handling.

The WdmIoCancel application is substantially the same as that of WdmIoTest and is contained in the book software WdmIo\Cancel directory. You will have to uncomment some of the code and recompile to undertake some of the tests. The PHDIoCancel application does similar tests for the PHDIo driver.

The tests are designed to be run with the printer switched off, so that Write IRPs do not complete straightaway. WdmIoCancel opens the WdmIo device for overlapped I/O, using the GetDeviceViaInterface01 function. It then issues two overlapped Write requests and does not wait for either of them to complete. It then exits straightaway. This behavior is designed to test the IRP cancelling.

Figure 16.2 shows the DebugPrint output that demonstrates that IRP cancelling works correctly. At points (1) in the diagram, one of the IOCTL IRPs has been issued. They are passed to WdmIoStartIo, which processes them immediately. At point (2), the first Write IRP is issued. At point (3), WdmIoStartIo starts processing this first Write. At point (4), the second Write IRP is issued. As the first Write is still in progress (the printer being off), this second Write IRP is queued.

Point (5) is where it gets interesting. WdmIoCancel has just exited. The I/O Manager tries to cancel all pending IRPs. It calls the cancel routine of the second Write. WdmIoCancelIrp finds that it is not the current IRP. It therefore removes it from the StartIo queue and completes the IRP with a cancelled status.

The I/O Manager then calls the cancel routine of the first Write IRP at point (6). WdmIoCancelIrp finds that this IRP is the current IRP. In this design, WdmIoCancelIrp simply exits. In due course, the Timeout1s time-out routine runs. Timeout1s finds that the IRP Cancel flag is set. The WdmIoDpcForIsr routine is called to complete the IRP with a cancelled status.

Finally, the Cleanup IRP is issued at point (7). However, this finds that it has no work to do as the device queue is empty and there is no interrupt driven I/O in progress.

If you look carefully at the DebugPrint output, you will notice that the IRP pointer for the first four IRPs issued is the same, 0xC8548180. The I/O Manager is obviously reusing the IRP structures from its pool of available IRPs. The second write needs another IRP structure, which is at 0xC17C3E00.

Figure 16.2 Cleanup handling DebugPrint output

If you remove the comments around the CloseHandle call at the end of WdmIoCancel, you will be able to test the Cleanup IRP handling correctly. In this case, the IRP cancel routines will not be called. Instead, the Cleanup IRP handler removes the second Write IRP from the device queue. It then goes on to cancel the current IRP (i.e., the first Write).

Finally, the WdmIoCancel code contains a commented out CancelIO call after the second Write is issued. Uncomment this and recompile to check that this cancels the Write IRPs correctly.

Supplemental Device Queues

You can set up your own device queues, if need be. They are usually termed supplemental device queues to differentiate them from the standard device queue for StartIo requests. Supplemental device queues might be used in the following situations.

1. A full duplex driver will need a separate queue of IRPs for each direction of transfer. This lets a Read IRP and a Write IRP be processed at the same time. Interrupt routines need to be designed carefully to ensure that interrupts are handled correctly. The standard device queue could be used for Write IRPs. A supplemental device queue could then be used for the Read IRPs.

2. The I/O Manager Cancel spin lock must be acquired whenever there is an operation on any standard device queue. This is a considerable bottleneck for the whole system. Supplemental device queues can be designed to operate with a much-reduced use of the Cancel spin lock. The example in the SRC\GENERAL\CANCEL directory of the Windows 2000 DDK shows this technique in action.

3. When a Plug and Play device is stopped for resource reassignment, all I/O IRPs should be queued[41]. The IRPs should be run when the device is restarted with new resource assignments. All the Plug and Play drivers in this book avoid this complication by not letting a device be stopped if there are any open handles. These IRPs can be queued in the standard device queue. However, you may deem that a supplemental device queue is needed instead of, or in addition to, the standard device queue.

These queued IRPs may not need to be processed serially. When the device is started, all the IRPs are eligible for running straightaway. To achieve this, system worker thread work items may be scheduled to process all the IRPs. System worker threads are covered briefly in Chapter 14.

Do not forget that any queued IRPs need to be cancellable. Adding a queue is not necessarily a quick operation.

Implementing a Supplemental Device Queue

Let's see how to set up and use a supplemental device queue. This queue will perform exactly the same function as the kernel-managed device queue for StartIo requests and so shows what must be happening behind the scenes in the kernel IoStartPacket call, etc. The aim is to serialize processing of IRPs in the AbcStartIo2 function. An IRP is put in the queue for processing using the AbcStartPacket2 routine. AbcStartNextPacket2 starts the processing of the next packet. Listing 16.8 shows the code for these routines.

These routines also handle the cancelling of IRPs correctly using the Cancel spin lock and cancel routines.

Declare a KDEVICE_QUEUE field somewhere in nonpaged memory. In this case, it is in the device extension in a field called AbcIrpQueue. Initialize it when the device object has been created by calling KeInitializeDeviceQueue at PASSIVE_LEVEL

KeInitializeDeviceQueue(&dx->AbcIrpQueue);

The device extension also needs a current IRP pointer, here in a field called AbcCurrentIrp.

Inserting IRPs into the Queue

AbcStartPacket2 uses KeInsertDeviceQueue to insert the IRP into the device queue. Note that KeInsertDeviceQueue must be called at DISPATCH_LEVEL IRQL. The call to IoAcquireCancelSpinLock in AbcStartPacket2 does this. Subsequent calls to IoReleaseCancelSpinLock return to the old IRQL.

If the device queue is empty, KeInsertDeviceQueue returns FALSE, but it sets the queue into a busy state. Subsequent insertion attempts return TRUE. In AbcStartPacket2, if KeInsertDeviceQueue returns FALSE, then the IRP is passed for processing in AbcStartIo2 straightaway. First, the IRP pointer is stored as the current IRP in the AbcCurrentIrp field in the device extension. Then the Cancel spin lock is released, returning the IRQL to its old level. StartIo routines must run at DISPATCH_LEVEL, so calls to KeRaiseIrql and KeLowerIrql are put around the call to AbcStartIo2.

If KeInsertDeviceQueue returns TRUE, the IRP has been put in the device queue. In this case, no further processing is required in AbcStartPacket2, apart from releasing the Cancel spin lock.

IRP Processing

The AbcStartIo2 function initially does some IRP cancel checks. Then, it sets about processing the IRP. Eventually, it completes the IRP and calls AbcStartNextPacket2 to begin processing the next IRP. The call to AbcStartNextPacket2 can occur in a different routine (e.g., after a hardware interaction).

Starting the Next IRP

AbcStartNextPacket2's job is to see if there are any more IRPs queued for processing. If there are, it dequeues one and sends it for processing in AbcStartIo2. Note that the calls to AbcStartNextPacket2 and AbcStartIo2 are recursive, which I trust will not be a problem. AbcStartNextPacket2 must be called at DISPATCH_LEVEL IRQL.

AbcStartNextPacket2 calls KeRemoveDeviceQueue to try to remove an IRP from the device queue. KeRemoveDeviceQueue must be run at DISPATCH_LEVEL IRQL. When running an IRP device queue, it is usual to call KeRemoveDeviceQueue while holding the Cancel spin lock. The call to IoAcquireCancelSpinLock raises the IRQL to the correct level, as before.

If KeRemoveDeviceQueue returns NULL, no IRPs are queued. The AbcCurrentIrp field is reset to NULL and the Cancel spin lock is released.

If KeRemoveDeviceQueue returns non-NULL then the return value represents the device queue entry in the IRP structure. Get the actual IRP pointer using the CONTAINING_RECORD macro. Store this in AbcCurrentIrp, release the Cancel spin lock, and call AbcStartIo2.

Listing 16.8 Supplemental device queue routines

VOID AbcStartPacket2(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp) {

 PABC_DEVICE_EXTENSION dx = (PABC_DEVICE_EXTENSION)fdo->DeviceExtension;

 KIRQL OldIrql;

 IoAcquireCancelSpinLock(&OldIrql);

 IoSetCancelRoutine(Irp, AbcCancelIrp);

 if (KeInsertDeviceQueue(&dx->AbcIrpQueue, &Irp->Tail.Overlay.DeviceQueueEntry)) IoReleaseCancelSpinLock(OldIrql);

 else {

  DeviceExtension->AbcCurrentIrp = Irp;

  IoReleaseCancelSpinLock(OldIrql);

  KeRaiseIrql(DISPATCH_LEVEL, &OldIrql);

  AbcStartIo2(DeviceObject, Irp);

  KeLowerIrql(OldIrql);

 }

}

VOID AbcStartNextPacket2(IN PDEVICE_OBJECT DeviceObject) {

 PABC_DEVICE_EXTENSION dx = (PABC_DEVICE_EXTENSION)fdo->DeviceExtension;

 KIRQL OldIrql;

 IoAcquireCancelSpinLock(&OldIrql);

 PKDEVICE_QUEUE_ENTRY QueueEntry = KeRemoveDeviceQueue(&dx->AbcIrpQueue);

 if (QueueEntry!=NULL) {

  PIRP Irp = CONTAINING_RECORD(QueueEntry, IRP, Tail.Overlay.DeviceQueueEntry);

  dx->AbcCurrentIrp = Irp;

  IoReleaseCancelSpinLock(OldIrql);

  AbcStartIo2(DeviceObject, Irp);

 } else {

  dx->AbcCurrentIrp = NULL;

  IoReleaseCancelSpinLock(OldIrql);

 }

}

VOID AbcStartIo2(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp) {

 PABC_DEVICE_EXTENSION dx = (PABC_DEVICE_EXTENSION)fdo->DeviceExtension);

 PIO_STACK_LOCATION IrpStack = IoGetCurrentIrpStackLocation(Irp);

 KIRQL Oldlrql;

 IoAcquireCancelSpinLock(&OldIrql);

 if (Irp->Cancel) {

  IoReleaseCancelSpinLock(OldIrql);

  return;

 }

 IoSetCancelRoutine(Irp, NULL);

 IoReleaseCancelSpinLock(OldIrql);

 // Process and complete request

 // …

 // Start next packet

 AbcStartNextPacket2(DeviceObject);

}

IRP Cancelling and Cleanup

Cancelling IRPs in supplemental device queues can use exactly the same techniques as the standard device queue. In this example, the cancel routine is removed when AbcStartIo2 is called. As shown in Listing 16.9, if the IRP to cancel is the current IRP, it is completed with a cancelled status in AbcCancelIrp. When AbcStartIo2 starts, it checks the Cancel flag and removes the cancel routine.

The Cleanup IRP handling should use exactly the same techniques as the WdmIo driver. The WdmIo driver uses a Timeout variable to stop an IRP that is being processed. You will need to use some similar technique if any of your IRPs may take a long time to be processed.

Listing 16.9 Supplemental device queue IRP cancel routine

VOID AbcCancelIrp(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp) {

 PABC_DEVICE_EXTENSION dx = (PABC_DEVICE_EXTENSION)fdo->DeviceExtension;

 if (Irp=dx->AbcCurrentIrp) {

  IoReleaseCancelSpinLock(Irp->CancelIrq1);

  CompleteIrp(Irp, STATUS_CANCELLED);

  AbcStartNextPacket2(DeviceObject, TRUE);

 } else {

  KeRemoveEntryDeviceQueue(&dx->AbcIrpQueue, &Irp->Tail.Overlay.DeviceQueueEntry);

  IoReleaseCancelSpinLock(Irp->CancelIrql);

  CompleteIrp(Irp, STATUS_CANCELLED);

 }

}

Conclusion

This chapter has shown how to queue IRPs for serial processing in a driver's StartIo routine. It has then shown how to cope with the necessary evil of cancelling IRPs. Make sure you clean up any IRPs that are still not completed when the device handle is being closed. Use techniques similar to that used in the example WdmIoCancel application to test that cancelling and cleanup happen correctly.

The next chapter inspects the next part of the WdmIo and PHDIo drivers, how to handle interrupts and do interrupt-driven programmed I/O. It also looks at timers, both for IRP time-outs in the order of seconds, and custom timers for finer grain intervals.


  1. Or for that matter, an application must not fire off several overlapped requests at the same time.

  2. If there is more than one WdmIo device, each device has a device queue that can be processed simultaneously by WdmIoStartIo.

  3. Note that the IoStartNextPacket implementation will, in fact, call WdmIoStartIo recursively (if there is another IRP in the queue). In the worst case, this recursive technique could overflow the kernel stack. One of the DDK examples shows a way of avoiding this problem by not queuing an IRP if the queue is "full". 

  4. At the last moment, I have moved this code back into WdmIoStartIo. RunCmds may not run at DIRQL and it is incorrect to allocate nonpaged memory above DISPATCH_LEVEL. 

  5. In fact, the I/O Manager could work out if an IRP is in the device queue.

  6. You may also decide to queue IRPs while a device is asleep. The example drivers in this book opt to wake up the device when an I/O request arrives.