Allocating Additional IRPs

< BACK  NEXT >
[oR]

There are some situations where an intermediate driver needs to allocate additional IRPs to send to another driver. For example, the initialization code in one driver might want to query the capabilities of a lower-level driver by issuing an IOCTL request. An example filter driver that implements this strategy is listed later in this chapter.

As another example, a fault-tolerant disk driver, implemented as an intermediate driver, might allocate an additional IRP to send to a second (mirror) driver. This second IRP would mirror the original request.

Yet a third example occurs when a higher-level driver exposes an abstract command to a client. The command itself is implemented through a series of lower-level calls, each requiring the allocation of a new IRP. The SCSI class driver implements this strategy when relying upon the lower-level SCSI port driver.

The IRP's I/O Stack Revisited

Before explaining the available IRP allocation techniques, it is important to have a clear understanding of the IRP stack operation. As already described, each driver that receives an IRP is supplied a unique IRP stack location that is easily obtained with a call to IoGetCurrentIrpStackLocation.

If an intermediate driver plans to pass an incoming IRP to a lower-level driver, it has to set up the I/O stack location for the next lower driver. To obtain a pointer to the lower driver's I/O stack slot, the intermediate driver uses IoGetNextIrpStackLocation. After setting up the lower stack slot (perhaps by copying the current slot into the next slot), the intermediate driver uses IoCallDriver to pass the IRP down. This function, IoCallDriver, automatically pushes the I/O stack pointer so that when the lower driver calls IoGetCurrentIrpStackLocation, it will get the right address (i.e., one lower than its caller).

When the lower driver calls IoCompleteRequest, the completed IRP's I/O stack is popped. This allows an I/O Completion routine belonging to the higher driver to call IoGetCurrentIrpStackLocation if it needs to access its own stack location. As the IRP bubbles its way back up to the original caller, the I/O stack is automatically popped again for each driver in the hierarchy. Table 15.3 summarizes the effects of these functions on an IRP's I/O stack pointer.

To maintain consistent behavior with driver-allocated IRPs, the I/O Manager initializes the new IRP's I/O stack pointer so that it points to a nonexistent slot one location before the beginning of the stack. This ensures that when the driver passes the IRP to a lower-level driver, IoCallDriver's "push" operation sets the stack pointer to the first real slot in the stack. Thus, the higher-level driver must call IoGetNextIrpStackLocation to retrieve a pointer to the I/O stack slot intended for the target driver.

Table 15.3. Effect of Functions on IRP's I/O Stack Pointer
Working with the IRP Stack Pointer
Function Effect on the IRP stack pointer
IoGetCurrentIrpStackLocation No change
IoGetNextIrpStackLocation No change
IoSetNextIrpStackLocation Pushes stack pointer by one location
IoCallDriver Pushes stack pointer by one location
IoCompleteRequest Pops stack pointer by one location

Controlling the Size of the IRP Stack

When a driver receives an IRP from an outside caller, the number of I/O stack slots is determined by the StackSize field of the driver's Device object. If the intermediate driver plans to pass incoming IRPs to a lower-level driver, it needs to increment this field to one more than the number of slots reserved by all lower drivers. That is, it must set the Device object's StackSize field to the value of the lower Device object's StackSize field plus one. This ensures that there are enough stack slots for all drivers within the hierarchy. Of course, the technique requires that drivers pile on top of each other, with the lower driver initialized prior to the higher driver.

The value of StackSize in a Device object represents the number of stack slots needed by all lower drivers, including one slot for itself. That is, it represents the maximum call depth beneath the current level plus one.

The I/O Manager constructs IRPs upon request of a driver when any of the following calls are made:

  • IoBuildAsynchronousFsdRequest

  • IoBuildDeviceIoControlRequest

  • IoBuildSynchronousFsdRequest

The IRP constructed contains the number of stack slots specified in the target (where the IRP is being sent) Device object's StackSize field. The target Device object is passed as an argument to the three functions listed. These IRPs therefore contain sufficient stack slots for all calls to lower drivers, but do not contain a slot for the intermediate driver itself.

If an intermediate driver uses IoAllocateIrp or ExAllocatePool to create an IRP, the driver must explicitly specify the number of I/O stack slots in the new IRP. Of course, the common practice is to use the StackSize field of the target Device object to determine the proper number of slots.

