Книга: Writing Windows WDM Device Drivers

Cancelling Queued IRPs

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);

Оглавление книги


Генерация: 1.598. Запросов К БД/Cache: 3 / 1
поделиться
Вверх Вниз