4.2 Device Drivers’ Staged IRP Processing

As shown in Figure 4.1, lowest-level NT drivers have certain standard routines that higher-level NT drivers need not have. The set of standard routines for NT device drivers also varies according to the following criteria:

Figure 4.2 illustrates the path an IRP takes through the standard routines of an NT mass-storage device driver with the following characteristics:

Figure 4.2 IRP Path through NT Device Driver Routines (DMA)

As Figure 4.2 shows, an IRP is sent first to the driver’s Dispatch routine for the given major function code (IRP_MJ_XXX), in this case DDDispatchReadWrite. An NT driver is never sent an IRP with an unknown function code, because each driver sets its Dispatch routine(s) in the driver object on an IRP_MJ_XXX-specific basis when the driver initializes.

Calling IoGetCurrentIrpStackLocation

A Dispatch routine that handles more than one IRP_MJ_XXX, handles an IRP_MJ_XXX with minor subfunctions (IRP_MN_XXX), or handles device I/O control requests (IRP_MJ_DEVICE_CONTROL and/or IRP_MJ_INTERNAL_DEVICE_CONTROL), and every other driver routine that processes each IRP must call IoGetCurrentIrpStackLocation in order to determine what to do and what parameters to use.

The IRP shown in Figure 4.2 requests a data transfer operation (IRP_MJ_READ or IRP_MJ_WRITE), and this driver’s I/O stack location is the lowest in the IRP, with an indefinite number of higher-level drivers’ I/O stack locations shown shaded. For simplicity, calls to IoGetCurrentIrpStackLocation from the DispatchReadWrite, StartIo, AdapterControl, and DpcForIsr routines are not shown in Figure 4.2.

Calling IoMarkIrpPending and IoStartPacket

Assuming the parameters for the read/write request are valid, the Dispatch routine would call IoMarkIrpPending to indicate that the IRP is not yet completed, and IoStartPacket to queue or pass the IRP on to the driver’s StartIo routine for further processing. The Dispatch routine would also return the NTSTATUS value STATUS_PENDING.

Figure 4.3 illustrates such a call to IoStartPacket.

Figure 4.3 Calling IoStartPacket

If the driver is currently busy processing another IRP on the device, IoStartPacket will insert the IRP into the device queue associated with the device object. If the driver is not busy and the device queue is empty, its StartIo routine will be called immediately with the input IRP.

Note that the driver of a mass-storage device would not supply a Cancel routine when it calls IoStartPacket both because a file system layered over such a driver handles the cancelation of file I/O requests and because mass-storage device drivers process IRPs so quickly. Usually, the highest-level driver in a chain of layered NT drivers handles the cancelation of IRPs.

The device driver shown in Figure 4.2 might supply a Key value to impose a driver-determined order on IRPs in the device queue when it calls IoStartPacket.

Calling IoAllocateAdapterChannel and IoMapTransfer

Assuming the StartIo routine shown in Figure 4.2 finds that the transfer request can be done by a single DMA operation, the StartIo routine calls IoAllocateAdapterChannel with the entry point of the driver’s AdapterControl routine and the IRP.

When the system DMA controller is available, the AdapterControl routine is called to set up the transfer operation, as shown in Figure 4.2. The AdapterControl routine calls IoMapTransfer with a pointer to the buffer, described in the MDL at Irp->MdlAddress, to set up the system DMA controller. Then, the driver programs its device for the DMA operation and returns. (For more detailed information about using DMA and adapter objects, see Chapter 3.)

Calling IoRequestDpc from the Driver’s ISR

When the device interrupts to indicate its transfer operation is complete, the driver’s ISR stops the device from generating interrupts and calls IoRequestDpc, as shown in Figure 4.2.

This call queues the driver’s DpcForIsr routine to complete as much of the transfer operation as possible at a lower hardware priority (IRQL).

Calling IoStartNextPacket and IoCompleteRequest

When the DpcForIsr routine has done its processing for the transfer, it calls IoStartNextPacket promptly so the driver’s StartIo routine will be called with the next IRP in the device queue, if any are queued. The DpcForIsr also sets the just completed IRP’s I/O status block and then calls IoCompleteRequest with the IRP.

Figure 4.4 illustrates this driver’s calls to IoStartNextPacket and IoCompleteRequest.

Figure 4.4 Calling IoStartNextPacket and IoCompleteRequest

Contrary to what Figure 4.4 implies, most NT device drivers call IoStartNextPacket before they call IoCompleteRequest.

NT drivers should call IoStartNextPacket or IoStartNextPacketByKey to begin the next requested I/O operation as soon as possible, preferably before they call IoCompleteRequest and certainly before they return control. If no IRPs are currently in the device queue, IoStartNextPacket merely returns to the caller.

Setting the I/O Status Block in an IRP

Every NT device driver must set the I/O status block in the IRP before calling IoCompleteRequest, in order to supply information to any interested higher-level drivers and, ultimately, to the original requestor of the I/O operation. Any higher-level driver layered above the device driver in Figure 4.4 might have set up an IoCompletion routine that reads the I/O status block set by the device driver. However, higher-level NT drivers usually do not modify the I/O status block in an IRP that has been completed by a device driver, unless such a higher-level driver is retrying the IRP and, therefore, reinitializes the I/O status block.

Every higher-level NT driver that completes an IRP without sending it on to the next lower driver also must set the I/O status block in that IRP before it calls IoCompleteRequest. For good overall I/O throughput, a higher-level driver should check the parameters in its own I/O stack location of a given IRP and, if it determines that they are invalid, should set the I/O status block and complete the request itself, rather than passing an invalid request on to lower drivers in the chain.

The I/O status block is defined as follows:
typedef struct _IO_STATUS_BLOCK { 
    NTSTATUS Status; 
    ULONG Information; 
} IO_STATUS_BLOCK, *PIO_STATUS_BLOCK; 
 

Assuming the transfer operation in Figure 4.4 was successful, the DpcForIsr shown in Figure 4.2 would set STATUS_SUCCESS in Status and the number of bytes transferred in Information for the IRP’s I/O status block.

Note that certain standard NT driver routines also return NTSTATUS-type values, as shown by the declarations in Section 4.1. For more information about NTSTATUS constants like STATUS_SUCCESS, see Chapter 16.

Supplying a PriorityBoost in Calls to IoCompleteRequest

If an NT device driver can complete an IRP in its Dispatch routine, it calls IoCompleteRequest with a PriorityBoost of IO_NO_INCREMENT because that driver can assume that the original requestor did not wait on its I/O operation.

Otherwise, such a device driver supplies a system-defined and device-type-specific value that boosts the requestor’s runtime priority to compensate for the time the requestor waited on its device I/O request. (See ntddk.h for specifics.)

Higher-level NT drivers apply the same PriorityBoost as their respective underlying device drivers when they call IoCompleteRequest with an IRP.

Effect of Calling IoCompleteRequest

When a driver calls IoCompleteRequest with a given IRP, the I/O Manager fills that driver’s I/O stack location with zeros before calling the next higher-level driver, if any, that has set up its IoCompletion routine to be called for the IRP.

A higher-level driver’s IoCompletion routine can check only the IRP’s I/O status block to determine how all lower drivers handled a given request.

The caller of IoCompleteRequest must not attempt to access the just completed IRP. Such an attempt is a programming error that causes a system crash.