Programming the Microsoft Windows Driver Model

Notifying Applications of Interesting Events

One extremely important use of IOCTL operations is to give a WDM driver a way to notify an application that an interesting event (however you define interesting, that is) has occurred. To motivate this discussion, suppose you had an application that needed to work closely with your driver in such a way that whenever a certain kind of hardware event occurred your driver would alert the application so that it could take some sort of user-visible action. For example, a button press on an instrument might trigger an application to begin collecting and displaying data. Whereas Windows 98/Me provides a couple of ways for a driver to signal an application in this kind of situation namely, asynchronous procedure calls or posted window messages those methods don t work in Windows XP because the operating system lacks (or doesn t expose) the necessary infrastructure to make them work.

A WDM driver can notify an application about an interesting event in two ways:

In either case, the application typically dedicates a thread to the task of waiting for the notification. That is, when you re sharing an event, you have a thread that spends most of its life asleep on a call to WaitForSingleObject. When you use a pending IOCTL, you have a thread that spends most of its life waiting for DeviceIoControl to return. This thread doesn t do anything else except, perhaps, post a window message to a user interface thread to make something happen that will be visible to the end user.

How to Organize Your Notification Thread

When using either notification method, you can avoid some plumbing problems by not having the notification thread block simply on the event or the IOCTL, as the case may be. Instead, proceed as follows: First define a kill event that your main application thread will set when it s time for the notification thread to exit.

If you re using the shared event scheme for notification, call WaitForMultipleObjects to wait for either the kill event or the event you re sharing with the driver.

If you re using the pending IOCTL scheme, make asynchronous calls to DeviceIoControl. Instead of calling GetOverlappedResult to block, call WaitForMultipleObjects to wait for either the kill event or the event associated with the OVERLAPPED structure. If the return code indicates that the DeviceIoControl operation has finished, you make a nonblocking call to GetOverlappedResult to get the return code and bytes-transferred values. If the return code indicates that the kill event has been signaled, you call CancelIo to knock down the DeviceIoControl, and you then exit from the thread procedure. Leave out the call to CancelIo if your application has to run in Windows 98 Second Edition.

These two methods of solving the notification problem have the relative strengths and weaknesses shown in Table 9-3. Notwithstanding that it appears superficially as though the pending IOCTL method has all sorts of advantages over the shared event method, I recommend you use the shared event method because of the complexity of the race conditions you must handle with the pending IOCTL method.

Table 9-3. Comparison of Alternative Notification Methods

Sharing an Event

Pending an IRP_MJ_DEVICE_CONTROL

Application needs to create an object by calling CreateEvent.

No object needed.

Driver has to convert handle to object pointer.

No conversion needed.

No cancel logic needed; trivial cleanup.

Cancel and cleanup logic needed; usual horrible race conditions.

Application knows only that something happened when event gets signaled.

Driver can provide arbitrary amount of data when it completes the IRP.

Using a Shared Event for Notification

The basic idea behind the event sharing method is that the application creates an event by calling CreateEvent and then uses DeviceIoControl to send the event handle to the driver:

DWORD junk; HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); DeviceIoControl(hdevice, IOCTL_REGISTER_EVENT, &hEvent, sizeof(hEvent), NULL, 0, &junk, NULL);

NOTE

The EVWAIT sample driver illustrates the shared event method of notifying an application about an interesting event. You generate the interesting event by pressing a key on the keyboard, thereby causing the test program to call a back-door IOCTL in the driver. In real life, an actual hardware occurrence would generate the event.

The call to CreateEvent creates a kernel-mode KEVENT object and makes an entry into the application process s handle table that points to the KEVENT. The HANDLE value returned to the application is essentially an index into the handle table. The handle isn t directly useful to a WDM driver, though, for two reasons. First of all, there isn t a documented kernel-mode interface for setting an event, given just a handle. Second, and most important, the handle is useful only in a thread that belongs to the same process. If driver code runs in an arbitrary thread (as it often does), it will be unable to reference the event by using the handle.

To get around these two problems with the event handle, the driver has to convert the handle to a pointer to the underlying KEVENT object. To handle a METHOD_BUFFERED control operation that the application uses to register an event with the driver, use code like this:

HANDLE hEvent = *(PHANDLE) Irp->AssociatedIrp.SystemBuffer; PKEVENT pevent; NTSTATUS status = ObReferenceObjectByHandle(hEvent, EVENT_MODIFY_STATE, *ExEventObjectType, Irp->RequestorMode, (PVOID*) &pevent, NULL);

ObReferenceObjectByHandle looks up hEvent in the handle table for the current process and stores the address of the associated kernel object in the pevent variable. If the RequestorMode in the IRP is UserMode, this function also verifies that hEvent really is a handle to something, that the something is an event object, and that the handle was opened in a way that includes the EVENT_MODIFY_STATE privilege.