Ordinarily, an intermediate driver does not need a stack slot for itself in the IRP it allocates. The exception occurs if the intermediate driver chooses to associate some per-request context within the IRP. In such a case, the driver allocates an IRP with one extra stack slot for itself, which is then used to hold private context data. The following code fragment shows how this technique is implemented:

 pNewIrp = IoAllocateIrp( pLowerDevice->StackSize + 1 ); // Bearing in mind that a new IRP is allocated with //   the stack pointer just before the beginning // Push the I/O stack pointer so that it points to //   the first valid slot. Use this slot to hold //   context information needed by the upper driver. IoSetNextIrpStackLocation( pNewIrp ); pContextArea = IoGetCurrentIrpStackLocation( pNewIrp ); pNextDriverSlot = IoGetNextIrpStackLocation( pNewIrp ); // Set up the next driver's I/O stack slot: pNextDriverSlot->MajorFunction = IRP_MJ_XXX; ... // Attach an I/O Completion routine: IoSetCompletionRoutine(           pNewIrp,           IoCompletion,           NULL,           TRUE, TRUE, TRUE );            // Send the IRP to someone else: IoCallDriver( pLowerDevice, pNewIrp ); 

Creating IRPs with IoBuildSynchronousFsdRequest

The I/O Manager provides three convenience functions that simplify the process of building IRPs for standard kinds of I/O requests. The first one is IoBuildSynchronousFsdRequest, and it fabricates read, write, flush, or shutdown IRPs. See Table 15.4 for a description of this function.

The number of I/O stack locations in IRPs created with this function is equal to the StackSize field of the TargetDevice argument. There is no straightforward way to leave room in the I/O stack for the intermediate driver itself.

The Buffer, Length, and StartingOffset arguments to this function are required for read and write operations. They must be NULL (or 0) for flush or shutdown operations.

IoBuildSynchronousFsdRequest automatically sets up various fields in the Parameters area of the next lower I/O stack location, so there is rarely any need to touch the I/O stack. For read or write requests, this function also allocates system buffer space or builds an MDL, depending on whether the TargetDevice does Buffered or Direct I/O. For buffered outputs, it also copies the contents of the caller's buffer into the system buffer; at the end of a buffer input, data is automatically copied from the system buffer back to the caller's buffer.

As the function name suggests, IoBuildSynchronousFsdRequest operates synchronously. In other words, the thread that calls IoCallDriver normally blocks itself until the I/O operation completes. To conveniently perform the block, pass the address of an initialized Event object in the IRP that is allocated. Then, after sending the IRP to a lower-level driver with IoCallDriver, use KeWaitForSingleObject to wait for the Event object. When a lower-level driver completes the IRP, the I/O Manager puts this Event object into the signaled state, which awakens the intermediate driver. The I/O status block signifies whether everything worked.

Drivers that perform blocking I/O can degrade system performance because they prevent the calling thread from overlapping its I/O operations. This is contrary to the philosophy of the Windows 2000 I/O architecture, so it should not be used without good reason.

Table 15.4. Function Prototype for IoBuildSynchronousFsdRequest
PIRP IoBuildSynchronousFsdRequest IRQL == PASSIVE_LEVEL
Parameter Description
IN ULONG MajorFunction One of the following:
  • IRP_MJ_READ

  • IRP_MJ_WRITE

  • IRP_MJ_FLUSH_BUFFERS

  • IRP_MJ_SHUTDOWN

IN PDEVICE_OBJECT pTargetDevice Device object where IRP is sent
IN OUT PVOID pBuffer Address of I/O buffer
IN ULONG Length Length of buffer in bytes
IN PLARGE_INTEGER startingOffset Device offset where I/O begins
IN PKEVENT pEvent Event object used to signal I/O completion
OUT PIO_STATUS_BLOCK Iosb Receives final status of I/O operation
Return value
  • Non-NULL: address of new IRP

  • IRP could not be allocated

This is contrary to the philosophy of the Windows 2000 I/O architecture, so it should not be used without good reason.

