Writing I/O Completion RoutinesAn I/O Completion routine is an I/O Manager callback that notifies a higher-level driver when the lower-level driver has finished work on an IRP. This section explains how to use I/O Completion routines in intermediate drivers. Requesting an I/O Completion CallbackTo regain control of an IRP after it has been processed by a lower-level driver, use IoSetCompletionRoutine (described in Table 15.1). This function puts the address of an I/O Completion routine in the IRP stack location associated with the next lower driver. When some lower-level driver calls IoCompleteRequest, the I/O Completion routine executes as the IRP bubbles its way back to the top of the driver hierarchy. Except for the driver on the bottom, each driver in the hierarchy can attach its own I/O Completion routine to an IRP. This allows every level to receive notification when an IRP completes. The I/O Completion routines execute in driver-stacking order, from bottom to top.
The three BOOLEAN parameters passed to IoSetCompletionRoutine allow the callback to be invoked based on the disposition of the IRP by the lower level. The I/O Manager uses the IoStatus.Status field of the IRP to decide whether to invoke the I/O Completion routine. Execution ContextBy the time an I/O Completion routine is called, the I/O Manager has already popped the I/O stack pointer, so the current stack location is the one belonging to the driver. Table 15.2 lists the arguments passed to an I/O Completion routine. The execution context of an I/O Completion routine is the same as that of the caller of IoCompleteRequest. This could be either PASSIVE_LEVEL or DISPATCH_LEVEL, depending upon whether a DPC routine is used to complete the IRP. Since the design of the lower level is not likely to be under the control of the higher level, an I/O completion routine must assume the more restrictive context of DISPATCH_LEVEL IRQL. When an I/O Completion routine is finished, it should return one of two status codes. Returning STATUS_SUCCESS allows the IRP to continue its journey back toward the original requester. Along the way, other I/O Completion routines attached to higher-level drivers execute. This is normally the appropriate return value when the IRP originated from a higher level. To suspend further processing of the "completed" IRP, an I/O Completion routine can return STATUS_MORE_PROCESSING_REQUIRED. This value blocks the execution of any higher-level I/O Completion routines attached to the IRP. It also prevents the original caller from receiving notification that the IRP has completed. An I/O Completion routine should return this code if it either plans to send the IRP back down to a lower-level driver (as in the case of a split transfer) or if the IRP was allocated by this driver and the I/O Completion routine is going to deallocate it.
What I/O Completion Routines DoAn intermediate driver can attach an I/O Completion routine to any IRP it sends to another driver. This includes the original IRP the driver received from another caller, as well as any IRPs that the driver itself allocates. When an I/O Completion routine executes, there are three general kinds of tasks it may need to perform. RELEASE THE ORIGINAL IRPIf the completed IRP is one that came from an outside caller, it may require some driver-specific cleanup. At the very least, the I/O Completion routine for one of these IRPs needs to do the following:
DEALLOCATE THE IRPIf the IRP was allocated by the driver, the I/O Completion routine may be responsible for releasing it. Based on how the IRP was allocated, an appropriate deallocation technique must be employed. The next section explains the entire process. RECYCLE THE IRPSome intermediate drivers have to split a transfer into smaller pieces before sending it to a lower-level driver. Normally, the most efficient way to do this is to send each partial transfer to the lower driver by reusing the same IRP. To recycle an IRP, the I/O Completion routine does the following:
During each partial transfer, an intermediate driver must maintain the current transfer count. One clever way to maintain this context information is to store it in unused fields of the intermediate driver's I/O stack location. For example, the Parameters.ByteOffset and Parameters.Key fields of the IRP stack area are often unused fields for a higher-level driver. Three DWORDs of context data can be maintained within these fields. Otherwise, the straightforward technique of allocating a private block from pooled memory that is passed as a context argument to the I/O Completion routine can always be used. Code Fragment: An I/O Completion RoutineListed below is a fragment of an I/O Completion routine. It complements the DispatchRead function presented in the last section. If the request completes normally, the IRP is sent back to the original caller. If some- thing fails at a lower level, it retries the operation a fixed number of times. NTSTATUS ReadCompletion( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp, IN PVOID pContext ) { PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION) pDevObj->DeviceExtension; PIO_STACK_LOCATION pThisIrpStack = IoGetCurrentIrpStackLocation( pIrp ); PIO_STACK_LOCATION pNextIrpStack = IoGetNextIrpStackLocation( pIrp ); // Set up a reference variable for the retryCount. // The variable uses the Parameters.Read.Key field. DWORD &retryCount = pThisIrpStack->Parameters.Read.Key; // If the lower level finshed successfully, // or if we have exceeded our retry count, // return normally. if ((NT_SUCCESS( pIrp->IoStatus.Status )) || ( retryCount == 0 )) { // If the lower level requested that the // IRP be marked "pending", make it so. if ( pIrp->bPendingReturned ) IoMarkIrpPending( pIrp ); return STATUS_SUCCESS; } // The lower level reported a failure, but we still // have the patience to try again (for a while) retryCount--; // patience counter // Copy down the stack (again) *pNextIrpStack = *pThisIrpStack; // Don't confuse lower level with our retry count *pNextIrpStack->Parameters.Read.Key = 0; // The I/O Completion routine must be reset each // time the IRP is recyled. IoSetCompletionRoutine( pIrp, ReadCompletion, NULL, TRUE, TRUE, TRUE ); // Send the IRP back to the lower level IoCallDriver( pDevExt->LowerDevice, pIrp ); // Indicate to the I/O Manager that we're still // working on the request. return STATUS_MORE_PROCESSING_REQUIRED; }
|