As shown in Figure 4.1, higher-level NT drivers have a different set of standard routines than lowest-level device drivers, with an overlapping subset of standard routines common to both types of drivers.
The set of routines for intermediate and highest-level NT drivers also varies, according to the following criteria:
Figure 4.5 illustrates the path an IRP might take through the standard routines of an NT driver layered somewhere over the device driver described Section 4.2.
The intermediate mirror driver shown in Figure 4.5 has the following characteristics:
Figure 4.5 IRP Path through NT Intermediate Driver Routines
As Figure 4.5 shows, an IRP is sent first to the intermediate driver’s Dispatch routine for the given major function code (IRP_MJ_XXX). This IRP also requests a data transfer operation (IRP_MJ_WRITE), and the intermediate driver’s I/O stack location is shown in the middle, with an indefinite number of I/O stack locations for higher- and lower-level drivers shown shaded.
This driver mirrors write requests to another physical device but sends read requests alternately to the driver(s) of devices with mirrored partitions. For write requests, it creates IRPs for both the original device and for a second device on which the data is mirrored, assuming the parameters in the input IRP are valid.
Figure 4.5 happens to show a call to IoAllocateIrp but higher-level NT drivers can call other support routines to allocate IRPs for requests they send to lower-level drivers. For more information about allocating IRPs, see Chapter 6.
When the Dispatch routine calls IoAllocateIrp, it specifies the number of I/O stack locations needed in each new IRP. The driver must specify a stack location for each lower driver in the chain, getting the appropriate value from the device objects of each driver just below the mirror driver. At the driver writer’s discretion, it can add one to this value when it calls IoAllocateIrp to get a stack location of its own in each IRP it allocates, as the driver in Figure 4.5 does.
This intermediate driver’s Dispatch routine calls IoGetCurrentIrpStackLocation (not shown) with the original IRP to determine what it should be doing and to check parameters, if possible.
It calls IoSetNextIrpStackLocation because it allocated its own stack location in each newly created IRP and IoGetCurrentIrpStackLocation to create a context for itself that it uses later in the IoCompletion routine.
Then, it calls IoGetNextIrpStackLocation with each newly created IRP so that it can set up the next lower-level drivers’ I/O stack locations in the IRPs it allocated. The mirror driver’s Dispatch routine copies the IRP function codes and parameters (pointer to the transfer buffer, length in bytes to be transferred for IRP_MJ_WRITE) into the I/O stack locations for the next-lower drivers. These drivers, in turn, will set up the I/O stack locations for the drivers just below themselves, if any.
The Dispatch routine in Figure 4.5 calls IoSetCompletionRoutine with each IRP it allocated.
A higher-level NT driver can request that its IoCompletion routine be called when lower drivers complete an IRP with one, some, or any of STATUS_SUCCESS, STATUS_CANCELLED, or an error status. Because the driver in Figure 4.5 must dispose of the IRPs it allocated, this driver sets its IoCompletion routine to be called when lower drivers complete its IRPs, whatever their completion status values.
Because the driver in Figure 4.5 mirrors in parallel, it passes both IRPs that it allocated on to the next-lower-level drivers by calling IoCallDriver twice, once for each target device object representing a mirrored partition.
When either set of lower-level drivers completes the requested operation, the intermediate mirror driver’s IoCompletion routine is called. This routine must determine whether the other set of lower drivers has also completed the requested operation, so the mirror driver maintains a count in its own I/O stack location of the original IRP.
Assuming that the I/O status block indicates that one set of lower drivers has completed the DupIRP1 shown in Figure 4.5 successfully, the mirror driver’s IoCompletion routine decrements its count but cannot complete the original IRP until it decrements the count to zero. If the decremented count is not yet zero, the IoCompletion routine calls IoFreeIrp with the first-returned IRP (DupIRP1 in Figure 4.5) that the driver allocated and returns STATUS_MORE_PROCESSING_REQUIRED.
When the mirror driver’s IoCompletion routine is called again with the DupIRP2 shown in Figure 4.5, the IoCompletion routine decrements the count in the original IRP and determines that both sets of lower-level drivers have carried out the requested operations.
Assuming the I/O status block in DupIRP2 also is set with STATUS_SUCCESS, the IoCompletion routine copies the returned IRP’s I/O status block into the original IRP and frees the second IRP it created. It calls IoCompleteRequest with the original IRP, and returns STATUS_MORE_PROCESSING_REQUIRED to forestall the I/O Manager’s completion processing of DupIRP2.
If either set of lower-level drivers does not complete the mirror driver’s IRPs successfully, the mirror driver’s IoCompletion routine would log an error and attempt appropriate mirrored-data recovery. For more information about logging errors, see Chapter 16.