Whenever you ask the object manager to resolve a handle obtained from user mode, request access and type checking by indicating UserMode for the accessor mode argument to whichever object manager function you re calling. After all, the number you get from user mode might not be a handle at all, or it might be a handle to some other type of object. In addition, avoid the undocumented ZwSetEvent function in order not to create the following security hole: even if you ve made sure that some random handle is for an event object, your user-mode caller could close the handle and receive back the same numeric handle for a different type of object. You d then unwittingly cause something bad to happen because you re a trusted caller of ZwSetEvent.

The application can wait for the event to happen:

WaitForSingleObject(hEvent, INFINITE);

The driver signals the event in the usual way:

KeSetEvent(pevent, EVENT_INCREMENT, FALSE);

Eventually, the application cleans up by calling CloseHandle. The driver has a separate reference to the event object, which it must release by calling ObDereferenceObject. The object manager won t destroy the event object until both these things occur.

Using a Pending IOCTL for Notification

The central idea in the pending IOCTL notification method is that when the application wants to receive event notifications from the driver, it calls DeviceIoControl:

HANDLE hDevice = CreateFile("\\\\.\\<driver-name>", ...); BOOL okay = DeviceIoControl(hDevice, IOCTL_WAIT_NOTIFY, );

(IOCTL_WAIT_NOTIFY, by the way, is the control code I used in the NOTIFY sample in the companion content.)

The driver will pend this IOCTL and complete it later. If other considerations didn t intrude, the code in the driver might be as simple as this:

NTSTATUS DispatchControl(...) { switch (code) { case IOCTL_WAIT_NOTIFY: IoMarkIrpPending(Irp); pdx->NotifyIrp = Irp; return STATUS_PENDING; } } VOID OnInterestingEvent(...) { CompleteRequest(pdx->NotifyIrp, STATUS_SUCCESS, 0); // <== don't do this! }

The other considerations I just so conveniently tucked under the rug are, of course, all-important in crafting a working driver. The originator of the IRP might decide to cancel it. The application might call CancelIo, or termination of the application thread might cause a kernel-mode component to call IoCancel Irp. In either case, we must provide a cancel routine so that the IRP gets completed. If power is removed from our device, or if our device is suddenly removed from the computer, we may want to abort any outstanding IOCTL requests. In general, any number of IOCTLs might need to be aborted. Consequently, we ll need a linked list of them. Since multiple threads might be trying to access this linked list, we ll also need a spin lock so that we can access the list safely.

Helper Routines

To simplify my own life, I wrote a set of helper routines for managing asynchronous IOCTLs. The two most important of these routines are named CacheControlRequest and UncacheControlRequest. They assume that you re willing to accept only one asynchronous IOCTL having a particular control code per device object and that you can, therefore, reserve a pointer cell in the device extension to point to the IRP that s currently outstanding. In NOTIFY, I call this pointer cell NotifyIrp. You accept the asynchronous IRP this way:

switch (code) { case IOCTL_WAIT_NOTIFY: if (<parameters invalid in some way>) status = STATUS_INVALID_PARAMETER; else status = CacheControlRequest(pdx, Irp, &pdx->NotifyIrp); break; } return status == STATUS_PENDING ? status : CompleteRequest(Irp, status, info);

The important statement here is the call to CacheControlRequest, which registers this IRP in such a way that we ll be able to cancel it later if necessary. It also records the address of this IRP in the NotifyIrp member of our device extension. We expect it to return STATUS_PENDING, in which case we avoid completing the IRP and simply return STATUS_PENDING to our caller.

NOTE

You can easily generalize the scheme I m describing to permit an application to have an IRP of each type outstanding for each open handle. Instead of putting the current IRP pointers in your device extension, put them instead into a structure that you associate with the FILE_OBJECT that corresponds to the handle. You ll get a pointer to this FILE_OBJECT in the I/O stack location for IRP_MJ_CREATE, IRP_MJ_CLOSE, and, in fact, all other IRPs generated for the file handle. You can use either the FsContext or FsContext2 field of the file object for any purpose you choose.

Later, when whatever event the application is waiting for occurs, we execute code like this:

PIRP nfyirp = UncacheControlRequest(pdx, &pdx->NotifyIrp);if (nfyirp) { <do something> CompleteRequest(nfyirp, STATUS_SUCCESS, <info value>); }

This logic retrieves the address of the pending IOCTL_WAIT_NOTIFY request, does something to provide data back to the application, and then completes the pending I/O request packet.

How the Helper Routines Work

I hid a wealth of complications inside the CacheControlRequest and UncacheControlRequest functions. These two functions provide a thread-safe and multiprocessor-safe mechanism for keeping track of asynchronous IOCTL requests. They use a variation on the techniques we ve discussed elsewhere in the book for safely queuing and dequeuing IRPs at times when someone else might be flitting about trying to cancel the IRP. I actually packaged these routines in GENERIC.SYS, and the NOTIFY sample in the companion content shows how to call them. Here s how those functions work (but note that the GENERIC.SYS versions have Generic in their names):

typedef struct _DEVICE_EXTENSION { KSPIN_LOCK IoctlListLock; LIST_ENTRY PendingIoctlList; } DEVICE_EXTENSION, *PDEVICE_EXTENSION; NTSTATUS CacheControlRequest(PDEVICE_EXTENSION pdx, PIRP Irp, PIRP* pIrp) { KIRQL oldirql;

KeAcquireSpinLock(&pdx->IoctlListLock, &oldirql); NTSTATUS status;

if (*pIrp) status = STATUS_UNSUCCESSFUL;

else if (pdx->IoctlAbortStatus) status = pdx->IoctlAbortStatus; else {

IoSetCancelRoutine(Irp, OnCancelPendingIoctl); if (Irp->Cancel && IoSetCancelRoutine(Irp, NULL)) status = STATUS_CANCELLED; else {

IoMarkIrpPending(Irp); status = STATUS_PENDING;

Irp->Tail.Overlay.DriverContext[0] = pIrp; *pIrp = Irp; InsertTailList(&pdx->PendingIoctlList, &Irp->Tail.Overlay.ListEntry); } } KeReleaseSpinLock(&pdx->IoctlListLock, oldirql); return status; } VOID OnCancelPendingIoctl(PDEVICE_OBJECT fdo, PIRP Irp) { KIRQL oldirql = Irp->CancelIrql; IoReleaseCancelSpinLock(DISPATCH_LEVEL); PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension; KeAcquireSpinLockAtDpcLevel(&pdx->IoctlListLock); RemoveEntryList(&Irp->Tail.Overlay.ListEntry); PIRP pIrp = (PIRP) Irp->Tail.Overlay.DriverContext[0]; InterlockedCompareExchange((PVOID*) pIrp, Irp, NULL); KeReleaseSpinLock(&pdx->IoctlListLock, oldirql); Irp->IoStatus.Status = STATUS_CANCELLED; IoCompleteRequest(Irp, IO_NO_INCREMENT); } PIRP UncacheControlRequest(PDEVICE_EXTENSION pdx, PIRP* pIrp) { KIRQL oldirql; KeAcquireSpinLock(&pdx->IoctlListLock, &oldirql);

PIRP Irp = (PIRP) InterlockedExchangePointer(pIrp, NULL); if (Irp) {

if (IoSetCancelRoutine(Irp, NULL)) { RemoveEntryList(&Irp->Tail.Overlay.ListEntry); } else Irp = NULL; } KeReleaseSpinLock(&pdx->IoctlListLock, oldirql); return Irp; }

  1. We use a spin lock to guard the list of pending IOCTLs and also to guard all of the pointer cells that are reserved to point to the current instance of each different type of asynchronous IOCTL request.

  2. This is where we enforce the rule it s more of a design decision, really that only one IRP of each type can be outstanding at one time.

  3. This if statement accommodates the fact that we may need to start failing incoming IRPs at some point because of PnP or power events.

  4. Since we ll pend this IRP for what might be a long time, we should have a cancel routine for it. I ve discussed cancel logic so many times in this book that I feel sure you d rather not read about it once more.

  5. Here we ve decided to go ahead and cache this IRP so that we can complete it later. Since we re going to end up returning STATUS_PENDING from our DispatchControl function, we need to call IoMarkIrpPending.

  6. We need to have a way to NULL out the cache pointer cell when we cancel the IRP. Since there s no way to get a context parameter passed to our cancel routine, I decided to co-opt one of the DriverContext fields in the IRP to hold a pointer to the cache pointer cell.

  7. In the normal course of events, this statement uncaches an IRP.

  8. Now that we ve uncached our IRP, we don t want it to be cancelled any more. If IoSetCancelRoutine returns NULL, however, we know that this IRP is currently in the process of being cancelled. We return a NULL IRP pointer in that case.

NOTIFY also has an IRP_MJ_CLEANUP handler for pending IOCTLs that looks just about the same as the cleanup handlers I ve discussed for read and write operations. Finally, it includes an AbortPendingIoctls helper function for use at power-down or surprise removal time, as follows:

VOID AbortPendingIoctls(PDEVICE_EXTENSION pdx, NTSTATUS status) { InterlockedExchange(&pdx->IoctlAbortStatus, status); CleanupControlRequests(pdx, status, NULL); }

CleanupControlRequests is the handler for IRP_MJ_CLEANUP. I wrote it in such a way that it cancels all outstanding IRPs if the third argument normally a file object pointer is NULL.

NOTIFY is a bit too simple to serve as a complete model for a real-world driver. Here are some additional considerations for you to mull over in your own design process:

Категории