Also, the Event object used to wait for I/O completion needs to be synchronized properly among multiple threads. Consider the case where two threads in the same process issue a read request using the same handle. The DispatchRead routine executes in the context of the first thread and blocks itself waiting for the Event object. Then, this same DispatchRead routine executes in the context of the other thread and reuses the same Event object to issue a second request. When the IRP for either request completes, the Event object signals. Both threads awaken, and neither thread knows which IRP really completed. One solution is to guard the Event object with a Fast Mutex. Perhaps a better solution is to allocate a new Event object with each IRP fabricated.

The I/O Manager automatically cleans up and deallocates IRPs created with IoBuildSynchronousFsdRequest after their completion processing is done. This includes releasing any system buffer space or MDL attached to the IRP. To trigger this cleanup, a lower-level driver simply has to call IoCompleteRequest.

Normally, there is no need to attach an I/O Completion routine to one of these IRPs unless some driver-specific postprocessing is needed. If an I/O Completion routine is attached, it should return STATUS_SUCCESS. This lets the I/O Manager free the IRP.

Creating IRPs with IoBuildAsynchronousFsdRequest

The second convenience function, IoBuildAsynchronousFsdRequest, is quite similar to the synchronous version. It builds read, write, flush, or shutdown requests without regard to many details. The main difference is that the IRPs fabricated by this call process asynchronously. There is no option to stop and wait for the I/O to complete. Table 15.5 contains the prototype for this function.

As with IoBuildSynchronousFsdRequest, the Buffer, Length, and StartingOffset parameters to IoBuildAsynchronousFsdRequest are required for read and write operations. They must be NULL (or 0) for flush or shutdown operations.

Notice that IoBuildAsynchronousFsdRequest can be called at or below DISPATCH_LEVEL IRQL. The synchronous version can be called only from PASSIVE_LEVEL.

Unlike the IRPs fabricated from the synchronous version, the ones from this function are not released automatically when a lower-level driver completes them. Instead, an I/O Completion routine must be attached to any IRP created with IoBuildAsynchronousFsdRequest. The I/O Completion routine calls IoFreeIrp, which releases the system buffer or MDL associated with the IRP and then deallocates the IRP itself. The return value of the I/O Completion routine should be STATUS_MORE_PROCESSING_REQUIRED.

Table 15.5. Function Prototype for IoBuildAsynchronousFsdRequest
PIRP IoBuildAsynchronousFsdRequest IRQL <= DISPATCH_LEVEL
Parameter Description
IN ULONG MajorFunction One of the following:
  • IRP_MJ_READ

  • IRP_MJ_WRITE

  • IRP_MJ_FLUSH_BUFFERS

  • IRP_MJ_SHUTDOWN

IN PDEVICE_OBJECT pTargetDevice Device object where IRP is sent
IN OUT PVOID pBuffer Address of I/O buffer
IN ULONG Length Length of buffer in bytes
IN PLARGE_INTEGER startingOffset Device offset where I/O begins
OUT PIO_STATUS_BLOCK Iosb Receives final status of I/O operation
Return value
  • Non-NULL: address of new IRP

  • IRP could not be allocated

Creating IRPs with IoBuildDeviceIoControlRequest

The last convenience function, IoBuildDeviceIoControlRequest (described in Table 15.6) simplifies the task of building IOCTL IRPs. This is useful because it is fairly common for drivers to expose odd behavior through custom IOCTLs.

The InternalDeviceIoControl argument specifies the major function code in the target driver's I/O stack slot. FALSE produces an IRP with IRP_MJ_ DEVICE_CONTROL, while TRUE causes it to be sent to IRP_MJ_INTERNAL_ DEVICE_CONTROL.

Also, notice that either synchronous or asynchronous calls can be performed with IRPs returned by this function. To perform synchronous I/O control operations, simply pass the address of an initialized Event object when the IRP is allocated. Then, after sending the IRP to a lower-level driver with IoCallDriver, use KeWaitForSingleObject to wait for the Event object. When a lower-level driver completes the IRP, the I/O Manager puts this Event object into the Signaled state, which awakens the intermediate driver. The I/O status block reports the ultimate disposition of the IRP. As with IoBuildSynchronousFsdRequest, care must be taken when the Event object is used among multiple threads.

