Êíèãà: Writing Windows WDM Device Drivers

Implementing Plug and Play

Implementing Plug and Play

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.

Adding and Removing Devices

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.

Basic PnP Handlers

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;
}

Passing Unsupported IRPs Down the Stack

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

PnP States and Messages

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


State Flags

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

Holding IRPs

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.

Open Handles

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

When to Process PnP IRPs

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

Device Locking

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.

Getting Resource Assignments

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 CmResourceTypeDeviceSpecific CmResourceTypeBusNumber CmResourceTypeDevicePrivate CmResourceTypePcCardConfig

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.

Îãëàâëåíèå êíèãè


Ãåíåðàöèÿ: 0.032. Çàïðîñîâ Ê ÁÄ/Cache: 0 / 0
ïîäåëèòüñÿ
Ââåðõ Âíèç