Writing Layered Drivers

< BACK  NEXT >
[oR]

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 Work

As 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.
graphics/15fig01.gif
  • Send the IRP directly to a lower-level driver.

  • Hold the IRP in a pending state while it allocates additional IRPs and sends them to one or more lower-level drivers.

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 Drivers

Like 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.

DriverEntry

The 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.

AddDevice

The work performed by a layered driver is a variation of the work that any WDM driver must perform. It includes

  1. Calling IoCreateDevice to build the upper-level device objects seen by the outside world. Like the device objects created by other drivers, the device name must be unique.

  2. Calling IoAttachDeviceToDeviceStack to pile the layered driver's device on top of an existing stack of devices.

  3. Normally, AddDevice saves the target Device object pointer in the Device Extension of the upper-level Device object at the time it stacks itself on top of the driver stack.

  4. If the layered driver forwards incoming IRPs to the target Device object, AddDevice should set the layered Device object's StackSize field to a value one greater than the StackSize field of the target Device object. This guarantees that there are enough stack slots for all the drivers in the hierarchy.

  5. If the lower-level driver requires it, AddDevice fabricates an IRP with IRP_MJ_CREATE as its major function code and sends it to the target Device object.

  6. If the Device object is exposed separately to Win32 applications, Add-Device calls IoCreateSymbolicLink to add its Win32 name to the \?? area of the Object Manager's namespace.

After these steps are performed, the layered driver can use the target Device object pointer to make calls to the lower-level driver.

RemoveDevice

When 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:

  1. It calls IoDeleteSymbolicLink to remove the upper-level Device object's Win32 name from the Object Manager's namespace.

  2. If the lower-level driver requires it, an IRP with IRP_MJ_CLOSE as its major function code is fabricated and sent to the target Device object.

  3. The target Device object's pointer reference count is decremented by calling IoDetachDevice. This effectively breaks the connection with the target Device object.

  4. Finally, it destroys the upper-level Device object by calling IoDeleteDevice.

Code Fragment: Connecting to Another Driver

The 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 Drivers

Layered drivers operate, generically, in one of two modes: transparent or virtual.

TRANSPARENT

Some 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:

  • Within DriverEntry, the exact set of MajorFunction codes as the lower driver should be supported, either passing through IRP requests or overriding the behavior of the lower driver.

  • Within AddDevice, copy the DeviceType and Characteristics fields from the target Device object into the layered Device object. This step is shown in the preceding code example.

  • AddDevice should copy the DO_DIRECT_IO or DO_BUFFERED_IO bits from the target Device's Flags field. This ensures that the layered Device object uses the same buffering strategy as the target.

VIRTUAL OR LOGICAL DEVICE LAYER

The 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 Drivers

Since 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 IRP

The 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:

  1. It calls IoGetCurrentStackLocation to get a pointer to the driver's I/O stack slot.

  2. The Dispatch routine processes the request using various fields in the IRP and the I/O stack location.

  3. It puts an appropriate value in the IoStatus.Information field of the IRP.

  4. The Dispatch routine also fills the IoStatus.Status field of the IRP with a suitable STATUS_XXX code.

  5. Then it calls IoCompleteRequest with the priority-boost value of IO_NO_INCREMENT to send the IRP back to the I/O Manager.

  6. As its return value, the Dispatch routine passes back the same STATUS_XXX code that it puts into the IRP.

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 DRIVER

The 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:

  1. It calls IoGetCurrentIrpStackLocation to get a pointer to its own I/O stack location.

  2. The Dispatch routine also calls IoGetNextIrpStackLocation to retrieve a pointer to the I/O stack location of the next lower driver.

  3. It sets up the next lower driver's I/O stack location, including the MajorFunction field and various members of the Parameters union.

  4. The Dispatch routine calls IoSetCompletionRoutine to associate an I/O Completion routine with the IRP. At the very least, this I/O Completion routine is going to be responsible for marking the IRP as pending.

  5. It sends the IRP to a lower-level driver using IoCallDriver. This is an asynchronous call that returns immediately regardless of whether the lower-level driver completed the IRP.

  6. As its return value, the Dispatch routine passes back whatever status code is returned by IoCallDriver. This will be STATUS_SUCCESS, STATUS_PENDING, or some STATUS_XXX error code.

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 IRPs

Finally, 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 Driver

The 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 );     
< 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