The I/O Manager automatically cleans up and deallocates IRPs created with IoBuildDeviceIoControlRequest after their completion processing is done. This includes releasing any system buffer space or MDL attached to the IRP. To trigger this cleanup, a lower-level driver simply has to call IoCompleteRequest.

Table 15.6. Function Prototype for IoBuildDeviceIoControlRequest
PIRP IoBuildDeviceIoControlRequest IRQL == PASSIVE_LEVEL
Parameter Description
IN ULONG IoControlCode IOCTL code recognized by target device
IN PDEVICE_OBJECT pTargetDevice Device object where IRP is sent
IN PVOID inputBuffer Buffer passed to lower driver
IN ULONG inputLength Length of input buffer in bytes
OUT PVOID outputBuffer Buffer returned by lower driver
IN ULONG outputLength Length of output buffer in bytes
IN BOOLEAN InternalDeviceIoControl TRUE-Internal request FALSE-External request
IN PKEVENT pEvent Event object used to signal I/O completion
OUT PIO_STATUS_BLOCK Iosb Receives final status of I/O operation
Return value
  • Non-NULL: address of new IRP

  • NULL: IRP could not be allocated

Normally, there is no need to attach an I/O Completion routine to one of these IRPs unless some driver-specific postprocessing is needed. If an I/O Completion routine must be used, it should return STATUS_SUCCESS when it is done. This lets the I/O Manager free the IRP.

The one idiosyncrasy with this function is the way it handles the buffering method bits embedded in the IOCTL code. If an IOCTL code contains METHOD_BUFFERED, IoBuildDeviceIoControlRequest allocates a nonpaged pool buffer and copies the contents of the InputBuffer. When the IRP completes, the contents of the nonpaged pool buffer are automatically copied to OutputBuffer. As just described, it behaves exactly like a Win32 DeviceIoControl call coming from a user-mode application.

But if an IOCTL code containing a Direct I/O method is specified, an interesting result occurs: IoBuildDeviceIoControl always builds an MDL forthe output buffer address and always uses a nonpaged pool buffer for theInput Buffer address, regardless of whether the IOCTL code specifies METHOD_IN_DIRECT or METHOD_OUT_DIRECT.

Creating IRPs from Scratch

The I/O Manager routines just described are the most convenient way to work with driver-allocated IRPs. Occasionally, however, they may not be the appropriate vehicle for IRP allocation. For example, when issuing a request other than for read, write, flush, shutdown, or device I/O control, these functions do not help. The only option is to allocate a blank IRP and set it up manually. The following sections describe several ways to do this.

IRPs from IoAllocateIrp

The IoAllocateIrp function allocates an IRP from an I/O Manager zone buffer and performs certain basic kinds of initialization. A driver must fill the I/O stack location for the target driver and set up whatever kind of buffer thetarget driver is expecting to find. The following code fragment illustrates the use of this function.

 PMDL pNewMdl; PIRP pIrp; PIO_STACK_LOCATION pNextIrpStack; // Allocate the new IRP with enough stack locations to //   hold the requirements of the drivers beneath us pNewIrp = IoAllocateIrp( pLowerDevice->StackSize ); // Allocate the memory descriptor list for any driver //   doing DMA beneath us pNewMdl = IoAllocateMdl(                MmGetMdlVirtualAddress(                     pOriginalIrp->MdlAddress ),                MAX_TRANSFER_SIZE,                FALSE,    // Primary buffer                FALSE,    // No quota charge                pNewIrp ); IoBuildPartialMdl(      pOriginalIrp->MdlAddress,      pNewMdl,      MmGetMdlVirtualAddress( pOriginalIrp->MdlAddress ),      MAX_TRANSFER_SIZE );       // Place a request into the new IRP (in this case, Read) //   The lower driver is being asked to perform a Read. pNextIrpStack = IoGetNextIrpStackLocation( pNewIrp ); pNextIrpStack->MajorFunction = IRP_MJ_READ; // Set any parameters appropriate for a Read request pNextIrpStack->Parameters.Read.Length =           MAX_TRANSFER_SIZE;            // Ensure that the lower driver knows what thread made //   the original request (in case an error must be //   reported - see text below) pNewIrp->Tail.Overlay.Thread =      pOriginalIrp->Tail.Overlay.Thread;       IoSetCompletionRoutine(      pNewIrp,      IoCompletion,      NULL,      TRUE, TRUE, TRUE );       // Finally, pass the new IRP request down: IoCallDriver( pLowerDevice, pNewIrp ); 

