The AddDevice function, called by the PnP Manager, merely initializes the device (and its extension) object. It is apparent from the AddDevice code that hardware is not yet touched. In fact, two general responsibilities remain for a driver.
Both tasks are performed by a driver upon receipt of a special IRP function (and subfunction) code that is new for WDM drivers: IRP_MJ_PNP. PnP IRP codes are sent by the PnP Manager when a variety of events occur, including
As described in chapter 6, IRP major function codes, such as Read and Write requests, are handled by an indexed table of Dispatch routines. Since an entire category of IRP_MJ_PNP messages are routed through this single Dispatch routine, it is the responsibility of this handler to perform a secondary dispatch using the minor subcode contained within the IRP. For PnP, the minor subcodes take the symbolic form IRP_MN_XXX, where XXX is a specific Plug and Play action requested by the PnP Manager. An example of the initialization of the major function dispatch is shown below. ... pDriverObject->MajorFunction[IRP_MJ_PNP] = DispPnP; The code to perform the secondary dispatch based upon the minor subcode of the IRP is shown below. NTSTATUS DispPnp( IN PDEVICE_OBJECT pDO, IN PIRP pIrp ) { // Obtain current IRP stack location PIO_STACK_LOCATION pIrpStack; pIrpStack = IoGetCurrentIrpStackLocation( pIrp ); switch (pIrpStack->MinorFunction) { case IRP_MN_START_DEVICE: ... case IRP_MN_STOP_DEVICE: ... default: // if not supported here, just pass it down return PassDownPnP(pDO, pIrp); } // all paths from the switch statement will "return" // the results of the handler invoked } Required Plug and Play IRPsIn order to be WDM compliant, drivers must support specific PnP IRPs, depending upon the type of device object nonbus FDO, bus FDO, and PDO. Table 9.3 lists the subcodes that must be supported for all device object types.
From an examination of Table 9.3, it should be apparent that PnP devices exist in one of many states and, indeed, the DDK provides a state diagram depicting transitions based on PnP IRPs processed by a driver. The states can be divided into two categories: the states traversed by a device while it is being inserted (i.e., the prestart states) and the states encountered after the device is started. A state diagram for the prestart states is shown in Figure 9. 4. The only responsibility of a driver during these state transitions is to correctly implement DriverEntry and AddDevice. Figure 9.4. Prestart device states.Once a device has entered the "Started" state, PnP IRPs direct all subsequent transitions. The possibilities are depicted in Figure 9.5. Drivers maintain post-start state within the device extension. Figure 9.5. Post-start device states.PDO Plug and Play IRPsIn addition to the PnP IRPs that must be handled by all WDM drivers, PDOs typically implement handlers for other minor subcodes of IRPs, as shown in Table 9.4. These IRP requests permit a driver to implement additional features such as device eject and reassignment of hardware resources. The closer a driver is to the hardware (i.e., the physical device or PDO), the more likely it is that a driver should support one or more of these codes.
Passing Down Plug and Play RequestsAll PnP requests are initiated by the PnP Manager. The PnP Manager always routes these requests to the highest driver in a device stack. Regardless of which PnP minor codes are handled by a driver, those that are not must be passed down the device stack to lower drivers, which may implement their own handler. Indeed, it is typical for a functional driver (controlling an FDO) to rely upon the physical driver (controlling a PDO) to implement many PnP requests. In turn, the physical driver relies upon the bus driver (i.e., a bus FDO) to implement many PnP requests. Passing a PnP request down the device stack is necessary for several reasons. Some drivers within the stack must "add value" to the request, and no one driver may assume that the complete response can be compiled from that level. In other cases, many levels of the stack benefit from a PnP notification. For example, a stopped device notice is critical to all layers. To pass down a PnP request, a driver taking action on the request must mark the IRP as complete (described in chapter 7) by setting the IoStatus.Status and IoStatus.Information fields of the IRP as appropriate. It then invokes IoCopyCurrentStackLocationToNext and IoCallDriver on the lower device. The lower device is known from the AddDevice call to IoAttachDeviceToDeviceStack, the result of which was saved in the device extension. An example of this technique is shown below. ... IoCopyCurrentStackLocationToNext(pIrp); PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION) pDO->DeviceExtension; IoCallDriver(pDevExt->pLowerDevice, pIrp); ... If a driver has no need to await the completion of lower drivers handling the passed down request, a more efficient mechanism for skipping the current IRP stack location can be utilized. The function IoSkipCurrentIrpStackLocation simply removes the current IRP stack location from participation in the IRP processing. Indeed, this is the suggested mechanism for handling PnP requests that are not handled by a driver and merely passed to the next lower driver: NTSTATUS PassDownPnP( IN PDEVICE_OBJECT pDO, IN PIRP pIrp ) { IoSkipCurrentIrpStackLocation( pIrp ); PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION) pDO->DeviceExtension; return IoCallDriver(pDevExt->pLowerDevice, pIrp); } Sometimes a driver passes down a PnP request before it can complete its own work on the request. For example, in response to a start request, IRP_MN_START_DEVICE, a driver typically needs to wait until lower-level drivers have started before starting its own hardware. The bus and any lower-level hardware initializes before individual devices start. Thus, a higher-level driver must first pass down the request and then await lower-level processing before continuing. This is best handled with a completion routine tied to the IRP by the higher-level driver. I/O Completion RoutinesAn I/O Completion routine is an I/O Manager callback that lets a driver layer recapture an IRP after a lower-level driver has completed it. I/O Completion routines are registered by a higher-level driver with IoSetCompletionRoutine described in Table 9.5. When a lower-level driver ultimately 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. The I/O Completion routines execute in the driver-stacking order, from bottom to top.
The three Boolean arguments passed to IoSetCompletionRoutine determine when and if the Completion routine ultimately runs. As an IRP returns "up the device stack," the field IoStatus.Status is used in conjunction with the three arguments to determine whether or not to invoke the registered routine. The prototype for an I/O Completion routine is described in Table 9.6. An example of the use of an I/O Completion routine to regain control of a PnP IRP after handling by a lower-level driver follows: ... IoCopyCurrentStackLocationToNext(pIrp); // Register the presence of a completion routine. // The completion routine is called when the IRP // is "completed" by the lower level. IoSetCompletionRoutine( pIrp, OnIoComplete, NULL, TRUE, TRUE, TRUE); PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION) pDO->DeviceExtension; IoCallDriver(pDevExt->pLowerDevice, pIrp); ... NTSTATUS OnIoComplete( PDEVICE_OBJECT pDO, PIRP pIrp, PVOID pContext) { // Perform post processing for IRP request here // At what IRQL level does this code run? // (see text below for explanation) ... return pIrp->IoStatus.Status; } Unfortunately, it is difficult to predict at what IRQL level a completion routine executes. If the lower-level driver calls IoCompleteRequest from PASSIVE_LEVEL IRQL, then higher-level I/O Completion routines execute at PASSIVE_LEVEL. If the lower-level driver completes the IRP request from DISPATCH_LEVEL (for example, from a DPC routine), then the higher-level Completion routines execute at DISPATCH_LEVEL.
Since PnP IRP requests sent by the PnP Manager execute at PASSIVE_LEVEL, no special design is necessary to ensure that a returned (completed from a lower level) PnP IRP continues to execute at PASSIVE_LEVEL. Code executing at DISPATCH_LEVEL is restricted in the kernel calls it may use and, as described in chapter 3, must ensure it does not reference paged memory. To ensure that PnP handlers execute at PASSIVE_LEVEL, a kernel Event object is used. A kernel Event object is a synchronization mechanism that is analogous to a flag. A thread of execution patiently waits for the Event flag to be raised (set) without consuming CPU resource. Once the Event flag is raised, the blocked (waiting) thread is scheduled for resumed operation. A full description of the use of kernel Events is contained in chapter 14. For now, however, it is sufficient to note that the Event flag could be used as a signal between an I/O Completion routine and a PASSIVE_LEVEL thread within a higher-level driver. To use a kernel event, storage must be reserved by the programmer in nonpaged memory. The full technique is shown below. ... IoCopyCurrentStackLocationToNext(pIrp); // Reserve space for a kernel Event object KEVENT event; // And initialize it, flag DOWN KeInitializeEvent( &event, NotificationEvent, FALSE); // Register the presence of a completion routine. // Pass the Event object to the Completion routine. IoSetCompletionRoutine( pIrp, OnIoComplete, (PVOID)&event, TRUE, TRUE, TRUE); // Call the lower level(s) PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION) pDO->DeviceExtension; IoCallDriver(pDevExt->pLowerDevice, pIrp); // Wait for lower level(s) to complete KeWaitForSingleObject( &event, Executive, KernelMode, FALSE, NULL); // Perform post-processing functions here... // On return from Wait, PASSIVE_LEVEL IRQL guaranteed ... NTSTATUS OnIoComplete( PDEVICE_OBJECT pDO, PIRP pIrp, PVOID pContext) { // cast the pContext arg into what it really is: // an Event pointer. PEVENT pEvent = (PEVENT) pContext; // Raise the Event flag to signal waiting thread KeSetEvent(pEvent, 0, FALSE); // Hold off further higher level processing // until this level completes: return STATUS_MORE_PROCESSING_REQUIRED; }
Bus Driver Plug and Play RequestsIn the unusual circumstance where a bus driver or filter must be written, it should be noted that some PnP IRP requests must be handled by such a driver. These minor code handlers are additional requirements above and beyond those already listed and are described in Table 9.7.
|