Cancel-Safe IRP Queues

Drivers that implement their own IRP queuing should use the cancel-safe IRP queue framework. Cancel-safe IRP queues split IRP handling into two parts:

  1. The driver provides a set of callback routines that implement standard operations on the driver's IRP queue. The provided operations include inserting and removing IRPs from the queue, and locking and unlocking the queue. See Implementing the Cancel-Safe IRP Queue.

  2. Whenever the driver needs to actually insert or remove an IRP from the queue, it uses the system-provided IoCsqXxx routines. These routines handle all synchronization and IRP canceling logic for the driver.

Drivers that use cancel-safe IRP queues do not implement Cancel routines to support IRP cancellation.

The framework ensures that drivers insert and remove IRPs from their queue atomically. It also ensures that IRP cancellation is implemented correctly. Drivers that do not use the framework must manually lock and unlock the queue before performing any insertions and deletions. They must also avoid the race conditions that can result when implementing a Cancel routine. (For a description of the race conditions that can arise, see Synchronizing IRP Cancellation.)

The cancel-safe IRP queue framework is included with Windows XP and later versions of Windows. Drivers that must also work with Windows 2000 and Windows 98/Me can link to the Csq.lib library that is included in the Windows Driver Kit (WDK). The Csq.lib library provides an implementation of this framework.

The IoCsqXxx routines are declared in the Windows XP and later versions of Wdm.h and Ntddk.h. Drivers that must also work with Windows 2000 and Windows 98/Me must include Csq.h for the declarations.

You can see a complete demonstration of how to use cancel-safe IRP queues in the \src\general\cancel directory of the WDK. For more information about these queues, also see the Flow of Control for Cancel-Safe IRP Queuing white paper.

Implementing the Cancel-Safe IRP Queue

To implement a cancel-safe IRP queue, drivers must provide the following routines:

  • Either of the following routines to insert IRPs into the queue: CsqInsertIrp or CsqInsertIrpEx. CsqInsertIrpEx is an extended version of CsqInsertIrp; the queue is implemented using one or the other.

  • A CsqRemoveIrp routine that removes the specified IRP from the queue.

  • A CsqPeekNextIrp routine that returns a pointer to the next IRP following the specified IRP in the queue. This is where the system passes the PeekContext value that it receives from IoCsqRemoveNextIrp. The driver can interpret that value in any way.

  • Both of the following routines to allow the system to lock and unlock the IRP queue: CsqAcquireLock and CsqReleaseLock.

  • A CsqCompleteCanceledIrp routine that completes a canceled IRP.

Pointers to the driver's routines are stored in the IO_CSQ structure that describes the queue. The driver allocates the storage for the IO_CSQ structure. The IO_CSQ structure is guaranteed to remain a fixed size, so a driver can safely embed the structure inside its device extension.

The driver uses either IoCsqInitialize or IoCsqInitializeEx to initialize the structure. Use IoCsqInitialize if the queue implements CsqInsertIrp, or IoCsqInitializeEx if the queue implements CsqInsertIrpEx.

Drivers need only provide the essential functionality in each callback routine. For example, only the CsqAcquireLock and CsqReleaseLock routines implement lock handling. The system automatically calls these routines to lock and unlock the queue as necessary.

You can implement any type of IRP queuing mechanism in your driver, as long as the appropriate dispatch routines are provided. For example, the driver could implement the queue as a linked list, or as a priority queue.

CsqInsertIrpEx provides a more flexible interface to the queue than does CsqInsertIrp. The driver can use its return value to indicate the result of the operation; if it returns an error code, the insertion failed. A CsqInsertIrp routine does not return a value, so there is no simple way to indicate that an insertion failed. Also, CsqInsertIrpEx takes an additional driver-defined InsertContext parameter that can be used to specify additional driver-specific information to be used by the queue implementation.

Drivers can use CsqInsertIrpEx to implement more sophisticated IRP handling. For example, if there are no pending IRPs, the CsqInsertIrpEx routine can return an error code and the driver can process the IRP immediately. Similarly, if IRPs can no longer be queued, the CsqInsertIrpEx can return an error code to indicate that fact.

The driver is insulated from all IRP cancellation handling. The system provides a Cancel routine for IRPs in the queue. This routine calls CsqRemoveIrp to remove the IRP from the queue, and CsqCompleteCanceledIrp to complete the IRP cancellation.