If the new IRP is targeted at a disk device, or a device with removable media, the intermediate driver needs to provide information about the thread making the original request. This provides the lower-level driver with a target for any pop-up dialog box reporting a potential error using IoSetHardErrorOrVerifyDevice. This thread information is contained in the original IRP's Tail.Overlay.Thread field and should be copied directly into the new IRP.

An intermediate driver is responsible for releasing any IRP created with IoAllocateIrp. It must also release other resources (MDLs or system buffers, for example) associated with the IRP. Normally, this cleanup occurs in the IRP's I/O Completion routine. The following code fragment provides an example.

 NTSTATUS IoCompletion(                IN PDEVICE_OBJECT pDevObj,                IN PIRP pIrp,                IN PVOID pContext ) {       ...       IoFreeMdl( pIrp->MdlAddress );       IoFreeIrp( pIrp );              return STATUS_MORE_PROCESSING_REQUIRED; } 
IRPs from ExAllocatePool

IRPs can also be allocated directly from a nonpaged pool using ExAllocatePool. The generic memory allocated must be initialized into an IRP using IoInitializeIrp. Setting up the I/O stack location, transfer buffers, and an MDL for DMA operations remain the responsibility of the driver.

The following is an example of a manually allocated IRP using ExAllocatePool. The lower Device object expects a nonpaged pool buffer rather than an MDL.

 pNewIrp = ExAllocatePool(                NonPagedPool,                IoSizeOfIrp( pLowerDevice->StackSize ));                 IoInitializeIrp(      pNewIrp,      IoSizeOfIrp( pLowerDevice->StackSize ),      pLowerDevice->StackSize );       pNextIrpStack = IoGetNextIrpStackLocation( pNewIrp ); // Assuming a Read operation, set it up pNextIrpStack->Parameters.Read.Length = BUFFER_SIZE; // Instead of an MDL, use a custom buffer pNewIrp->AssociatedIrp.SystemBuffer =      ExAllocatePool( NonPagedPool, BUFFER_SIZE );       // As before, copy thread info of original caller pNewIrp->Tail.Overlay.Thread =      pOriginalIrp->Tail.Overlay.Thread;       IoSetCompletionRoutine(      pNewIrp,      IoCompletion,      NULL,      TRUE, TRUE, TRUE );       // Tell the fabricated IRP to "come on down" IoCallDriver( pLowerDevice, pNewIrp ); 

