52964.fb2
The last chapter went through the theory of Plug and Play (PnP) and the device stack. This chapter looks at how to implement Plug and Play in practice, with a discussion of the advanced topics of Plug and Play Notification and Bus drivers.
The Wdm2 example includes full PnP and Power Management facilities. Power Management is described in the next chapter.
The Wdm1Test user mode application tests some of the Wdm2 PnP functionality. The Wdm1Notify Win32 program displays PnP Notification device change events for the Wdm2 device interface.
Supporting Plug and Play primarily means implementing an AddDevice routine and an IRP_MJ_PNP handler. This PnP IRP has eight minor function codes that most WDM drivers need to support.
• IRP_MN_START_DEVICE (Start Device)
• IRP_MN_QUERY_REMOVE_DEVICE (Query Remove)
• IRP_MN_REMOVE_DEVICE (Remove Device)
• IRP_MN_CANCEL_REMOVE_DEVICE (Cancel Remove)
• IRP_MN_STOP_DEVICE (Stop Device)
• IRP_MN_QUERY_STOP_DEVICE (Query Stop)
• IRP_MN_CANCEL_STOP_DEVICE (Stop Device)
• IRP_MN_SURPRISE_REMOVAL (Surprise Removal)
Looking at this list, it might not seem too complicated to handle Plug and Play in a driver. In fact, there are many things to get right. At a basic level this means:
• Coping with adding and removing devices
• Getting resource assignments
• Handling Query Stop and Query Remove messages
• Handling Stop messages
• Handling Surprise Removal messages
However, as will be shown, it soon becomes apparent that you must also do the following tasks:
• Allow only I/O requests while the device is started
• Not allow a device to be removed while there are any open handles
• Queue I/O requests while the device is not started
• Wait until any I/O requests have completed before handling remove requests
• Process Start Device messages after lower devices have started
• Pass unsupported IRPs down the stack
The rest of this chapter will look at the theory behind all the important PnP messages. The Wdm2 example driver shows how to implement most of the related tasks listed above. However, the most complicated — queuing I/O requests — is left until Chapter 16.
Refer to the previous chapter if you need to remind yourself of the difference between function and filter drivers, and the different types of device objects that the Wdm2 driver deals with.
When a PnP driver handles an AddDevice message, it means that a new device has be found. Either a bus driver has found the device at power on or when it was inserted, or the user has added it by hand from the Control Panel. As Wdm2 devices are virtual, these have to be added by hand.
The PnP Manager will have worked out which drivers are going to be in the stack. The Physical Device Object (PDO) is created by the bus driver first. Then, going up the stack, each driver's AddDevice routine is called in turn. Chapter 11 describes how the PnP Manager works out which drivers to put in the stack.
If a driver's AddDevice routine fails, any drivers below it (whose AddDevice succeeded) will be sent a Remove Device message. Be prepared to accept a Remove Device message straightaway after your AddDevice routine has completed.
The job of an AddDevice routine is to create and initialize a device object for the current driver to use, and to connect it to the device stack. For function drivers, the device object is called a Functional Device Object (FDO). Filter drivers make a Filter Device Object (Filter DO). As mentioned in the last chapter, PDOs, FDOs, and Filter DOs all use the same DEVICE_OBJECT structure. Microsoft recommends that we use different names to help us remember what sort of driver owns the device object.
When a Wdm2 device is added, the Unknown bus driver makes a PDO for it. The PnP Manager passes the PDO to Wdm2. The Wdm2AddDevice routine, shown later, calls IoCreateDevice to create the Wdm2 Functional Device Object and eventually calls IoAttachDeviceToDeviceStack to attach it to the device stack. In between, the FDO and its device extension are initialized and a device interface for it is set up.
The eventual job of the Remove Device message handler is to stop the device and delete the FDO. In Wdm2, PnpRemoveDeviceHandler, shown later, eventually calls IoDetachDevice to detach the device object from the stack and calls IoDeleteDevice to delete the device object and its device extension memory. Processing Remove Device PnP messages safely is, in fact, a bit more complicated than this, as will be shown. Note that a Remove Device request will be the last IRP a driver receives before it is unloaded.
The Wdm1 driver handles adding a device in its AddDevice routine and its Wdm1Pnp routine handles the remove device minor code IRP_MN_REMOVE_DEVICE.
The AddDevice and IRP_MJ_PNP handlers are called at PASSIVE_LEVEL IRQL in the context of a system thread. Calls to these routines may be issued while other IRPs are running in the same driver. Even in a uniprocessor computer, processing of a Read IRP could have stalled for some reason. A PnP call could then be issued.
AddDevice
In the Wdm2 driver, the code for adding and removing devices is substantially the same as Wdm1. The Wdm2AddDevice routine in Listing 9.1 is exactly the same, apart from new lines initializing some extra fields in the device extension. These all relate to handling PnP and Power IRPs correctly and are explained in due course.
Most of the PnP handling code is in the Wdm2 Pnp.cpp module. Some changes have been made to the Dispatch.cpp code from the Wdm1 version. A new module DeviceIo.cpp handles device starting and stopping.
Listing 9.1 Wdm2 Wdm2AddDevice
NTSTATUS Wdm2AddDevice(IN PDRIVER_OBJECT DriverObject, IN PDEVICE_OBJECT pdo) {
DebugPrint("AddDevice");
NTSTATUS status;
PDEVICE_OBJECT fdo;
// Create our Functional Device Object in fdo
status = IoCreateDevice(DriverObject, sizeof(WDM2_DEVICE_EXTENSION), NULL, FILE_DEVICE_UNKNQWN, 0, FALSE, &fdo);
if (NT_ERROR(status)) return status;
// Initialise device extension
PWDM2_DEVICE_EXTENSION dx = (PWDM2_DEVICE_EXTENSION)fdo->DeviceExtension;
dx->fdo = fdo;
dx->pdo = pdo;
dx->UsageCount = 1;
KeInitializeEvent(&dx->StoppingEvent, NotificationEvent, FALSE);
dx->OpenHandleCount = 0;
dx->GotResources = false;
dx->Paused = false;
dx->IODisabled = true;
dx->Stopping = false;
dx->PowerState = PowerDeviceD3;
dx->PowerIdleCounter = NULL;
DebugPrint("FDO is %x",fdo);
// Initialise device power state
POWER_STATE NewState;
NewState.DeviceState = dx->PowerState;
PoSetPowerState(fdo, DevicePowerState, NewState);
// Register and enable our device interface
status = IoRegisterDevicelnterface(pdo, &WDM2_GUID, NULL, &dx->ifSymLinkName);
if (NT_ERROR(status)) {
IoDeleteDevice(fdo);
return status;
}
IoSetDeviceInterfaceState(&dx->ifSymLinkName, TRUE);
DebugPrint("Symbolic Link Name is %T",&dx->ifSymLinkName);
// Attach to the driver stack below us
dx->NextStackDevice = IoAttachDeviceToDeviceStack(fdo.pdo);
// Set fdo flags appropriately
fdo->Flags &= ~DO_DEVICE_INITIALIZING;
fdo->Flags |= DO_BUFFERED_IO;
dx->PowerIdleCounter = PoRegisterDeviceForIdleDetection(pdo, 30, 60, PowerDeviceD3);
return STATUS_SUCCESS;
}
Remove Device handler
The code that handles removing devices has been moved to PnpRemoveDeviceHandler, as shown in Listing 9.2. This is the same as before, apart from calling the PnpStopDevice routine, which is explained in the following text.
Listing 9.2 Wdm2 PnpRemoveDeviceHandler
NTSTATUS PnpRemoveDeviceHandler(IN PDEVICE_OBJECT fdo, IN PIRP Irp) {
PWDM2_DEVICE_EXTENSION dx=(PWDM2_DEVICE_EXTENSION)fdo->DeviceExtension;
DebugPrintMsg("PnpRemoveDeviceHandler");
// Wait for I/O to complete and stop device
PnpStopDevice(dx);
// Pass down stack and carry on immediately
NTSTATUS status = PnpDefaultHandler(fdo, Irp);
// disable device interface
IoSetDeviceInterfaceState(&dx->ifSymLinkName, FALSE);
RtlFreeUnicodeString(&dx->ifSymLinkName);
// unattach from stack if (dx->NextStackDevice)
IoDetachDevice(dx->NextStackDevice);
// delete our fdo
IoDeleteDevice(fdo);
return status;
}
Main PnP IRP Handler
The main IRP_MJ_PNP dispatch routine, Wdm2Pnp, has changed considerably from Wdm1, as shown in Listing 9.3. The bulk of the code is a switch statement based on the PnP minor function code. Most of the interesting minor function handling is delegated to subsidiary routines, but some are handled inline. All other minor function codes are handled by the PnpDefaultHandler routine. The PnpQueryCapabilitiesHandler routine is described in the chapter on Power Management.
Listing 9.3 Wdm2 Wdm2Pnp
NTSTATUS Wdm2Pnp(IN PDEVICE_OBJECT fdo, IN PIRP Irp) {
PWDM2_DEVICE_EXTENSION dx=(PWDM2_DEVICE_EXTENSION)fdo->DeviceExtension;
DebugPrint("PnP %I",Irp);
if (!LockDevice(dx)) return CompleteIrp(Irp, STATUS_DELETE_PENDING, 0);
// Remember minor function
PIO_STACK_LOCATION TrpStack = IoGetCurrentIrpStackLocation(Irp);
ULONG MinorFunction = IrpStack->MinorFunction;
NTSTATUS status = STATUS_SUCCESS;
switch (MinorFunction) {
case IRP_MN_START_DEVICE:
status = PnpStartDeviceHandler(fdo,Irp);
break;
case IRP_MN_QUERY_REMOVE_DEVICE:
status = PnpQueryRemoveDeviceHandler(fdo,Irp);
break;
case IRP_MN_SURPRISE_REMOVAL:
status = PnpSurpriseRemovalHandler(fdo, Irp);
break;
case IRP_MN_REMOVE_DEVICE:
status = PnpRemoveDeviceHandler(fdo,Irp);
return status;
case IRP_MN_QUERY_STOP_DEVICE:
dx->Paused = true;
dx->IODisabled = true;
status = PnpDefaultHandler(fdo,Irp);
break;
case IRP_MN_STOP_DEVICE:
status = PnpStopDeviceHandler(fdo,Irp);
break;
case IRP_MN_QUERY_CAPABILITIES:
status = PnpQueryCapabilitiesHandler(fdo.Irp);
break;
case IRP_MN_CANCEL_REMOVE_DEVICE: // fall thru case IRP_MN_CANCEL_STOP_DEVICE:
dx->Paused = false;
dx-> IODisabled = false;
status = PnpDefaultHandler(fdo,Irp);
break;
default:
status = PnpDefaultHandler(fdo,Irp);
}
UnlockDevice(dx);
#if DBG
if (status!=STATUS_SUCCESS) DebugPrint("PnP completed %x",status);
#endif
return status;
}
PnpDefaultHandler (Listing 9.4) passes the PnP IRP down the device stack for processing by all the lower device drivers. Call IoCallDriver whenever you want a driver to process an IRP. If you have created an IRP from scratch, you must set up all the IRP and IRP stack fields correctly. The different ways of allocating and sending your own IRPs are discussed in Chapter 21.
For PnpDefaultHandler, all the IRP structure fields are already set up correctly in the existing PnP IRP that is to be passed onto the next driver. What about the IRP stack?
One IRP stack location is reserved for each possible device in a device stack. When an IRP is passed to the next driver, the next stack location must be set up for it. However, in this case, the Wdm2 driver is never going to need to look at the IRP or its stack location again. IoSkipCurrentIrpStackLocation does not copy the current stack location to the next one. In fact, it simply sets up the IRP internally so that the next driver's call to IoGetCurrentlrpStackLocation returns the same stack location as Wdm2 saw.
You need to set up the next IRP stack location properly if you are going to inspect the IRP processing results or even simply wait for the IRP to complete. Use the routine IoCopyCurrentIrpStackLocationToNext if you simply want to copy the current stack location without changing any of the information. An example of this function is given in Listing 9.4 when Wdm2 waits for an IRP to be processed by all the lower drivers.
IoCallDriver, IoSkipCurrentlrpStackLocation, IoCopyCurrentlrpStackLocationToNext, and the other relevant function IoSetCompletionRoutine must be called at DISPATCH_LEVEL IRQL or lower.
PnpDefaultHandler therefore simply sets up the stack location and passes the IRP to the next lower driver in the device stack. It does not wait for the IRP to complete. It returns the status code that IoCallDriver returns.
Listing 9.4 Wdm2 PnpDefaultHandler
NTSTATUS PnpDefaultHandler(IN PDEVICE_OBJECT fdo, IN PIRP Irp) {
DebugPrintMsg("PnpDefaultHandler");
PWDM2_DEVICE_EXTENSI0N dx=(PWDM2_DEVICE_EXTENSION)fdo->DeviceExtension;
IoSkipCurrentIrpStackLocation(Irp);
return IoCallDriver(dx->NextStackDevice, Irp);
}
Before continuing with the rest of the PnP implementation, it is worth looking again at the Plug and Play message and state diagram that was shown in the last chapter.
Figure 9.1 shows the main theoretical device states that a device can be in and the messages that are sent to change between these states. As mentioned before, a message such as START_DEVICE in this diagram corresponds to a PnP IRP with a minor code of IRP_MN_START_ DEVICE.
Note that I said "theoretical" device states. There are no visible flags in the kernel device structure that say which state a device is in. Wdm2 has to maintain its own state variables.
Another important point to note is that your code should be prepared to accept more-or-less any message from any state. The DDK documentation says in at least two places that an unexpected message may occasionally be sent when in one particular state.
Figure 9.1 Plug and Play device states and messages
The device extension for Wdm2 device objects is shown in Listing 9.5. Four state flags are used to ensure that I/O requests are only begun when the device is in the Started state.
Paused | Device has a remove pending or stop pending |
GotResources | Device running normally or paused (i.e., not stopped) |
IODisabled | Paused or stopped |
Stopping | Device is in process of being removed or stopped |
The GotResources flag is set when the device has retrieved and allocated any hardware resources that it needs. In Figure 9.1, the GotResources flag is set when the device is in the Started, Stop pending, and Remove Pending states.
The Paused flag is set when the device is in the Stop Pending or Remove Pending States.
The IODisabled flag is set when GotResources is false or Paused is true (i.e., in the Stoppending, Remove Pending, Stopped, and Surprise removed states).
The Stopping flag is used during the processing of Remove Device and Stop Device messages, as described in the following text.
In the Device not present state, a device object and its flags simply do not exist.
Some devices may want to have an InterruptsEnabled flag, as well, to indicate when device interrupts are enabled.
Listing 9.5 Wdm2 device extension
typedef struct _WDM2_DEVICE_EXTENSION {
PDEVICE_OBJECT fdo;
PDEVICE_OBJECT NextStackDevice;
UNICODE_STRING ifSymLinkName;
bool GotResources; // Not stopped
bool Paused; // Stop or remove pending
bool IODisabled; // Paused or stopped
LONG OpenHandleCount; // Count of open handles
LONG UsageCount; // Pending I/O Count
bool Stopping; // In process of stopping
KEVENT StoppingEvent; // Set when all pending I/O complete
DEVICE_POWER_STATE PowerState; // Our device power state
PULONG PowerIdleCounter; // Device idle counter
// Resource allocations
bool GotPortOrMemory;
bool PortInIOSpace;
bool PortNeedsMapping;
PUCHAR PortBase;
PHYSICAL_ADDRESS PortStartAddress;
ULONG PortLength;
bool GotInterrupt;
ULONG Vector;
KIRQL Irql;
KINTERRUPT_MODE Mode;
KAFFINITY Affinity;
PKINTERRUPT InterruptObject;
} WDM2_DEVICE_EXTENSION, *PWDM2_DEVICE_EXTENSION;
The GotResources flag indicates that Wdm2 has been assigned its resources. Wdm2 does not use any hardware resources. However, it still needs this flag to indicate the state it is in.
The Paused flag has no hardware interpretation. It is simply a way of stopping IRPs from starting when in the Stop Pending or Remove Pending states. In a full PnP implementation, IRPs must be queued while the Paused flag is set. Wdm2 should never get in the Stop Pending state, as it has no resources to reallocate. It should only be in the Remove Pending state briefly. For simplicity sake, Wdm2 does not queue IRPs. Chapter 16 gives this subject the full airing it deserves.
The IODisabled flag is provided so that the dispatch routines have to check only one flag quickly, not both GotResources and Paused. Each normal IRP dispatch routine, therefore, has the following code at the top that fails the IRP straightaway with an appropriate error code if the device is disabled.
if (dx->IODisabled) return CompleteIrp(Irp, STATUS_DEVICE_NOT_CONNECTED, 0);
To recap, three flags in the Wdm2 device extension are used to ensure that normal I/O requests are permitted only when the Wdm2 device is fully started. In fact, there will be very few times in the life of a Wdm2 device when I/O requests are not permitted. (Strictly speaking, I could have combined the Paused and IODisabled flags, but it is clearer to have them separate.)
When a device is not fully started, a full PnP implementation ought to queue I/O request IRPs.
Consider the various PnP stop messages. These occur when some new hardware has been added to the system or a new device has been plugged in. The PnP Manager may decide that it can only accommodate the new device by reassigning the resources that an existing device currently uses. It does this by issuing a Query Stop message first. If all devices in the stack agree that a stop can take place, then the PnP Manager issues a Stop Device request. If any of the drivers in the stack do not want the device stopped, the PnP Manager issues a Cancel Stop message. It then probably informs the user that there are not enough resources available currently and so a restart is necessary. When the resources have been reassigned, it sends a StartDevice message with the new resource allocations.
During this entire process, it seems reasonable that I/O requests on existing devices carry on as normal. In practice, this means not starting any new requests and holding them in a queue for processing when the device is started again. A user might therefore notice a slight pause in proceedings while resources are reassigned, but I/O requests should not fail.
The DDK documentation recommends that drivers do not start any new I/O requests after a Query Stop or Query Remove message has been received. This lets any following stop or remove request proceed quickly.
The end result is that IRPs ought to be queued when in the Stop Pending, Remove Pending, and Stopped states. In the Wdm2 driver, this means that IRPs ought to be queued when the IODisabled flag is true. However, as stated before, Wdm2 does not hold IRPs, as this is a complicated subject to be covered in Chapter 16.
As shown in Listing 9.3, Wdm2 handles Query Stop message by setting the Paused and IODisabled flags before passing the IRP down the stack. The Query Remove request does the same job. The Cancel Stop and Cancel Remove messages undo these actions by clearing Paused and IODisabled.
What happens if the user asks to remove the Wdm2 device while there are open handles to it?
The PnP Manager sends a Query Remove request to the driver. The simplest approach is to refuse to let a device be removed while there are any open handles. The DDK documentation says that the Query Remove must be failed if "there are open handles that cannot be closed".
Wdm2 uses the simple approach. It keeps a count of open handles to a device in the OpenHandleCount variable in the device extension. OpenHandleCount is initialized to zero when the device is created in Wdm2AddDevice. The InterlockedIncrement and InterlockedDecrement routines are used in the Wdm2Create and Wdm2Close dispatch routines, respectively, to maintain this count safely.
InterlockedIncrement(&dx->OpenHandleCount);
Listing 9.6 shows the complete Query Remove handler for Wdm2. If OpenHandleCount is greater than zero, PnpQueryRemoveDeviceHandler simply fails the IRP straightaway. Notice that it does not need to pass the PnP IRP down the stack as it is failing it. Instead, it just completes the IRP with the STATUS_UNSUCCESSFUL status code.
If there are no open handles, Wdm2 sets its Paused and IODisabled flags, as discussed before. However, in this case, PnpQueryRemoveDeviceHandler must pass the IRP down the stack in PnpDefaultHandler to give lower devices a chance to reject the Query Remove IRP.
Listing 9.6 Wdm2 PnpQueryRemoveDeviceHandler
NTSTATUS PnpQueryRemoveDeviceHandler(IN PDEVICE_OBJECT fdo, IN PIRP Irp) {
PWDM2_DEVICE_EXTENSION dx=(PWDM2_DEVICE_EXTENSION)fdo->DeviceExtension;
DebugPrintMsg("PnpQueryRemoveDoviceHandler");
if( dx->OpenHandleCount>0) {
DebugPrint("PnpQueryRemoveDeviceHandler: %d handles still open", dx->OpenHandleCount);
return CompleteIrp(Irp, STATUS_UNSUCCESSFUL, 0);
}
dx->Paused = true;
dx->IODisabled = true;
return PnpDefaultHandler(fdo,Irp);
}
You must be careful to process PnP IRPs at the right time. When a Start Device message is received for a USB device, for example, the USB drivers must enable the device at the bus level first. It is only then that the function drivers above can access the device. In fact, when processing the Start Device message, the drivers must process the message in order, going up the device stack.
Similar considerations apply when processing Stop, Remove, and Surprise Removal messages. In these cases, all the drivers in the stack must process the IRP first, in order going down the stack. Each driver must do whatever it needs to do to stop its device before the lower drivers pull the rug out from under its feet.
Handle the Cancel Remove and Cancel Stop requests on the way up the stack so that all the lower devices have restarted. However, the Wdm2 driver enables requests straightaway and then passes the IRP down the stack for processing.
The other PnP messages are usually processed by drivers on the way down the device stack. However, in a few circumstances, you may wish to see what results the lower drivers have produced.
WDM drivers can process IRPs in both these orders. So far, I have only shown how to process IRPs in order going down the stack. I shall now look at how to process IRPs in the other order.
IRP Completion Routines
As mentioned before, the IoCallDriver routine is used to call another driver. It is important to realize that IoCallDriver may return before the IRP has been completely processed. In this case, IoCallDriver returns STATUS_PENDING.
If a driver wants to process an IRP when all lower drivers have completed processing it, the driver must set a completion routine for the IRP. The completion routine is called when all the lower drivers have finished processing the IRP. The completion routine is called in an arbitrary context. I now show how a completion routine signals that it has been run using a kernel event.
Listing 9.7 shows how the ForwardIrpAndWait routine forwards an IRP to the lower drivers and waits for its completion. As ForwardIrpAndWait waits for the completion routine event to become signalled, it must run at PASSIVE_LEVEL IRQL. Waiting for dispatcher objects, such as events, is covered in full in Chapter 14. PnP IRPs are always called at PASSIVE_LEVEL, so it is safe to call ForwardIrpAndWait.
To set a completion routine, the next IRP stack location must be set up correctly. As described previously, IoCopyCurrentIrpStackLocationToNext copies all the current stack location parameters. Having done this, IoSetCompletionRoutine is used to set the completion routine to the ForwardedIrpCompletionRoutine function. ForwardIrpAndWait is then ready to call the next driver using IoCallDriver.
The last three BOOLEAN parameters of IoSetCompletionRoutine specify the circumstances in which you want the completion routine called. If the first BOOLEAN, InvokeOnSuccess, is TRUE, the completion routine is called if the IRP completes successfully. The other two BOOLEAN parameters, InvokeOnError and InvokeOnCancel, state whether the completion routine should be called if an error is returned or the IRP is cancelled. In ForwardIrpAndWait, I want the completion routine called in all circumstances, so all these parameters are set to TRUE.
ForwardIrpAndWait now has two tasks to perform. The completion routine has to signal when it has run, and the main code must wait for this signal. The signalling mechanism is a kernel event, which is basically the same as its Win32 equivalent. Chapter 14 discusses kernel events in full. The event is initialized to the nonsignalled state using KeInitializeEvent. When the completion routine runs, it simply calls KeSetEvent to set the event into the signalled state. ForwardIrpAndWait uses KeWaitForSingleObject to wait for the event to become signalled.
A completion routine has a standard prototype, passing the device object, the IRP, and a context pointer. In this case, ForwardIrpAndWait uses a pointer to the event as the context pointer. The context pointer is set in the IoSetCompletionRoutine call. When the IRP has been processed by all the lower drivers, ForwardedIrpCompietionRoutine is called. It simply sets the event.
ForwardedIrpCompletionRoutine returns a status of STATUS_MORE_PROCESSING_REQUIRED. This means that the IRP has not been completed by this driver and some other part of the driver will complete it. The only other alternative is to return STATUS_SUCCESS, in which case the IRP continues its journey back up the device stack.
IoCallDriver returns STATUS_PENDING if the IRP has not completed its processing by the lower drivers. If this value is returned, ForwardIrpAndWait must wait for the completion routine to run. Once the event has been set, the call to KeWaitForSingleObject returns. ForwardIrpAndWait retrieves the status returned by the lower drivers from the IRP's IoStatus.Status field.
If IoCallDriver returns any value apart from STATUS_PENDING, this means that the IRP has been processed by all the lower drivers. The completion routine has been run and the event set. However, ForwardIrpAndWait does not need to wait for the event, as it already knows that the IRP has been processed in full.
There is no way to know in advance if IoCallDriver will return a pending status. Therefore, if you want to use an IRP after calling IoCallDriver you must set a completion routine.
Completion routines may run at DISPATCH_LEVEL IRQL or lower in an arbitrary thread context. To be safe, you should assume that it is running at DISPATCH_LEVEL IRQL. This means that the completion routine itself and the context pointer must not be in paged memory. The ForwardIrpAndWait event variable is in the kernel stack. This is normally in non-paged memory. Apparently, the kernel stack can be pageable if a user mode wait is issued. However, as a kernel mode wait is used here, the event memory should be safely nonpaged.
Listing 9.7 Wdm2 Forwarding IRPs and waiting for completion
NTSTATUS Forward IrpAndWait(IN PDEVICE_OBJECT fdo, IN PIRP Irp) {
DebugPrintMsg("ForwardIrpAndWait");
PWDM2_DEVICE_EXTENSION dx=(PWDM2_DEVICE_EXTENSION)fdo->DeviceExtension;
KEVENT event;
KeInitializeEvent(&event, NotificationEvent, FALSE);
IoCopyCurrentIrpStackLocationToNext(Irp);
IoSetCompletionRoutine(Irp, (PIO_COMPLETION_ROUTINE)ForwardedIrpCompletionRoutine, (PVOID)&event, TRUE, TRUE, TRUE);
NTSTATUS status = IoCallDriver(dx->NextStackDevice, Irp);
if (status==STATUS_PENDING) {
DebugPrintMsg("ForwardIrpAndWait: waiting for completion");
KeWaitForSingleObject(&event, Executive, KernelMode, FALSE, NULL);
status = Irp->IoStatus.Status;
}
#if DBG
if (status!=STATUS_SUCCESS) DebugPrint("ForwardIrpAndWait: completed %x",status);
#endif
return status;
}
NTSTATUS ForwardedIrpCompletionRoutine(IN PDEVICE_OBJECT fdo, IN PIRP Irp, IN PKEVENT ev) {
KeSetEvent(ev, 0, FALSE);
return STATUS_MORE_PROCESSING_REQUIRED;
}
Most completion routines should include the following code to remember if any lower driver has pended an IRP. If any driver in a stack marks an IRP as pending then the I/O Manager has to take special steps to ensure that the IRP results are returned to user space in the correct thread context.
Calling IoMarkIrpPending sets an internal pending flag in the current IRP stack location. When a lower driver completes an IRP, IoCompleteRequest sets the Irp->PendingReturned flag equal to the IRP stack internal flag. To remember that the IRP was pended, a completion routine must therefore call IoMarkIrpPending if Irp->PendingReturned is set.
if (Irp->PendingReturned) IoMarklrpPending(Irp);
PnP Start Device Handler
The Wdm2 driver handles the PnP Start Device message in its PnpStartDeviceHandler routine shown in Listing 9.8. The state diagram in Figure 9.1 shows that the Start Device message is received either from the Awaiting resources state or the Stopped state.
PnpStartDeviceHandler can then do whatever it needs to do to start its device. Wdm2 delegates this to the routine StartDevice, which will be covered later. StartDevice is passed a pointer to the allocated resources, in the IRP stack Parameters.StartDevice.AllocatedResourcesTranslated field.
If StartDevice succeeds, it will have set the GotResources flag. PnpStartDeviceHandler also then clears the Paused and IODisabled flags.
The drivers above Wdm2 in the device stack could fail the Start Device IRP. If this happens, Wdm2 and the other lower drivers will be sent a Remove Device request.
Listing 9.8 Wdm2 PnP start device handler
NTSTATUS PnpStartDeviceHandle(IN PDEVICE_OBJECT fdo, IN PIRP Irp) {
DebugPrintMsg("PnpStartDeviceHandler");
PWDM2_DEVICE_EXTENSION dx=(PWDM2_DEVICE_EXTENSION)fdo->DeviceExtension;
PIO_STACK_LOCATION IrpStack = IoGetCurrentIrpStackLocation(Irp);
NTSTATUS status = ForwardIrpAndWait(fdo, Irp);
if (!NT_SUCCESS(status)) return CompleteIrp(Irp, status, Irp->IoStatus.Information);
DebugPrint("PnpStartDeviceHandler: post-processing");
status = StartDevice(dx, IrpStack->Parameters.StartDevice.AllocatedResourcesTranslated);
if (NT_SUCCESS(status)) {
dx->Paused = false;
dx->IODisabled = false;
}
return Complete Irp(Irp, status, 0);
}
When a user wants to remove a Wdm2 device, the PnP Manager always asks if it is all right to remove it using a Query Remove request. As described previously, Wdm2 agrees to a removal request only if there are no open handles to the device. Therefore, for the main RemoveDevice request, the Wdm2 driver can be certain that there is no I/O in progress on the device.
However, some devices are hot-pluggable (i.e., a user can ruthlessly pull out the plug, or it could be bashed out by mistake). In this case, a PnP driver does not receive a Query Remove message. Instead, in Windows 98, it simply gets a Remove Device message. Windows 2000 sends a Surprise Removal message first and then sends a Remove Device request when all the open handles are closed.
A driver needs to cope with surprise removals in the best way possible. A SurpriseRemoval IRP must succeed. Obviously, the driver needs to stop any further I/O as soon as possible. However, one or more I/O requests may be in progress or queued up. It is fairly straightforward to cancel all the IRPs in a device queue; see Chapter 16 for details. However, the best method to handle I/O IRPs that are in progress is more complicated.
If you cannot somehow fail any I/O requests in progress, the recommended solution is to wait for them to complete. The main IRP processing routines are more than likely to be using the device or device extension, so these structures cannot be deleted until it is certain that they will not be used again. The main IRP processing routines almost certainly encounter some sort of problem. Make sure that they can handle a device being removed. A routine might be expecting an interrupt. Chapter 17 later shows how two different types of timers can be used to provide a time-out for I/O requests.
What is best way to keep track of how many I/O requests are in progress? The answer is to laboriously keep track of the number of open I/O requests in a UsageCount field in the device extension. A call to LockDevice is made at the beginning of each IRP request to increment UsageCount. UnlockDevice is called when each IRP is completed to decrement UsageCount.
UsageCount is set to one when the device is created. To remove or stop a device, an extra call to UnlockDevice is made. If there are no other IRPs in progress, this call should have decremented UsageCount to zero. If there are IRPs in progress, UsageCount will at this stage have a value greater than zero. However, when these IRPs finish, UnlockDevice will be called and so UsageCount will in due course become zero.
The Remove Device or Stop Device PnP IRP needs to know when UnlockDevice decrements UsageCount to zero. Another kernel event, StoppingEvent, is used for this purpose. UnlockDevice sets StoppingEvent into the signalled state when UsageCount drops to zero.
Listing 9.9 shows the PnpStopDevice routine that the Wdm2 driver uses to stop a device. It is called by the Remove Device, Surprise Removal, and Stop Device PnP IRP handlers. PnpStopDevice sets the IODisabled flag straightaway to stop any new requests from starting. The device is already stopped if GotResources is false, so no more processing is required.
PnpStopDevice resets StoppingEvent and then calls UnlockDevice twice. The first undoes the call to LockDevice at the start of the main PnP IRP handler. The second will reduce UsageCount to zero, either straightaway or when all the IRPs in progress complete. PnpStopDevice then waits for StoppingEvent to be set by UnlockDevice.
PnpStopDevice then calls the StopDevice routine, described later. StopDevice resets the GotResources flag. PnpStopDevice's last task is to call LockDevice again to increment UsageCount again. The main PnP IRP handler calls UnlockDevice in due course to get UsageCount down to its correct value of zero.
Listing 9.9 PnpStopDevice routine
void PnpStopDevice(IN PWDM2_DEVICE_EXTENSION dx) {
// Stop I/O ASAP
dx->IODisabled = true;
// Do nothing if we're already stopped
if (!dx->GotResources) return;
// Wait for any pending I/O operations to complete
dx->Stopping = true;
KeResetEvent(&dx->StoppingEvent);
UnlockDevice(dx);
UnlockDevice(dx);
KeWaitForSingleObject(&dx->StoppingEvent, Executive, KernelMode, FALSE, NULL);
DebugPrint("PnpStopDevice: All pending I/O completed");
dx->Stopping = false;
// Stop our device before passing down
StopDevice(dx);
// Bump usage count back up again
LockDevice(dx);
LockDevice(dx);
}
Listing 9.10 shows how a typical I/O IRP dispatch routine fits into the Wdm2 device Plug and Play handling. Initially, it checks the IODisabled flag and then it tries to lock the device using LockDevice. If LockDevice fails, the correct response is to return STATUS_DELETE_PENDING. The Wdm2 PnpStopDevice routine always sets the IODisabled flag first, so the dispatch routine will never return STATUS_DELETE_PENDING. This could be fixed by adding another suitable flag to the device extension.
The dispatch routine ends by completing the IRP and calling UnlockDevice. If the IRP is not completed straightaway, do not call UnlockDevice straightaway. Instead, wait until the IRP is completed or cancelled before calling UnlockDevice.
All dispatch routines should include calls to LockDevice when an IRP arrives and UnlockDevice when an IRP is completed or passed onto another driver. PnP and WMI IRP handlers should also call these routines.
Listing 9.10 Dispatch routine entry and exit code
NTSTATUS Wdm2Read( IN PDEVICE_OBJECT fdo, IN PIRP Irp) {
PWDM2_DEVICE_EXTENSION dx = (PWDM2_DEVICE_EXTENSION)fdo->DeviceExtension;
if (dx->IODisabled) return CompleteIrp(Irp, STATUS_DEVICE_NOT_CONNECTED, 0);
if (!LockDevice(dx)) return CompleteIrp(Irp, STATUS_DELETE_PENDING, 0);
// …
// Complete IRP
CompleteIrp(Irp,status,BytesTxd);
UnlockDevice(dx);
return status;
}
PnpStopDevice also sets another flag in the device extension, Stopping, to true while it is waiting for all pending I/O to complete. Although not strictly necessary in Wdm2, it is another way of forcing an IRP routine to see if it is OK to start during the call to LockDevice.
Listing 9.11 shows the LockDevice and UnlockDevice routines. They use InterlockedIncrement and InterlockedDecrement calls to ensure that UsageCount is maintained safely in multiprocessor systems. If UnlockDevice finds that UsageCount has decremented to zero, it sets the StoppingEvent flag. LockDevice also checks the Stopping flag. If it is set, a PnP IRP is trying to stop the device and so the caller should not continue. In this case, UsageCount is decremented and StoppingEvent is set, if appropriate, before false is returned.
Listing 9.11 LockDevice and UnlockDevice routines
bool LockDevice(IN PWDM2_DEVICE_EXTENSION dx) {
InterlockedIncrement(&dx->UsageCount);
if (dx->Stopping) {
if (InterlockedDecrement(&dx->UsageCount)==0) KeSetEvent(&dx->StoppingEvent, 0, FALSE);
return false;
}
return true;
}
void UnlockDevice(IN PWDM2_DEVICE_EXTENSION dx) {
LONG UsageCount = InterlockedDecrement(&dx->UsageCount);
if (UsageCount==0) {
DebugPrintMsg("UnlockDevice: setting StoppingEvent flag");
KeSetEvent(&dx->StoppingEvent, 0, FALSE);
}
}
To wrap up the last loose ends, the handlers for the Stop Device and Surprise Removal messages are exactly the same, as shown in Listing 9.12. They both simply call PnpStopDevice and call PnpDefaultHandler to pass the IRP down the stack.
Listing 9.12 Stop device handler
NTSTATUS PnpStopDeviceHandler(IN PDEVICE_OBJECT fdo, IN PIRP Irp) {
DebugPrintMsg("PnpStopDeviceHandler");
PWDM2_DEVICE_EXTENSION dx=(PWDM2_DEVICE_EXTENSI0N)fdo->DeviceExtension;
// Wait for I/O to complete and stop device
PnpStopDevice(dx);
return PnpDefaultHandler(fdo, Irp);
}
W2000 Device Locking
W2000 provides standard routines to replace our LockDevice and UnlockDevice routines, and the associated variables. You must provide an IO_REMOVE_LOCK field in your device extension. Initialise this field using IoInitializeRemoveLock function in your AddDevice routine. Replace all calls to LockDevice with IoAcquireRemoveLock and calls to UnlockDevice with IoReleaseRemoveLock. In the PnpStopDevice routine above, replace the code that waits for all pending I/O to complete with a call to IoReleaseRemoveLockAndWait.
The DDK recommended that you call IoAcquireRemoveLock whenever you pass out a reference to your code, e.g., a timer, DPC, or any other call-back routine. Call IoReleaseRemoveLock when these call-backs are disabled, i.e., usually in your Remove Device handler.
A Start Device PnP IRP passes a list of the resources that have been assigned to the device. Although the Wdm2 driver never gets any resources, the code in DeviceIo.cpp goes through the steps to find these out. In addition, there is a lot of commented out code that shows how to use the resources. The WdmIo driver described in Chapters 15-17 show how hardware resources are actually used.
DeviceIo.cpp primarily contains the StartDevice and StopDevice routines. StartDevice must get the list of assigned resources, check that the resources are suitable, allocate them, and do any hardware-related initialization for the driver. If that all goes well, it should set the GotResources flag to true. StopDevice must release any hardware resources and reset the GotResources flag.
StartDevice calls RetrieveResources to find out which resources have been assigned to it by the PnP Configuration Manager. The Start Device PnP message passes two fields in the IRP stack Parameters.StartDevice structure, AllocatedResources, and AllocatedResourcesTranslated. AllocatedResources lists the resources in "raw form". This is how the device itself will see addresses, etc. Use AllocatedResources to program the device itself. Usually the "translated form" of the resource list, AllocatedResourcesTranslated, is of more interest to the driver. Use the AllocatedResourcesTranslated to connect to interrupt vectors, map I/O space, and memory.
If there are no resources assigned, W2000 sets these fields to NULL. In W98, a resource list is allocated but the "partial resource" count is zero.
Table 9.1 shows the raw and translated resource assignments for an I/O Port and two different interrupts in Windows 98 and Windows 2000.
Table 9.1 Resource assignments
I/O Port | Address | |||
---|---|---|---|---|
AllocatedResources | 378 | |||
AllocatedResourcesTranslated | 378 | |||
Interrupt (W98) | Vector | IRQL | Affinity | Mode |
AllocatedResources | 7 | 7 | 1 | Latched |
AllocatedResourcesTranslated | 37 | 20 | 1 | Latched |
Interrupt (W2000) | Vector | IRQL | Affinity | Mode |
AllocatedResources | 3 | 3 | -1 | Latched |
AllocatedResourcesTranslated | 33 | 24 | 1 | Latched |
Partial Resource Descriptors
A resource list is an array of full resource descriptors. A full resource descriptor has a partial resource list, an array of partial resource descriptors. The relevant structures (CM_RESOURCE_ LIST, CM_FULL_RESOURCE_DESCRIPTOR, CM_PARTIAL_RESOURCE_LIST, and CM_PARTIAL_ RESOURCE_DESCRIPTOR) are also covered in Chapter 18.
WDM drivers have only one full resource descriptor[21]. Therefore, inspect the array of partial resource descriptors to see what resources have been assigned. The resource descriptors are given in no particular order.
Table 9.2 shows the different types of resource information that can be found in a partial resource descriptor. The Type field specifies what resource is described. There is also a ShareDisposition field that specifies how the resource can be shared. One of the following values can be found: CmResourceShareDeviceExclusive, CmResourceShareDriverExclusive, or CmResourceShareShared.
Table 9.2 Partial resource descriptors
I/O Port | (Type==CmResourceTypePort) |
---|---|
PHYSICAL_ADDRESS | u.Port.Start Port Bus specific start address |
ULONG u.Port.Length | Number of addresses |
USHORT Flags | CM_RESOURCE_PORT_IO or CM_RESOURCE_PORT_MEMORY if you need to map the port into memory with MmMapIoSpace |
Memory | (Type==CmResourceTypeMemory) |
PHYSICAL_ADDRESS | u.Memory.Start Bus specific start address |
ULONG u.Memory.Length | Number of addresses |
USHORT Flags | CM_RESOURCE_MEMORY_READ_WRITE CM_RESOURCE_MEMORY_READ_ONLY CM_RESOURCE_MEMORY_WRITE_ONLY |
Interrupt | (Type==CmResourceTypeInterrupt) |
ULONG u.Interrupt.Level | The interrupt IRQL |
ULONG u.Interrupt.Vector | The interrupt vector |
ULONG u.Interrupt.Affinity | The set of processors to which the interrupt is dispatched. |
USHORT Flags | CM_RESOURCE_INTERRUPT_LEVEL_SENSITIVE CM_RESOURCE_INTERRUPT_LATCHED |
DMA | (Type==CmResourceTypeDma) |
ULONG u.Dma.Channel | System DMA controller channel |
ULONG u.Dma.Port | MCA type device port |
USHORT Flags | See the DDK |
Other resource types | |
UCHAR Type |
|
The RetrieveResources code shown in Listing 9.13 extracts and checks the resources that have been assigned for this device. It first checks whether any resources have been assigned. As Wdm2 does not need any resources, it simply returns STATUS_SUCCESS if there are no resources.
RetrieveResources then goes through the partial resource descriptor list checking for I/O Port, Memory, and Interrupt resource types. The assigned information is stored in the device extension and printed out using DebugPrint. Other resource types result in an error.
Listing 9.13 RetrievereSources routine
NTSTATUS RetrieveResources(IN PWDM2_DEVICE_EXTENSION dx, IN PCM_RESOURCE_LIST AllocatedResourcesTranslated) {
if (AllocatedResourcesTranslated==NULL || AllocatedResourcesTranslated->Count==0) {
DebugPrintMsg("RetrieveResources: No allocated translated resources");
return STATUS_SUCCESS; // or whatever
}
// Get to actual resources
PCM_PARTIAL_RESOURCE_LIST list = &AllocatedResourcesTranslated->List[0].PartialResourceList;
PCM_PARTIAL_RESOURCE_DESCRIPTOR resource = list->PartialDescriptors;
ULONG NumResources = list->Count;
DebugPrint("RetrieveResources: %d resource lists %d resources", AllocatedResourcesTranslated->Count, NumResources);
bool GotError = false;
// Clear dx
dx->GotInterrupt = false;
dx->GotPortOrMemory = false;
dx->PortInIOSpace = false;
dx->PortNeedsMapping = false;
// Go through each allocated resource
for (ULONG i=0; i<NumResources; i++,resource++) {
switch (resource->Type) {
case CmResourceTypePort:
if (dx->GotPortOrMemory) {
GotError = true;
break;
}
dx->GotPortOrMemory = true;
dx->PortStartAddress = resource->u.Port.Start;
dx->PortLength = resource->u.Port.Length;
dx->PortNeedsMapping = (resource->Flags & CM_RESOURCE_PORT_IO)==0;
dx->PortInIOSpace = !dx->PortNeedsMapping;
DebugPrint("RetrieveResources: Port %x%x Length %d NeedsMapping %c",
dx->PortStartAddress.HighPart, dx->PortStartAddress.LowPart, dx->PortLength, dx->PortNeedsMapping);
break;
case CmResourceTypeInterrupt:
dx->GotInterrupt = true;
dx->Irql = (KIRQL)resource->u.Interrupt.Level;
dx->Vector = resource->u.Interrupt.Vector;
dx->Affinity = resource->u.Interrupt.Affinity;
dx->Mode = (resource->Flags = CM_RESOURCE_INTERRUPT_LATCHED) ? Latched : LevelSensitive;
DebugPrint("RetrieveResources: Interrupt vector %x IRQL %d Affinity %d Mode %d",
dx->Vector, dx->Irql, dx->Affinity, dx->Mode);
break;
case CmResourceTypeMemory:
if (dx->GotPortOrMemory) { GotError = true; break; }
dx->GotPortOrMemory = true;
dx->PortStartAddress = resource->u.Memory.Start;
dx->PortLength = resource->u.Memory.Length;
dx->PortNeedsMapping = true;
DebugPrint("RetrieveResources: Memory %x%x Length %d",
dx->PortStartAddress.HighPart, dx->PortStartAddress.LowPart, dx->PortLength);
break;
case CmResourceTypeDma:
case CmResourceTypeDeviceSpecific:
case CmResourceTypeBusNumber:
default:
DebugPrint("RetrieveResources: Unrecognised resource type %d", resource->Type);
GotFrror = true;
break;
}
}
// Check we've got the resources we need
if (GotError /*|| !GotPortOrMemory || !GotInterrupt*/)
return STATUS_DEVICE_CONFIGURATION_ERROR;
return STATUS_SUCCESS;
}
Allocating Resources
The StartDevice routine now allocates the system resources it requires. The Wdm2 driver does not need or expect any resources, so this code is commented out. The WdmIo driver covered in Chapters 15-17 does use system resources. These chapters cover Critical section routines and Interrupts in detail. However, I will briefly introduce these topics now.
For memory mapped I/O ports (with the CM_RESOURCE_PORT_MEMORY flag bit set) you must call MmMapIoSpace to get a pointer that can be used by a driver. Do not forget to call MmUnmapIoSpace when the device is stopped.
dx->PortBase = (PUCHAR)MmMapIoSpace(dx->PortStartAddress, dx->PortLength, MmNonCached);
For ordinary I/O ports and memory, simply use the low 32 bits of the PortStartAddress[22].
dx->PortBase = (PUCHAR)dx->PortStartAddress.LowPart;
For interrupts, IoConnectInterrupt is called to install an interrupt handler. You must be ready to handle an interrupt straightaway, so make sure that everything is set up correctly. It is common to be able to disable interrupts by writing some value to a device register. Write a DisableDeviceInterrupts routine to disable interrupts before calling IoConnectInterrupt and call your EnableDeviceInterrupts routine when you are ready to receive interrupts.
Any code that tries to access some real hardware must synchronize its activities with the interrupt handler. It is no good having an interrupt during a complicated device access procedure. Critical section routines solve this problem. You call KeSynchronizeExecution passing the name of the function you want called. KeSynchronizeExecution raises the IRQL to the correct interrupt level and calls your routine. When it has completed, the IRQL is lowered again. Critical section routines obviously need to be in nonpaged memory and cannot access paged memory.
EnableDeviceInterrupts and DisableDeviceInterrupts are usually Critical section routines and will usually need to be called via KeSynchronizeExecution.
Finally, StartDevice powers up its device, as described in the next chapter.
Port and Memory I/O
There are several standard kernel routines to access I/O ports and memory, as this code snippet shows.
void WriteByte(IN PWDM2_DEVICE_EXTENSION dx, IN ULONG offset, IN UCHAR byte) {
if (dx->PortInIOSpace) WRITE_PORT_UCHAR(dx->PortBase+offset, byte);
else WRITE_REGISTER_UCHAR(dx->PortBase+offset, byte);
}
Read data using the routines with READ in their name and write data with WRITE routines. PORT routines access I/O registers in I/O port space, while REGISTER routines access registers in memory space. Each type has UCHAR, USHORT, and ULONG variants. Finally, BUFFER variants transfer more than one data value. For example, use the following code to read a set of ULONG values from I/O port space into a buffer.
READ_PORT_BUFFER_ULONG( PortBase, Buffer, Count);
As mentioned previously, synchronize all your hardware accesses with any interrupt routines using Critical section routines and KeSynchronizeExecution. Chapter 16 covers Critical section routines.
Device Access
Most WDM drivers do not, in fact, need to access I/O ports and memory or handle interrupts. Instead, they access their devices using the facilities provided by class drivers. For example, a USB client driver uses the oft-mentioned USB Request Blocks (URBs) to access its device.
When a USB client driver handles a PnP Start Device message, it first waits for the IRP to be processed by lower level drivers. It then typically issues one or more URBs to its device before completing its IRP.
For Stop Device messages, a USB client driver might well want to issue one or more URBs to its device before the IRP is sent down the stack.
The Wdm2Test Win32 console application in the Wdm2\exe subdirectory of the book software tests the Wdm2 driver. Wdm1Test is the same as Wdm1Test with only one change, apart from referencing the Wdm2 driver. Wdm2Test halts halfway through, waiting for the user to press a key. While it is waiting, it has a handle to the first Wdm2 device still open.
First, install a Wdm2 device using one of the installation INF files in the book software Wdm2\sys directory. It does not matter whether you use the free or checked build, although the checked build produces DebugPrint trace output.
Wdm2Test tests to see if a Query Remove request is rejected while there are any open handles to a Wdm2 device. Run Wdm2Test but do not press a key when it stops halfway through. Now try to remove the Wdm2 device or reinstall its driver. The request should be rejected. Windows will state that the system must be restarted for the operation to complete.
Check that the Wdm2 driver can be removed or reinstalled when Wdm2Test has completed.
It is very difficult to test the other new aspects of Plug and Play support in the Wdm2 driver. First, Stop Device and Surprise Removal requests should never be issued for virtual devices that have no resources. Second, it is not possible to suddenly remove a Wdm2 device in such a way that the Wdm2 driver has to wait for pending I/O to complete. Do appropriate tests for drivers that have resources and can be suddenly removed.
If the test for open handles is not made, then W2000 in fact will still not allow the device to be removed, as it must contain its own internal reference count for the device. However, W98 would let the device be removed. Any I/O requests on open handles would then simply fail.
The DebugPrint output from Wdm2 shows exactly which Plug and Plug messages are sent by Windows during add device and remove device operations.
Adding a Device
The following PnP calls are made when a Wdm2 device is successfully added, or when the driver for a device is reinstalled. Two of the messages are issued only by Windows 2000.
AddDevice
(W2000) IRP_MN_QUERY_LEGACY_BUS_INFORMATION
IRP_MN_FILTER_RESOURCE_REQUIREMENTS
IRP_MN_START_DEVICE
IRP_MN_QUERY_CAPABILITIES
IRP_MN_QUERY_PNP_DEVICE_STATE
IRP_MN_QUERY_DEVICE_RELATIONS BusRelations
IRP_MN_QUERY_DEVICE_RELATIONS BusRelations (W2000)
Removing a Device
The following PnP messages are sent when a Wdm2 device is successfully removed, or when the driver for a device is reinstalled.
IRP_MN_QUERY_DEVICE_RELATIONS RemoveRelations
IRP_MN_QUERY_REMOVE_DEVICE
IRP_MN_REMOVE_DEVICE
Unknown Status Returns
It is interesting to note the IRP status values are returned by the lower Unknown driver when Wdm2 sends IRPs down the stack. Windows is supposed to set the IRP status return value to STATUS_NOT_SUPPORTED before it is issued to the top of the device stack. If Wdm2 sees this value on return from its PnpDefaultHandler routine, it means that the lower drivers have not processed the IRP or have deliberately not returned STATUS_NOT_SUPPORTED.
For Wdm2, W98 succeeds all the PnP IRPs that it receives.
For Wdm2, W2000 does not process IRP_MN_FILTER_RESOURCE_REQUIREMENTS, IRP_MN_QUERY_PNP_DEVICE_STATE, IRP_MN_QUERY_BUS_INFORMATION, and IRP_MN_QUERY_DEVICE_RELATIONS IRPs.
This section briefly describes the Plug and Plug minor function code IRPs that have not been described in full before. These IRPs are handled by PnP bus drivers. The notes for each function code indicate if it is possible for a function driver to intercept the IRP.
Most function drivers ignore all these PnP IRPs. However, a function driver that performs Power Management may well want to handle IRP_MN_QUERY_CAPABILITIES. The following chapter describes how this is done.
This message tells a driver if its device is in the path of a paging, hibernation, or crash dump file. Do not allow a device to be removed until you are notified that no critical file is on its path.
The Windows 2000 PnP Manager sends this IRP to a device stack so filter and function drivers can adjust the resources required by the device, if appropriate. Function, filter, and bus drivers can handle this request.
The PnP Manager uses this IRP to request the type and instance number of a device's parent bus.
Bus drivers should handle this request for their child devices (PDOs). Function and filter drivers do not handle this IRP.
The PnP Manager sends this IRP to get the capabilities of a device, such as whether the device can be locked or ejected, and various Power Management features. Function and filter drivers can handle this request if they alter the capabilities supported by the bus driver. Bus drivers must handle this request for their child devices.
This IRP is sent twice, both before and after function drivers are loaded and started.
A driver can send one of these IRPs down the stack to see what the bus driver capabilities are.
This IRP asks how this device relates to other devices and comes in five different forms. All forms return an array of pointers to the relevant PDOs.
A BusRelations query asks for the PDOs of all the devices physically present on the bus. EjectionRelations asks which devices are also ejected if this device is ejected. PowerRelations asks which devices are also powered down when this device is powered down. RemovalRelations asks which devices must be removed when this device is removed. TargetDeviceRelation calls ObReferenceObject for the device PDO and returns the PDO.
The PnP Manager uses this IRP to get a device's description or location information. Bus drivers must handle this request for their child devices if the bus supports this information. Function and filter drivers do not handle this IRP.
Parameters.QueryDeviceText.DeviceTextType is either DeviceTextDescription or DeviceTextLocationInformation.Parameters.QueryDeviceText.LocaleId is an LCID specifying the locale for the requested text.
This IRP gets device, hardware, compatible, or instance IDs for a device, depending on whether Parameters.QueryId.IdType is BusQueryDeviceID, BusQueryHardwareIDs, BusQueryCompatibleIDs, or BusQueryInstanceID.
The IRP_MN_QUERY_INTERFACE request enables a driver to export a direct-call interface to other drivers.
The query asks the drivers in the device stack to set any of the state bits shown in Table 9.3. Be careful not to overwrite any bits that are set by other drivers.
Table 9.3 Query device state bits
PNP_DEVICE_DISABLED | The device is physically present but is disabled in hardware. |
PNP_DEVICE_DONT_DISPLAY_IN_UI | Don't display the device in the user interface. The device is physically present but not usable in the current configuration. |
PNP_DEVICE_FAILED | The device is present but not functioning correctly. When both this flag and PNP_DEVICE_RESOURCE_REQUIREMENTS_CHANGED are set, the device must be stopped before the PnP Manager assigns new hardware resources. |
PNP_DEVICE_REMOVED | The device has been physically removed. |
PNP_DEVICE_RESOURCE_REQUIREMENTS_CHANGED | The resource requirements for the device have changed. |
PNP_DEVICE_NOT_DISABLEABLE | The device cannot be disabled. |
If any of the state characteristics change after the initial query, a driver notifies the PnP Manager by calling IoInvalidateDeviceState. In response to a call to IoInvalidateDeviceState, the PnP Manager queries the device's PNP_DEVICE_STATE again.
The PnP Manager uses this IRP to get a device's resource requirements list. Bus drivers must handle this request for their child devices that require hardware resources. Function and filter drivers do not handle this IRP.
The PnP Manager uses this IRP to get a device's boot configuration resources. Bus drivers must handle this request for their child devices that require hardware resources. Function and filter drivers do not handle this IRP.
Bus drivers for buses with configuration space must return the relevant information for their child devices. Filter and function drivers do not handle this request.
This request is used to lock or unlock a device. Bus drivers must handle this IRP for a child device that supports device locking. Function and filter drivers do not handle this request.
Bus drivers must write the given information into the configuration space of the child device. Function and filter drivers do not handle this request.
Plug and Play Notification informs Win32 programs and device drivers of device change events that interest them, such as device arrival and removal. They can also refuse requests to remove a device. PnP Notification uses a device interface GUID or a file handle to identify which devices are of interest.
Win32 applications and device drivers register to receive PnP notifications so that they can cope if a device is about to be removed, or if they want to use a device that has just been added. For example, if a program is in the middle of a long transfer, it could refuse permission for a device to be removed. In addition, device drivers can be informed of all devices that expose the required device interface.
A Win32 program calls RegisterDeviceNotification to register that it wants to receive PnP Notification device change messages. You must pass either a window handle or (in W2000 only) a service status handle. The NotificationFilter parameter points a structure that says what type of events you want to receive.
To receive events about devices with a particular device interface, pass a DEV_BROADCAST_DEVICEINTERFACE structure pointer to RegisterDeviceNotification. Set the dbcc_classguid field to the device interface GUID of interest. Set the dbcc_size and dbcc_devicetype fields appropriately.
I have not tried it, but passing a DEV_BROADCAST_HANDLE structure pointer should let you receive events from one open file handle, including custom events.
Do not forget to call UnregisterDeviceNotification when you do not wish to receive any further PnP Notification events.
Device Change Message
Each PnP Notification event is sent to a Win32 program as a WM_DEVICECHANGE message. In MFC applications, this appears in an OnDeviceChange handler.
Table 9.4 lists the main event types returned in wParam. lParam may point to an appropriate structure. For device interface change events, it is a DEV_BROADCAST_DEVICEINTERFACE structure whose dbcc_name field contains the device filename.
For a WM_DEVICECHANGE message that asks permission to remove a device, the handler must return TRUE to agree or BROADCAST_QUERY_DENY[23] to deny the requests. Returning FALSE seems to have the same effect as returning TRUE.
Table 9.4 WM_DEVICECHANGE event types
DBT_CONFIGCHANGECANCELED | A request to change the current configuration (dock or undock) has been cancelled |
DBT_CONFIGCHANGED | The current configuration has changed, due to a dock or undock |
DBT_CUSTOMEVENT | A custom event has occurred |
DBT_DEVICEARRIVAL | A device has been inserted and is now available |
DBT_DEVICEQUERYREMOVE | Permission is requested to remove a device. Any application candeny this request and cancel the removal |
DBT_DEVICEQUERYREMOVEFAILED | A request to remove a device has been cancelled |
DBT_DEVICEREMOVEPENDING | A device is about to be removed. Cannot be denied |
DBT_DEVICEREMOVECOMPLETE | A device has been removed |
DBT_DEVICETYPESPECIFIC | A device-specific event has occurred |
DBT_QUERYCHANGECONFIG | Permission is requested to change the current configuration (dock or undock) |
DBT_DEVNODES_CHANGED | Device tree has changed |
DBT_USERDEFINED | The meaning of this message is user-defined |
Wdm2Notify application
Wdm2Notify is a Win32 MFC dialog application that displays any device change events for the Wdm2 device interface. It can be found in the Wdm2\Notify directory of the book software. I had a little difficulty setting up the project so that it would compile and link correctly. As I was using VC++ 5, some of the header files and libraries were out of date. I therefore had to change the project settings to use the Platform SDK include directories first, before the Visual C++ directories. My Platform SDK was installed at D:\MSSDK, so you may need to change the project settings if you want to recompile Wdm2Notify. I also had to link to the version of user32.lib in the Platform SDK. If you are running a later version of Visual Studio, you may be able to undo these project changes.
Some of the device change structures are defined in dbt.h, so I included this header file in the Wdm2Notify main MFC header, stdafx.h. I also had to include the following line in the stdafx.h to ensure that the correct functions were declared.
#define WINVER 0x0500
Listing 9.14 shows the main Wdm2Notify PnP Notification routines in file Wdm2Notif.yDlg.cpp. RegisterDeviceNotification is called in the dialog OnInitDialog routine and unregistered in DestroyWindow. Device change messages are handled in OnDeviceChange. The rest of the source code for Wdm2Notify can be found on the book software disk.
Listing 9.14 Wdm2Notify device change routines
BOOL CWdm2NotifyDlg::OnInitDialog() {
CDialog::OnlnitDialog();
// …
// Register for Wdm2 device interface changes
DEV_BROADCAST_DEVICEINTERFACE dbch;
dbch.dbcc_size = sizeof(dbch);
dbch.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE;
dbch.dbcc_classguid = WDM2_GUID;
dbch.dbcc_name[0] = '\0';
WdmNotificationHandle = RegisterDeviceNotification(GetSafeHwnd(), &dbch, DEVICE_NOTIFY_WINDOW_HANDLE);
if (WdmNotificationHandle==NULL)
GetDlgItem(IDC_STATUS)->SetWindowText("Cannot register for Wdm2 class device notification");
return TRUE;
}
BOOL CWdm2NotifyDlg::DestroyWindow() {
if (WdmNotificationHandle!=NULL) {
UnregisterDeviceNotification(WdmNotificationHandle);
WdmNotificationHandle = NULL;
}
return CDialog::DestroyWindow();
}
BOOL CWdm2NotifyDlg::OnDeviceChange(UINT nEventType, DWORD dwData) {
CString Msg = "duh";
switch (nEventType) {
case DBT_CONFIGCHANGECANCELED:
Msg.Format("DBT_CONFIGCHANGECANCELED");
break;
case DBT_CONFIGCHANGED:
Msg.Format("DBT_CONFIGCHANGED");
break;
case DBT_CUSTOMEVENT:
Msg.Format("DBT_CUSTOMEVENT");
break;
case DBT_DEVICEARRIVAL:
Msg.Format("DBT_DEVICEARRIVAL");
break;
case DBT_DEVICEQUERYREMOVE:
Msg.Format("DBT_DEVICEQUERYREMOVE");
break;
case DBT_DEVICEQUERYREMOVEFAILED:
Msg.Format("DBT_DEVICEQUERYREMOVEFAILED");
break;
case DBT_DEVICEREMOVEPENDING:
Msg.Format("DBT_DEVICEREMOVEPENDING");
break;
case DBT_DEVICEREMOVECOMPLETE:
Msg.Format("DBT_DEVICEREMOVECOMPLETE");
break;
case DBT_DEVICETYPESPECIFIC:
Msg.Format("DBT_DEVICETYPESPECIFIC");
break;
case DBT_QUERYCHANGECONFIG:
Msg.Format("DBT_QUERYCHANGECONFIG");
break;
case DBT_DEVNODES_CHANGED:
Msg.Format("DBT_DEVNODES_CHANGED");
break;
case DBT_USERDEFINED:
Msg.Format("DBT_USERDEFINED");
break;
default:
Msg.Format("Event type %d",nEventType);
}
PDEV_BROADCAST_DEVICEINTERfACE pdbch = (PDEV_BROADCAST_DEVICEINTERFACE)dwData;
if (pdbch!=NULL && pdbch->dbcc_devicetype==DBT_DEVTYP_DEVICEINTERFACE) {
CString Msg2;
Msg2.Format("%s: %s",Msg,pdbch->dbcc_name);
Msg = Msg2;
}
CListBox* EventList = (CListBox*)GetDlgItem(IOC_EVENT_LIST);
EventList->AddString(Msg);
return TRUE; // or BROADCAST_QUERY_DENY to deny a query remove
}
Running Wdm2Notify in Windows 98
Figure 9.2 shows some typical output in the Wdm2Notify window in Windows 98 as the Wdm2 driver is updated for an installed Wdm2 device. First the PnP Manager asks if it is OK for the existing Wdm2 device to be removed. Then it issues remove pending and remove complete messages. When the driver has been updated, the Wdm2 device is added again resulting in a device arrival message. Finally, a DBT_DEVNODES_CHANGED event indicates that the device tree has changed.
I am not sure why there are so many duplicate device change messages.
If Wdm2Notify returns BROADCAST_QUERY_DENY to the Query Remove message, the remove is correctly refused and a DBT_DEVICEQUERYREMOVEFAILED message is then issued.
Figure 9.2 Wdm2Notify output in Windows 98
Running Wdm2Notify in Windows 2000
The Beta 2 version of Windows 2000 produces output very different from Wdm2Notify. It issues only a DBT_DEVICEREMOVECOMPLETE message and then a DBT_DEVICEARRIVAL message. A Query Remove request is not issued and there are no repeat messages. I hope that the final version of W2000 will issue more PnP Notification device change messages to Win32 applications.
A driver uses PnP Notification to find devices with a particular device interface. A driver does this so that it can issue calls to the devices that it finds. It may wish to layer its own device on top of each found device. This technique is commonly used when finding Human Input Devices, and a full example of this is given in Chapter 23.
PnP Notification in device drivers works in a similar way to Win32 applications. Remember that this is one device driver asking for device change notifications about devices controlled by another driver.
The driver must call IoRegisterPlugPlayNotification to indicate which events it is interested in receiving, and eventually call IoUnregisterPlugPlayNotification when done. You must pass the name of a callback routine to IoRegisterPlugPlayNotification, along with a context pointer.
In device drivers, you can ask for three different categories of PnP Notification events. If you ask for EventCategoryDeviceInterfaceChange events your callback routine is passed a DEVICE_INTERFACE_CHANGE_NOTIFICATION structure that notifies you only of device removal and device arrival events, along with the appropriate symbolic link. Specify the relevant GUID in the EventCategoryData parameter of IoRegisterPlugPlayNotification. If you specify PNPNOTIFY_DEVICE_INTERFACE_INCLUDE_EXISTING_INTERFACES for the EventCategoryFlags parameter, arrival events are also sent straightaway for any existing devices that have a matching device interface GUID.
If you register for EventCategoryHardwareProfileChange events, you receive query change, change complete, and change cancelled events.
Finally, if you ask for EventCategoryTargetDeviceChange events, you receive query remove, removal completes, and remove cancelled messages for one specific device. You must supply a pointer to the relevant file object as the EventCategoryData parameter of IoRegisterPlugPlayNotification. I assume that you can reject query remove requests.
Driver PnP Notification is particularly important for client drivers that use device interfaces exposed by the system class drivers. For example, kernel mode Human Input Device (HID) client drivers can use PnP Notification to identify all installed HID devices. See Chapter 23 for a full example of PnP Notification in device drivers.
As mentioned earlier, device stacks cannot be changed once they are built. PnP Notification lets you find existing devices and effectively layer on top of them.
As might expect, Win32 programs and device drivers that register for PnP Notification events will effect the operation of a driver. Query remove requests are processed by PnP Notification applications and drivers before the main driver receives an IRP_MN_QUERY_REMOVE_DEVICE IRP. Cancel remove notification messages are sent to applications and other drivers after the main driver has processed its IRP_MN_CANCEL_REMOVE_DEVICE message. Remove pending messages are sent before a device is removed (or surprise removed) and a remove complete notification message is sent afterwards.
This section looks at some advanced Plug and Play topics.
This section looks briefly at the job a bus driver must do. Please consult the DDK documentation for full details. The W2000 DDK source code kernel\serenum example shows how a bus driver is coded.
Bus drivers must manage their device objects carefully. A bus driver has a Functional Device Object (FDO) for each instance of a bus. A bus driver detects any devices on its bus and creates child device objects for each device. These are actually the Physical Device Objects (PDOs) to which higher driver layers attach.
The IRP dispatch routines in a bus driver handle requests for both FDOs and child PDOs. Use a flag in a common portion of the FDO and PDO device extensions to indicate whether the device is an FDO or a child PDO.
If a bus driver detects a Plug and Play device arriving or removing, it must call IoInvalidateDeviceRelations. The PnP Manager then sends an IRP_MN_QUERY_DEVICE_RELATIONS PnP IRP to get the new details. Non-Plug and Play devices can be reported in W2000 using IoReportDetectedDevice.
Creating Child Devices
A bus driver must enumerate its own bus to find any attached devices. It must also keep checking to see if any devices are removed or added. A bus driver might use a system worker thread, described in chapter 14, to schedule its device checks.
A bus driver must call IoInvalidateDeviceRelations if it detects any new devices or if one of its child devices has been removed. This call forces the PnP Manager to issue an IRP_MN_QUERY_DEVICE_RELATIONS BusRelations IRP to the bus driver FDO. The bus driver then enumerates its bus fully. It calls IoCreateDevice to make the child PDO for each new child device.[24] The bus driver returns a list of all valid child PDOs. The PnP Manager works out what PDOs have been added or removed.
If a new device has arrived, the PnP Manager starts to configure this device and build a new device stack. It issues various PnP IRPs to this child PDO. The IRP_MN_QUERY_ID IRP requests the hardware and compatible ids for the device. The PnP Manager also issues IRP_MN_QUERY_CAPABILITIES. IRP_MN_QUERY_DEVICE_TEXT, IRP_MN_QUERY_ RESOURCES and IRP_MN_QUERY_RESOURCE_REQUIREMENTS requests to get further information about the device. Chapter 11 shows how the PnP Manager uses all this information and INF files to determine which drivers should be loaded on top of the child PDO. The stack drivers' AddDevice routines are run and Start Device IRPs issued as usual.
If a function driver decides that a device is no longer present, it can call IoInvalidateDeviceState. The PnP Manager issues an IRP_MN_QUERY_PNP_DEVICE_STATE request. If it finds the PNP_DEVICE_FAILED state bit set, then it issues a (surprise) remove PnP message.
FDO IRP Handling
Normally, a bus driver FDO will receive only AddDevice and Plug and Play IRPs. As mentioned previously, it should enumerate its bus when it receives an IRP_MN_QUERY_DEVICE_ RELATIONS BusRelations PnP IRP. It should return some meaningful information for an IRP_ MN_QUERY_BUS_INFORMATION request.
The bus driver FDO should also process all the standard Plug and Play IRPs in the usual way. Make sure that it can cope if it receives a Remove Device IRP while child devices are still attached to the bus. The DDK documentation says that Query Remove requests are sent to the child devices before being sent to the main FDO.
Child PDO PnP Handling
The child PDO receives IRPs that have been passed down the device stack. Note that it will not receive an AddDevice call. AddDevice is called to create a new device. The bus driver has already done this job, so there is no need for an AddDevice call for a child PDO.
The child PDO handles all the PnP IRPs that come down the device stack. A bus driver handles some of these IRPs differently from function and filter drivers. In particular, it must always complete the PnP IRP so that the IRP can begin its journey up the stack. However, if it does not handle a particular minor function code, it should not change the value in the IoStatus.status field as the PnP Manager sets this field to STATUS_NOT_SUPPORTED in advance.
A bus driver should consider handling these IRPs correctly for the child PDO: IRP_MN_ QUERY_CAPABILITIES, IRP_MN_QUERY_DEVICE_TEXT, IRP_MN_QUERY_ID, and IRP_MN_QUERY_ DEVICE_RELATIONS.
Child PDO I/O IRP Handling
The child PDO also receives any other IRPs that come down the stack. These IRPs will be the create, close, read, write, or IOCTL I/O requests that do a useful job.
The child PDO handler for these requests could interact with the bus directly. However, it is possible that all bus operations have to be performed in a controlled manner, rather than have all child PDOs trying to access it at once. One option, therefore, is to pass any incoming I/O IRPs to the bus driver FDO. The bus driver FDO handler for these IRPs can do whatever is necessary (e.g., serialize all I/O requests).
If you use this technique, you will obviously need to store a pointer to the parent bus driver FDO in the child PDO device extension. Use IoCallDriver as usual to pass on any IRPs.
There is one thing to remember if using this technique. The number of IRP stack locations for the child PDOs will have to be increased as the IRPs will be passed down through more driver layers. Simply add the FDO Stacksize field to the child PDO Stacksize after IoCreateDevice has returned.
Most PnP IRPs are sent by the PnP Manager, but some can be sent by drivers (e.g., IRP_MN_ QUERY_INTERFACE). A driver must send a PnP IRP to the driver at the top of the device stack. Call IoGetAttachedDeviceReference to get a pointer to the device object for the driver at the top of the device stack.
In AddDevice (or elsewhere) you can ask for various properties of the PDO using IoGetDeviceProperty. The property information is stored in the registry but is best accessed using IoGetDeviceProperty. The DeviceProperty parameter has the information you want. You also supply a buffer that is filled with the appropriate information.
Among other things, you can ask for the device's hardware ID and the GUID of the bus driver and the device's setup class. Sometimes you may need to call IoGetDeviceProperty twice. The first call informs you of the size of buffer required, while the second call actually gets the data.
This chapter has looked in detail at a practical Plug and Play function driver implementation for Wdm2 devices. The Wdm2 driver now handles all its PnP operations safely. The Wdm2Test Win32 test program checked that the Wdm2 driver could not be removed or replaced while its devices had open handles.
We also looked at how Plug and Play Notification can inform Win32 and device drivers of device change events. The Wdm2Notify Win32 test program displays any device change events for the Wdm2 device interface. Finally, bus drivers were briefly described.
The next chapter completes the discussion of the Wdm2 driver by describing its Power Management features.
Non-WDM NT style drivers may use more than one full resource descriptor.
Obviously, this may well change in 64-bit systems.
BROADCAST_QUERY_DENY has a value of 0x424D5144, ASCII "BMQD", which I presume stands for Broadcast Message Query Deny!
I think PDOs must be created with FILE_AUTOGENERATED_DEVICE_NAME as the DeviceCharacteristics parameter to IoCreateDevice.