The Windows Driver Model Simplifies Management of
Device Driver I/O Requests
|Windows NT device drivers use IRPs as a messaging and data transfer vehicle. As messages are the lifeblood of Windows-based apps, IRPs are the lifeblood of WDM drivers. But, even among experienced Windows driver developers, the precise rules for managing IRPs are not well known.|
This article assumes you're familiar with C, WDM |
Code for this article: WDMIRP.exe (2KB)
Ervin Peretz is a USB and peripherals device driver developer
in the Windows NT core drivers group at Microsoft. He can
be reached at email@example.com.|
Windows NT device drivers use I/O Request Packets (IRPs) as a messaging and data transfer vehicle. The Windows® Driver Model (WDM), which defines a common architecture for Windows 98 and Windows 2000 device drivers, inherits a lot from the old Windows NT® device driver modelso much that almost everything I will say here about IRP handling in WDM applies to pure Windows NT drivers as well.
I decided to write this article when I realized how little-understood WDM is. Even among experienced Windows driver developers, the precise rules for managing IRPs are not well known. Unlike some articles that may include the source code for an entire driver, I will only give you a few general-purpose functions that you can copy into your code.
This article addresses IRP queuing and related WDM issues that give driver developers the most trouble. After setting up a general model, I will go into synchronization and other issues you'll need to understand to be a successful WDM programmer. I'll assume you have a working knowledge of WDM. For a general introduction to WDM, see Walter Oney's articles Surveying the New Win32 Driver Model for Windows 98 and Windows NT 5.0
in the November 1997 issue and Implementing the New Win32 Driver Model for Windows 98 and Windows NT 5.0 in the December 1997 issue of MSJ.
As messages are the lifeblood of Windows-based applications, IRPs are the lifeblood of WDM drivers. In WDM, all I/O is potentially asynchronous. The function used to initiate the I/O does not necessarily return the result of the
I/O; instead, the result may be returned via a completion function.
A device driver architecture usually evolves into a driver stack, a sequence of device drivers each specializing on the features of the lower driver. For example, on a machine with Universal Serial Bus (USB) ports, there may be a USB hub driver to drive the USB controller chip, followed by a minidriver and class driver for some class of devices (such as cameras), followed by a vendor-supplied driver that controls the specific features of the camera currently attached to the USB port.
An IRP is a kernel or driver-allocated structure representing a single I/O action. The I/O-initiating driver initializes the IRP with the request type, optional completion routine, and input/output buffer for the action. It then passes a pointer to the IRP down the driver stack. Upon receiving an IRP, a driver may do one of the following tasks:
In any event, a well-behaved WDM driver never blocks or polls to satisfy an IRP. This is essential if Windows is to be continuously responsive and always preemptible.
- Satisfy the I/O and complete the IRP with a successful status
- Complete the IRP with an error status
- Pass the IRP to a lower driver
- Queue the IRP, to be completed or passed down at a later time
Figure 1 Driver Stack
Some time after the I/O is initiated, a lower driver completes the IRP. The kernel calls each driver's completion routine with a pointer to the IRP. Thus, the IRP traverses back up the driver stack until the top-level driver's completion routine gets either a result or an error status.
Let's go right to a simple scenario that uses the aforementioned hypothetical USB camera stack (see Figure 1). I'll only cover the parts of initialization that are relevant to IRP handling. When the ACME camera is plugged in, the USB hub driver initiates a PnP event. The PnP ID of the camera matches one in the system registry or in an .INF installation script. This causes the USB camera minidriver, the camera class driver, and the ACME camera driver to be loaded. The minidriver registers itself by passing its context to some class-specific function in the class driver. The kernel calls the ACME camera driver's AddDevice function, passing it a pointer to a device object (of type DEVICE_OBJECT) representing the camera. The ACME camera driver uses IoCreateDevice to create its own device object, then uses IoAttachDeviceToDeviceStack to attach its new device object to the top of the device stack (the series of device objects connected one atop another in this fashion).
A device object has a StackSize field, which indicates how high it is on the device stack. This corresponds to how high its driver is on the driver stack. An IRP sent to this driver stack must include a private scratch area, or IRP stack location, for each driver it traverses. When the ACME camera driver wants to read a video frame from the camera, it uses the StackSize field of its device object in the IoAllocateIrp call to create an IRP with the right number of stack locations.
To set up the I/O, the camera driver sets a few fields in the appropriate IRP stack location. The convention is that each driver sets up the stack location of the driver below it. The camera driver uses IoGetNextIrpStackLocation to get a PIO_STACK_LOCATION pointer to the camera class driver's stack location. It sets the MajorFunction and MinorFunction fields of the stack location to values indicating what it wants to do. It then associates an I/O buffer with the IRP. (I'll discuss buffering methods later.) The camera driver then uses IoSetCompletionRoutine to set a completion routine for the IRP. This routine is required for the initiating driver, which needs to get the result. It is optional for lower-level drivers, which may or may not need to do postprocessing. Like the call parameters, the pointer to the current driver's completion routine is also stored in the next lower driver's IRP stack location. The completion routine is set as follows:
DeviceExtension, // context passed to completion
TRUE, // InvokeOnSuccess
TRUE, // InvokeOnError
TRUE); // InvokeOnCancel
status = IoCallDriver(NextDeviceObject, Irp);
|The camera driver now calls IoCallDriver with the IRP pointer to pass the IRP to the camera class driver, which receives the IRP via an interface indicated by the MajorFunction array in its driver object. It may just pass the IRP down to the appropriate minidriver. The minidriver's job is now to get the result into the IRP's I/O buffer and complete the IRP.
In some architectures, this intermediate driver might just pass the IRP down one more level and let the lower driver do all the work. But in this case, let's assume that the USB hub driver doesn't handle packets of 1MB. So the minidriver queues the IRP and returns STATUS_PENDING, indicating that it is retaining ownership of the IRP. The minidriver must now satisfy the IRP in some device-specific way. For example, the minidriver might create a succession of IRPs requesting small USB-sized packets from the USB hub driver. The class driver's job is to multiplex video frames from various miniports to multiple clients. The miniport's job is to recognize video frame boundaries and return entire video frames to the class driver. When the minidriver receives a complete video frame, it copies the frame into the original IRP's buffer, sets Irp->IoStatus.Status to STATUS_SUCCESS, and calls IoCompleteRequest to complete the IRP. The camera driver's completion routine gets called by the kernel; it does whatever it wants with the video frame and calls IoFreeIrp to free the IRP.
There are many variations and optimizations on the foregoing scenario. Also, there are a number of details I purposely left out because they require lengthy discussion. For example, when the top-level driver's completion routine frees the IRP, it must return STATUS_MORE_ PROCESSING_REQUIRED. This indicates to the IoCompleteRequest kernel service that the driver is retaining ownership of the IRP. Otherwise, the kernel continues to process your deallocated IRP, which will probably cause a system crash.
Suppose that instead of allocating and freeing an IRP for each video frame, the camera driver keeps a queue of IRPs and reuses them. This is a great optimization. The completion routine would again return STATUS_MORE_ PROCESSING_REQUIRED, as the driver is retaining ownership of the IRP.
Queuing the IRP
For the most part, an IRP is a hot potato, which a driver either services immediately or relays to another driver. But when a driver must wait to service an IRP, as in the USB camera minidriver example, it needs to retain ownership of the IRP and still return immediately. In this case, the driver should queue the IRP on a private queue and return STATUS_PENDING. IRPs contain a LIST_ENTRY field, making them easily queueable. The following sections and sample code show how to do this correctly.
Figure 2 shows some simple cut-and-paste code for IRP queue management, which I'll improve on later. In a multithreading environment such as WDM, you must protect your data structures from corruption using a spinlock, as shown. Note that any function that grabs a spinlock cannot be pageable. (I'll explain later.) Also note that it is dangerous to make any call out of your driver while holding a spinlock. Any call that executes code in another driver or the kernel constitutes a call outside the driver.
This sample code is incomplete in that it ignores the fact that an IRP can get cancelled at any time. That is, a higher-level driver can call IoCancelIrp to force an IRP to complete. This is a source of much nastiness. First of all, you must make sure that the cancelled IRP gets removed from your queue, or you may later end up touching an IRP you no longer own or that may even be freed. Second, you have to deal with all the subtle race conditions of the IRP getting cancelled while you are enqueuing or dequeuing it. I'll address these soon.
The Cancel Routine
The mechanism for dealing with IRP cancellation is the cancel routine. Unlike a completion routine, of which there may be one per driver, there is at most one cancel routine set for an IRP at a time; only the owner of an IRP may have a cancel routine set for it. The purpose of a cancel routine is to get a driver's data structures into a consistent state and then to complete the IRP as quickly as possible when the IRP is cancelled.
It's not as simple as just setting a cancel routine, which dequeues the IRP and completes it, while the IRP is queued. The IRP can be cancelled at any time. By the time your handler routine gets called with the IRP pointer, the IRP may already be cancelled. Likewise, just as you are dequeuing the IRP, it can get cancelled.
Figures 3 and 4 show the sample code again, this time with the cancellation measures in place. Figure 5 shows the cancel routine. Note that IrpCancelRoutine acquires the local spinlock, dequeues the IRP, and releases the local spinlock before releasing the global cancel spinlock. This prevents the extremely unlikely scenario in which between releasing the cancel spinlock and acquiring the local spinlock, the IRP in question is removed from the queue, completed, reallocated, reinserted back onto the same queue, and then cancelled, resulting in two threads in IrpCancelRoutine that will complete the same IRP. This is the kind of insane race condition you are up against when writing WDM-based drivers.
It is possible to release the global cancel spinlock right after acquiring your local spinlock, but you have to be careful what Interrupt Request Level (IRQL) you are restoring when you release the spinlocks. You don't want to restore the IRQL to Irp->CancelIrql until you are finished with your local spinlock, and you must exit the cancel routine with the IRQL restored to Irp->CancelIrql. You should probably avoid any variation here.
The IRP handler function should use the EnqueueIrp function to retain ownership of an Irp. Here's an outline of the IrpHandlerFunction:
NTSTATUS IrpHandlerFunction(IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp)
status = EnqueueIrp(Irp);
// in case of failure, EnqueueIrp() completed this IRP
|Note the use of IoMarkIrpPending in the EnqueueIrp function. This macro sets a bit in the current IRP stack location that indicates to the kernel that the IRP may complete on a different thread than the calling thread. The bit doesn't matter until the IRP is completed, but a simple rule is to call the IoMarkIrpPending macro whenever you are about to return STATUS_PENDING for an IRP. A common mistake is to invoke IoMarkIrpPending after EnqueueIrp returns success. However, this is flawed because once an IRP is queued, you cannot touch it without acquiring the queue's spinlock.
Of course, anything I say about queuing IRPs applies to any other method of retaining ownership. If you return STATUS_PENDING but keep the IRP pointer in a private pointer variable instead of a queue, you had better have a spinlock protecting that pointer and set a cancel routine for the retained IRP. This can come up in some subtle ways. For example, suppose you retain ownership and pass the IRP pointer as a context to a timer function. You must set a cancel routine that cancels the timer. The timer callback function must grab the same spinlock as the cancel routine and check if the IRP has been cancelled. If it completes the IRP, the timer callback function must first clear the
cancel routine while holding the local spinlock and release the spinlock.
Questions on IRP Cancellation
At this point you may be asking yourself a question: If
the IRP can be cancelled at any time, how is it legal for me to do anything with that IRP? If a higher-level driver can cancel (and subsequently free) an IRP that I own, why is it legal for me to dereference the IRP to obtain its MajorFunction field, its Cancel field, or anything else?
The answer to both questions is that an IRP can be in one of two states: cancelable or not cancelable. What makes an IRP cancelable is having a cancel routine. Canceling an IRP with no cancel routine has no immediate effect other than setting the Cancel flag. As long as an IRP is a hot potato, it will complete soon anyway. It is only when a driver queues the IRP, thus indefinitely delaying its completion, that cancellation becomes an issue. A driver that wants to queue an IRP must check the Cancel flag and complete the IRP immediately if the flag is set. If the IRP is cancelled while it is queued, the cancel routine will dequeue and complete it. Thus, canceling an IRP may not complete the IRP directly; it just ensures that the IRP will complete soon.
Here's some pseudocode for the kernel function IoCancelIrp:
acquire the global cancel spinlock
set the IRP's Cancel field to TRUE
check the IRP's CancelRoutine field
if CancelRoutine is set, clear the
CancelRoutine field, call the cancel routine,
and return TRUE;otherwise, release the cancel
spinlock and return FALSE
|Note that IoCancelIrp does not free the global cancel spinlock if there is a cancel routine. That's why it is ultra-important that your cancel routine call IoReleaseCancelSpinLock to release it. Otherwise, the global cancel spinlock will never be released, and the system will grind
to a halt.
Why doesn't IoCancelIrp just free the cancel spinlock for you after calling the cancel routine? Calling IoCompleteRequest to complete an IRP constitutes a call outside the driver. A deadlock could occur in the system if your cancel routine completes the IRP while the kernel is holding the cancel spinlock for you. So the cancel routine has to release the global cancel spinlock first, then complete the cancelled IRP. So why doesn't IoCancelIrp release the cancel spinlock and complete the IRP for you? You don't necessarily want IoCancelIrp to complete the IRP that is passed to it. In my implementation of IrpCancelRoutine, for example, I only complete the IRP if it is in my queue. It would also mean that the cancel routine could not call outside the driver, since the cancel spinlock is being held for it. Having IoCancelIrp cancel the IRP would be taking too much power away from the driver.
Finally, inquisitive readers may have surmised from IoReleaseCancelSpinLock that there must be an IoAcquireCancelSpinLock. It's exposed by the kernel, as you can see in the WDM.H header file that comes with the WDM DDK. Why do you need a local spinlock to protect your IRP queue? Since your cancel routine must already deal with the global cancel spinlock, why not also use the global cancel spinlock for queue synchronization in EnqueueIrp and DequeueIrp? Please, please, don't do this. Having drivers constantly contending for the global cancel spinlock in regularly traversed code paths will lead to serious systemwide performance degradation. I don't know of any reason why a driver would ever need to call IoAcquireCancelSpinLock. Why is there a single global cancel spinlock, instead of one in each IRP? Because the cancel spinlock is used for various other things in the kernelthat's all I can say.
Alternative IRP Queuing Implementations
Make sure your enqueue/dequeue and cancel code is bulletproof. There are several correct implementations, and I've given what I believe to be the simplest. The following is some rough pseudocode for the implementation in Figures 3 through 5 (excluding local spinlock acquisition and release):
Set the cancel routine
Check if IRP is cancelled
If cancelled, clear the cancel routine and
complete the IRP
If not cancelled, queue the IRP
Remove the first IRP from the queue
Clear its cancel routine
If this IRP was cancelled, complete it and loop
Otherwise, return the IRP
Release the global cancel spinlock
If the cancelled IRP is in the list, dequeue and
|EnqueueIrp checks the IRP's cancelled state before enqueuing it. If EnqueueIrp finds the IRP to be cancelled, it just completes the IRP right away. It is tempting to check Irp->Cancel first, and then only set a cancel routine if you're actually queuing the IRP. But this is flawed because the IRP can get cancelled right after you check Irp->Cancel, causing you to queue a cancelled IRP. By setting a cancel routine first, I am making the cancellation code path for this IRP contend for my local spinlock.
Note that IrpCancelRoutine does not automatically complete the IRP that is passed to it; it only completes those cancelled IRPs that are in the queue. In the case that EnqueueIrp finds that the IRP has been cancelled, IrpCancelRoutine may or may not have been called. But if IrpCancelRoutine was called, it is waiting to acquire the local spinlock. When it does so it will not find the IRP in the queue, and so it will not complete the IRP. If the IRP is cancelled it is up to EnqueueIrp to complete the IRP whether or not IrpCancelRoutine was called. This ensures that the IRP gets completed and that it is not completed twice, something you definitely want to avoid because it will cause a system crash. IrpCancelRoutine also cannot assume that the IRP with which it was called
is in the list (it must not loop forever if it does not find
DequeueIrp enjoys the same simplification. If it finds that the IRP at the head of the queue is cancelled, it can just complete the IRP and go on without having to worry about whether IrpCancelRoutine was called. If IrpCancelRoutine was called, it is waiting for the local spinlock currently held by DequeueIrp. Since DequeueIrp has removed the IRP from the queue, IrpCancelRoutine will not complete the IRP. This leaves DequeueIrp free to complete the cancelled IRP whether or not IrpCancelRoutine was called.
The nice thing about this implementation is that, in the case that EnqueueIrp or DequeueIrp finds the IRP to be already cancelled, it doesn't have to know or care whether IrpCancelRoutine was or will be called. IrpCancelRoutine is a nop as far as this IRP is concerned.
An alternative implementation is to always let IrpCancelRoutine complete the IRP if it is called. This is
the pseudocode (again, omitting local spinlock acquisition and release):
Set cancel routine
Queue the IRP
Check if IRP is cancelled
If cancelled, test and clear the cancel routine
If the cancel routine was not already clear,
our cancel routine was not called, so
dequeue and complete the IRP
Remove the first IRP from the queue
Test and clear its cancel routine
If the IRP was cancelled, then
If the cancel routine was not already called,
complete the IRP Loop
Otherwise, return the IRP
Release the global cancel spinlock
Dequeue and complete the IRP passed in as an
|Here I have moved the complexity from IrpCancelRoutine to EnqueueIrp and DequeueIrp. Because this version of IrpCancelRoutine always completes the IRPnot just if the IRP is still in the queue, as in the suggested implementationEnqueueIrp and DequeueIrp have to think about whether IrpCancelRoutine was called before completing a cancelled IRP.
Irp->Cancel set to TRUE indicates that the IRP was cancelled, but it does not indicate whether a cancel routine was called for the IRP. The way to determine the latter is to check the previous cancel routine returned by IoSetCancelRoutine (see the pseudocode for IoCancelIrp). Because the alternative IrpCancelRoutine always completes the IRP, the alternative EnqueueIrp and DequeueIrp must complete a cancelled IRP if and only if the cancel routine was not called. I don't like this method because EnqueueIrp and DequeueIrp have to think about the activity of IrpCancelRoutine.
Any implementation must use the same local spinlock when enqueuing, dequeuing, and canceling an IRP. Your local spinlock is the only thing preventing the IRP from being freed between the time EnqueueIrp sets the cancel routine and the time it checks the Cancel flag (or between checking the Cancel flag and queuing the IRP, depending on your implementation).
Whatever your implementation, keep in mind that IRPs rarely get cancelled. So optimize the regularly traversed code paths at the expense of the cancellation-handling code.
If your driver allocates IRPs, you may want to recycle them rather than allocating and freeing an IRP for each use. Your driver is the top-level driver as far as these IRPs are concerned. In this case, you should return STATUS_
MORE_PROCESSING_REQUIRED in the completion routine so that the kernel stops processing the IRP and leaves your driver owning the IRP.
When you send one of these recycled IRPs back down the driver stack, make sure that you first reinitialize the IRP
by calling IoInitializeIrp. Among other things, this clears the IRP's Cancel field. Otherwise, if the IRP was cancelled previously and the Cancel flag is still set, the first driver beneath yours that attempts to queue the IRP will see the flag set and will immediately complete the IRP with STATUS_CANCELLED.
If an IRP is sending data to or retrieving data from a device, the kernel or initiating driver attaches a data buffer to the IRP. A lower driver servicing the IRP needs to know how to get a pointer to this buffer.
There are three ways that a data buffer can be attached to an IRP. You can find the codes for these in the WDM.H header. With METHOD_BUFFERED, the buffer pointer is Irp->AssociatedIrp.SystemBuffer. For METHOD_IN_DIRECT and METHOD_OUT_DIRECT, the IRP contains a memory descriptor list. Use MmGetSystemAddressForMdl(Irp->MdlAddress) to get a usable buffer pointer. With METHOD_NEITHER, the buffer pointer is Irp->UserBuffer.
For IOCTL IRPs (IrpSp->MajorFunction == IRP_MJ_ DEVICE_CONTROL, where IrpSp is a pointer to the current IRP stack location), the buffering method is determined by the lower two bits in the IRP's control code itself (that is, in IrpSp->Parameters.DeviceIoControl.IoControlCode). Read and write IRPs (IrpSp->MajorFunction == IRP_MJ_READ or IRP_MJ_WRITE) use the buffering method determined by the device object's flags. The DO_ BUFFERED_IO bit corresponds to METHOD_BUFFERED. The DO_DIRECT_IO bit corresponds to METHOD_IN_ DIRECT or METHOD_OUT_DIRECT. If neither bit is set, the buffering method is METHOD_NEITHER.
In WDM, the direction (IN or OUT) of an I/O action is judged relative to the driver completing the IRP. So if you call a lower driver in a camera stack to read in a video frame, and you want to use direct buffering, use METHOD_OUT_ DIRECT. This also applies to the parameter indicating the length of a buffer attached to an IOCTL IRP. If the caller is sending data out some device, then the driver servicing and completing the IRP is receiving the data from the caller; therefore, the length of the buffer would be given by IrpSp->Parameters.DeviceIoControl.InputBufferLength. If the caller is reading data from a device, then the driver
completing the IRP is sending the data to the caller. So the buffer length would be given by IrpSp->Parameters.
DeviceIoControl.OutputBufferLength. This is counterintuitive, I know.
Locked Versus Pageable Code
A chunk of driver code or data can be either locked or pageable. If it is locked, it resides in physical memory whenever the driver is loaded. If it is pageable, the code or data can be swapped out by the memory manager to make room for something else. Unless you specifically make it pageable, your entire driver will be locked. Making portions of your driver pageable may be the conscientious thing to do, especially if your driver can be loaded but not active (for example, the camera can be plugged in but not capturing). However, there are several dangers involved in making a driver pageable, and you should make sure to understand all of the following issues before doing so.
At any point in time, a thread is executing at some IRQL. The normal level is PASSIVE_LEVEL (0). Interrupt service routines (ISRs) execute at IRQL > 2. Dispatch routines (DPCs) typically execute at DISPATCH_LEVEL (2). Completion routines for kernel functions may execute at APC_LEVEL (1). The important thing to remember is that a page miss at IRQL>=DISPATCH_LEVEL results in a system crash. So only functions that execute at IRQL<DISPATCH_LEVEL can be made pageable. Acquiring a spinlock involves an IRQL boost. If a function acquires a spinlock, then the following code will execute at higher IRQL; the function (and any data that it touches) must be locked.
If a function is not an ISR or a DPC, and it does not execute while holding a spinlock (it is not called from code that holds a spinlock and does not acquire a spinlock itself), then you can make your driver a better citizen by making that function pageable. If you are using the Microsoft Visual C++ compiler, you can use the following pragma to do this. For testing you can verify the IRQL in your pageable functions as shown here:
#pragma alloc_text(PAGE, MyPageableFunction)
VOID MyPageableFunction (…)
ASSERT(KeGetCurrentIrql() < DISPATCH_LEVEL);
|Remember to also lock down any data that is touched at IRQL>=DISPATCH_LEVEL.
Using the Device Queue
The kernel provides wrapper functions (IoStartPacket, IoStartNextPacket, and so on) that can do some of the top-layer IRP handling for you by queuing IRPs in the device object itself. Using the device queue limits your driver to serial half-duplex operation; that is, since there is only one queue and at most one IRP designated as the current IRP for each device object at a time, your driver can only process IRPs in one direction at a time for any given device object. The device-queuing functions are well documented in MSDN, so I won't cover them here. One important thing to remember is that if your driver uses a device queue, the cancel routine must remove the cancelled IRP from the device object's CurrentIrp field.
Also note that the device queue functions use the global cancel spinlock to synchronize the device object's IRP queue. As I mentioned earlier, using the global cancel spinlock during regularly traversed code paths (for anything other than IRP cancellation) can seriously deteriorate system performance. For this reason, I avoid these kernel interfaces whenever I can.
If your dispatch routine is called with an IRP that your driver does not handle, you should do one of two things. If your driver is an intermediate driver on the driver stack, you should simply pass the IRP to the driver below you:
return IoCallDriver(deviceObject, Irp);
|If your driver is at the bottom of the driver stack (this usually means that your driver implements a bus of some sort), you should complete the IRP with the default status. The default status is usually STATUS_ NOT_SUPPORTED. However, the default status may be changed by a higher-level driver as the IRP is passed down. To allow higher-level drivers to change the default status for an IRP that your driver does not support, you should just leave the status alone and return the default status as the result of your dispatch routine, as follows:
status = Irp->IoStatus.Status;
|It is easy to write a WDM driver that will demo well, but much harder to produce one that is bulletproof in all stress scenarios. Some of the guidelines given here are quite esoteric. Refer to the list of tips below, which summarizes the points I've examined here.
- To retain ownership of an IRP in an IRP handler function, return STATUS_PENDING.
- To retain ownership of an IRP in a completion routine, return STATUS_MORE_PROCESSING_REQUIRED.
- If you free the IRP in your completion routine, also return STATUS_MORE_PROCESSING_REQUIRED.
- If you queue an IRP (whether completed or not), you must set a cancel routine.
- Right after setting a cancel routine, check that the IRP was not already cancelled.
- Your cancel routine must call IoReleaseCancelSpinLock to release the global cancel spinlock whether or not the cancel routine actually completes any IRPs. Do this after acquiring your local spinlock and before calling outside the driver to complete the cancelled IRP.
- Only the cancel routine needs to release the cancel spinlock. If you discover that the IRP was cancelled as you're about to queue it, and you never set the cancel routine, then IoCancelIrp releases the cancel spinlock for you. IoCancelIrp either calls a cancel routine or releases the cancel spinlock itself, not both.
- When dequeuing an IRP, Irp->Cancel TRUE indicates that the IRP was cancelled. It does not indicate whether your cancel routine was called. The way to determine that is to test and clear the cancel routine using IoSetCancelRoutine(Irp, NULL). If the result is NULL, the cancel routine was called; otherwise, it was not called.
- Whenever you are about to return STATUS_PENDING for an IRP, call the IoMarkIrpPending macro on that IRP. Do this while still holding the local spinlock.
- If you reuse an IRP, make sure to first call IoInitializeIrp, which clears the Cancel field.
- Never call outside your driver while holding a spinlock.
- Functions that execute only at IRQL<DISPATCH_ LEVEL can be made pageable. However, if any part of the function executes while holding a spinlock, that code will execute at a higher IRQL, so the code and any data that it touches must be locked.
- For unsupported IRPs, your dispatch function should either pass the IRP down to the next driver or complete it without changing the default status.
From the January 1999 issue of Microsoft Systems Journal.
Get it at your local newsstand, or better yet, subscribe.