Again, it is the job of the I/O Completion routine attached to the new IRP to perform cleanup and release the IRP. The following code fragment demonstrates.

 NTSTATUS IoCompletion(           IN PDEVICE_OBJECT pDevObj,           IN PIRP pIrp,           IN PVOID pContext ) {     ...     // Free the custom buffer used by the lower driver     ExFreePool( pIrp->AssociatedIrp.SystemBuffer );          // Free the manually allocated IRP     IoFreeIrp( pIrp );          return STATUS_MORE_PROCESSING_REQUIRED; } 

Notice that IoFreeIrp is used to free the IRP, even though it was allocated with ExAllocatePool. This is because the field in the IRP tells the I/O Manager whether this IRP came directly from the pool or whether it came from the I/O Manager's private zone buffer.

IRPs from Driver-Managed Memory

Finally, there are situations where a driver design chooses to maintain a private collection of IRPs allocated within a driver-specific zone buffer or a look-aside list. Such IRPs still need to be initialized using IoInitalizeIrp. However, since the I/O Manager knows nothing about the driver's memory management strategy for these IRPs, the IoFreeIrp function cannot be used. Instead, the I/O Completion routine needs to call whatever internal driver function is responsible for releasing the IRP.

Setting Up Buffers for Lower Drivers

The previous examples of the manually allocated IRPs demonstrate the need to initialize and clean up any buffers needed by those I/O requests. The actual technique utilized depends on whether the target Device object performs buffered or direct I/O.

Buffered I/O Requests

In this case, the Dispatch routine in the intermediate driver has to call ExAllocatePool to allocate the buffer. It stores the address of this buffer in the AssociatedIrp.SystemBuffer field of the driver-allocated IRP. Later, an I/O Completion routine attached to the IRP must release the buffer with a call to ExFreePool.

Direct I/O Requests

Handling these requests means the intermediate driver must set up an MDL describing the I/O buffer. The intermediate driver's Dispatch routine performs the following:

  1. It calls IoAllocateMdl to create an MDL large enough to map the buffer. It stores the address of this MDL in the MdlAddress field of the driver-allocated IRP.

  2. The Dispatch routine fills the MDL. To map a portion of the buffer associated with the original caller's IRP, it calls IoBuildPartialMdl. To map system memory into the MDL, it uses MmBuildMdlForNonPagedPool.

  3. It then attaches an I/O Completion routine to the driver-allocated IRP using IoSetCompletionRoutine.

  4. Finally, the Dispatch routine sends the IRP to a lower-level driver with IoCallDriver.

When the lower-level driver completes the IRP, the intermediate driver's I/O Completion routine uses IoFreeMdl to release the MDL.

Keeping Track of Driver-Allocated IRPs

Intermediate drivers must be careful about the handling of incoming I/O requests that result in multiple IRPs being sent in parallel to lower drivers. In particular, it is vital for the original incoming IRP not to be completed until all the allocated IRPs have finished their work. Exactly how the intermediate driver does this depends on whether it performs synchronous or asynchronous I/O with the driver-allocated IRPs.

Synchronous I/O

This is the simpler of the two cases since the intermediate driver's Dispatch routine just has to stop and wait until all the allocated IRPs have been completed. In general, the Dispatch routine does the following:

  1. It calls IoBuildSynchronousFsdRequest to create some number of driver-allocated IRPs.

  2. Next, the Dispatch routine calls IoCallDriver to pass all the driver-allocated IRPs to other drivers.

  3. It then calls KeWaitForMultipleObjects and freezes until all the allocated IRPs have completed.

  4. Finally, it calls IoCompleteRequest with the original IRP to send back to the caller.

Notice that since the original request is blocked inside the Dispatch routine itself, there is no need to mark the original IRP as pending.

Asynchronous I/O

This is the more complex case because there is no central point of control where the driver can stop and wait for everything to finish. Instead, the intermediate driver must attach I/O Completion routines to each driver-allocated IRP, and the completion routine must decide whether it is time to complete the original caller's IRP.

The following steps are typical of work that is done in the Dispatch routine of intermediate drivers using Asynchronous I/O requests to lower drivers.

  1. It puts the original caller's IRP in the pending state by calling IoMarkPending.

  2. Next, the Dispatch routine uses one of the methods described in the previous section to allocate additional IRPs.

  3. It attaches an I/O Completion routine to each of these IRPs with IoSetCompletionRoutine. When it makes this call, the Dispatch routine passes a pointer to the original caller's IRP as the pContext argument.

  4. The Dispatch routine stores a count of outstanding allocated IRPs in an unused field of the original IRP. The Key field in the current I/O stack locations Parameters union is one possible context.

  5. Next, it uses IoCallDriver to pass all the IRPs to other drivers.

  6. Finally, the Dispatch routine passes back STATUS_PENDING as its return value. This is necessary because the original IRP is not yet ready for completion processing.

As each of the lower drivers complete each of their IRPs, the intermediate driver's I/O Completion routine executes. This routine does the following:

  1. First, it performs whatever cleanup is necessary and deletes the driver-allocated IRP.

  2. The I/O Completion routine calls ExInterlockedDecrementLong to decrement the count of outstanding IRPs contained in the original caller's IRP. A pointer to this original IRP is passed as its pContext argument.

  3. If the count equals zero, then this indicates that the last outstanding driver-allocated IRP has completed. In this case, the I/O Completion routine completes the original IRP by calling IoCompleteRequest.

  4. Finally, it returns STATUS_MORE_PROCESSING_REQUIRED to prevent any further completion processing of the driver-allocated IRP (which incidentally has just been deleted).

< BACK  NEXT >


The Windows 2000 Device Driver Book(c) A Guide for Programmers
The Windows 2000 Device Driver Book: A Guide for Programmers (2nd Edition)
ISBN: 0130204315
EAN: 2147483647
Year: 2000
Pages: 156

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net