The following diagram illustrates the flow of control for IRP cancellation.

diagram illustrating the flow of control for irp cancellation.

A basic implementation of CsqCompleteCanceledIrp is as follows.

VOID CsqCompleteCanceledIrp(PIO_CSQ Csq, PIRP Irp) {
  Irp->IoStatus.Status = STATUS_CANCELLED;
  Irp->IoStatus.Information = 0;

  IoCompleteRequest(Irp, IO_NO_INCREMENT);
}

Drivers can use any of the operating system's synchronization primitives to implement their CsqAcquireLock and CsqReleaseLock routines. Available synchronization primitives include spin locks and mutex objects.

Here is an example of how a driver can implement locking using spin locks.

/* 
  The driver has previously initialized the SpinLock variable with
  KeInitializeSpinLock.
 */

VOID CsqAcquireLock(PIO_CSQ IoCsq, PKIRQL PIrql)
{
    KeAcquireSpinLock(SpinLock, PIrql);
}

VOID CsqReleaseLock(PIO_CSQ IoCsq, KIRQL Irql)
{
    KeReleaseSpinLock(SpinLock, Irql);
}

The system passes a pointer to an IRQL variable to CsqAcquireLock and CsqReleaseLock. If the driver uses a spin lock to implement locking for the queue, the driver can use this variable to store the current IRQL when the queue is locked.

Drivers are not required to use spin locks. For example, the driver could use a mutex to lock the queue. For a description of the synchronization techniques that are available to drivers, see Synchronization Techniques.

Using the Cancel-Safe IRP Queue

Drivers use the following system routines when queuing and dequeuing IRPs:

The following diagram illustrates the flow of control for IoCsqRemoveNextIrp.

diagram illustrating the flow of control for iocsqremovenextirp.

The following diagram illustrates the flow of control for IoCsqRemoveIrp.

diagram illustrating the flow of control for iocsqremoveirp.

These routines, in turn, dispatch to driver-supplied routines.

The IoCsqInsertIrpEx routine provides access to the extended features of a CsqInsertIrpEx routine. It returns the status value that was returned by CsqInsertIrpEx. The caller can use this value to determine if the IRP was successfully queued or not. IoCsqInsertIrpEx also allows the caller to specify a value for the InsertContext parameter of CsqInsertIrpEx.

Note that both IoCsqInsertIrp and IoCsqInsertIrpEx can be called on any cancel-safe queue, whether the queue has a CsqInsertIrp routine or a CsqInsertIrpEx routine. IoCsqInsertIrp behaves the same in either case. If IoCsqInsertIrpEx is passed a queue that has a CsqInsertIrp routine, it behaves identically to IoCsqInsertIrp.

The following diagram illustrates the flow of control for IoCsqInsertIrp.

diagram illustrating the flow of control for iocsqinsertirp.

The following diagram illustrates the flow of control for IoCsqInsertIrpEx.

diagram illustrating the flow of control for iocsqinsertirpex.

There are several natural ways to use the IoCsqXxx routines to queue and dequeue IRPs. For example, a driver could simply queue IRPs to be processed in the order in which they are received. The driver could queue an IRP as follows:

    status = IoCsqInsertIrpEx(IoCsq, Irp, NULL, NULL);

If the driver is not required to distinguish between particular IRPs, it could then simply dequeue them in the order in which they were queued, as follows:

    IoCsqRemoveNextIrp(IoCsq, NULL);

Alternatively, the driver could queue and dequeue specific IRPs. The routines use the opaque IO_CSQ_IRP_CONTEXT structure to identify particular IRPs in the queue. The driver queues the IRP as follows:

    IO_CSQ_IRP_CONTEXT ParticularIrpInQueue;
    IoCsqInsertIrp(IoCsq, Irp, &ParticularIrpInQueue);

The driver can then dequeue the same IRP by using the IO_CSQ_IRP_CONTEXT value.

    IoCsqRemoveIrp(IoCsq, Irp, &ParticularIrpInQueue);

The driver might also be required to remove IRPs from the queue based on a particular criterion. For example, the driver might associate a priority with each IRP, such that higher priority IRPs get dequeued first. The driver might pass a PeekContext value to IoCsqRemoveNextIrp, which the system passes back to the driver when it requests the next IRP in the queue.