Layered drivers are the most general type of intermediate driver. They depend on a well-defined inter-driver calling mechanism provided by the I/O Manager. The following three sections explain how this mechanism works and what a driver needs to do if it wants to use another driver as a component. How Layered Drivers WorkAs shown in Figure 15.1, a layered driver may expose one or more named Device objects to which clients send I/O requests. When an IRP representing one of these requests arrives, the layered driver can proceed in two different ways. Figure 15.1. Layared driver operation.
If the layered driver needs to regain control after a lower-level driver finishes with an IRP, it can attach an I/O Completion routine to the IRP. This routine executes when the lower driver calls IoCompleteRequest. Initialization and Cleanup in Layered DriversLike every other kernel-mode driver, a layered driver must expose a main entry point called DriverEntry. It must also supply a corresponding Unload routine. As a WDM driver, a layered driver must support AddDevice and RemoveDevice, along with other PnP request handlers as appropriate. The following sections describe what these routines must do. DriverEntryThe initialization steps performed by a layered driver are similar to those of a regular driver. The prime difference is that a layered driver must determine which I/O requests it handles directly and which it passes through to lower-level drivers. The layered driver entry points are initialized to routines that perform the appropriate action on the various IRP requests. AddDeviceThe work performed by a layered driver is a variation of the work that any WDM driver must perform. It includes
After these steps are performed, the layered driver can use the target Device object pointer to make calls to the lower-level driver. RemoveDeviceWhen a layered driver is sent the PnP request for IRP_MN_REMOVE_DEVICE, the driver must reverse the work of AddDevice. Although the exact steps may vary, a layered driver's RemoveDevice routine generally performs the following:
Code Fragment: Connecting to Another DriverThe following code fragment shows how one driver might layer itself on top of another. In this example, the lower-level driver, LODRIVER, exposes a device called LO0 and the layered driver, HIDRIVER, exposes HI0. NTSTATUS AddDevice ( IN PDRIVER_OBJECT pDriverObject, IN PDEVICE_OBJECT pdo ) { CUString hiDevName("\device\HI"); PDEVICE_OBJECT pHiFdo; PDEVICE_EXTENSION pHiExt; NTSTATUS status; // Form the internal Device Name for the hi object ulHiDeviceNumber++; hiDevName += CUString(ulHiDeviceNumber); // Now create the device status = IoCreateDevice( pDriverObject, sizeof(DEVICE_EXTENSION), &(UNICODE_STRING)hiDevName, FILE_DEVICE_UNKNOWN, 0, TRUE, &pHiFdo ); if (!NT_SUCCESS(status)) return status; // Initialize the Device Extension pHiExt = (PDEVICE_EXTENSION)pHiFdo->DeviceExtension; pHiExt->pDevice = pHiFdo; // back pointer pHiExt->DeviceNumber = ulHiDeviceNumber; pHiExt->ustrDeviceName = hiDevName; // Pile this new fdo on top of the existing lower stack pHiExt->pLowerDevice = // downward pointer IoAttachDeviceToDeviceStack( pHiFdo, pdo); // Since IRPs will be forwarded to the lower device, // room must be reserved within the IRP I/O Stack // locations for this higher Device object. pHiFdo->StackSize = pHiExt->pLowerDevice->StackSize + 1; // Finally, copy the characteristics of the lower device // into the high device's "Flags" & "DeviceType" fields: pHiFdo->Flags |= (pHiExt->pLowerDevice->Flags & (DO_BUFFERED_IO | DO_DIRECT_IO | POWER_INRUSH | POWER_PAGABLE )); pHiFdo->DeviceType = pHiExt->pLowerDevice->DeviceType; pHiFdo->Characteristics = pHiExt->pLowerDevice->Characteristics; // Form the symbolic link name (if necessary), etc. ... Other Initialization Concerns for Layered DriversLayered drivers operate, generically, in one of two modes: transparent or virtual. TRANSPARENTSome layered drivers are intended to slip transparently between a lower-layer driver and its clients. The layered driver would therefore need to mimic the behavior of the lower driver, since clients are likely to be unaware of the inserted layer. Windows 2000 fault-tolerant disk driver is one example of a transparent layer. To guarantee that the layered driver behaves in a transparent manner, the DriverEntry and AddDevice function needs to perform the following extra initialization:
VIRTUAL OR LOGICAL DEVICE LAYERThe other possibility is that the layered driver exposes virtual or logical Device objects. It is possible to synthesize a device abstraction that bears little, if any, resemblance to the actual underlying physical implementation. For example, a named pipe object is a logical device that is far removed from the actual network hardware upon which it may be implemented. In this case, the layered driver should choose appropriate values for the Type and Characteristics fields of the layered Device object. Also, the exact set of MajorFunction dispatch functions supported by the layered driver is the set appropriate to the logical Device object. Similarly, there is no requirement for the layered and target Device objects to use the same buffering strategy. I/O Request Processing in Layered DriversSince layered drivers do not directly manage hardware, they do not need Start I/O, Interrupt Service, or DPC routines. Instead, most of the code in a layered driver consists of Dispatch routines and I/O Completion routines. Because they deserve extra attention, I/O Completion routines are discussed later in this chapter. The sections below describe the operation of the layered driver's Dispatch routines. When one of these Dispatch routines receives an IRP, one of three actions can be taken: complete the IRP directly, pass down the IRP, or generate new IRP requests for lower layers. Each possibility is descried below. COMPLETE THE ORIGINAL IRPThe simplest case is when the Dispatch routine is able to process the request all by itself and return either success or failure notification to the original caller. The Dispatch routine does the following:
This process is not magic. In fact, it is the same procedure any Dispatch routine follows when it wants to end the processing of a request. PASS THE IRP TO ANOTHER DRIVERThe second possibility is that the layered driver's Dispatch routine needs to pass the IRP to the next lower driver. The Dispatch routine does the following:
Notice that the Dispatch routine does not call IoMarkIrpPending to put the original IRP in the pending state before sending it to the lower driver. This is because the Dispatch routine does not know whether the IRP should be marked pending until after IoCallDriver returns. Unfortunately, by that time IoCallDriver has already pushed the I/O stack pointer in the IRP, so a call to IoMarkIrpPending (which always works with the current stack slot) would mark the wrong stack location. The solution is to call IoMarkIrpPending in an I/O Completion routine after the IRP stack pointer has been reset to the proper level. ALLOCATE ADDITIONAL IRPsFinally, the layered driver's Dispatch routine may need to allocate one or more additional IRPs, which it then sends to lower-level drivers. The Dispatch routine has the option of waiting for these additional IRPs to complete, or of issuing asynchronous requests to the lower driver. In the asynchronous case, cleanup of the additional IRPs occurs in an I/O Completion routine The technique of allocating IRPs within a driver is explained later in this chapter. Code Fragment: Calling a Lower-Level DriverThe code fragment below shows how the Dispatch routine in one driver might forward an IRP to a lower-level driver. For purposes of the example, it also shows how the upper driver could store some context (in this case, a retry count) in an unused field of its own I/O stack location. NTSTATUS DispatchRead( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp ) { PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION) pDevObj->DeviceExtension; PIO_STACK_LOCATION pThisIrpStack = IoGetCurrentIrpStackLocation( pIrp ); PIO_STACK_LOCATION pNextIrpStack = IoGetNextIrpStackLocation( pIrp ); // In this case, the upper driver copies the entire // stack location from its slot into the stack // of the next lower driver. // In other words - pass-thru to next lower driver *pNextIrpStack = *pThisIrpStack; // Choose a (now) unused field with the upper // driver's IRP to store some context in this // case, a retry count. pThisIrpStack->Parameters.Read.Key = RETRY_COUNT_MAXIMUM_VALUE; // To recapture this IRP after the lower driver // finishes, the upper driver attaches an // I/O Completion routine: ReadCompletion. // Since the final 3 args of the following call are // TRUE, ReadCompletion is called regardless // of why the IRP completes. IoSetCompletionRoutine( pIrp, ReadCompletion, NULL, TRUE, TRUE, TRUE ); // Now send the IRP to the lower driver. // Return whatever the lower driver returns. return IoCallDriver( pDevExt->pLowerDevice, pIrp );
|