Synchronization of IO Request Cancellation in KMDF Drivers

Synchronization of I/O Request Cancellation in KMDF Drivers

Cancellation of I/O requests is inherently asynchronous and requires careful synchronization. Although some drivers can rely on the framework to cancel I/O requests, most drivers are required to implement cancellation code for at least some of their I/O requests.

 Chapter 8 describes how the framework cancels I/O requests  If your driver always keeps its long-term I/O requests in queues and then completes them quickly, you can avoid any synchronization code. The framework cancels the requests while they are in the queue. After a request has been delivered from the queue, the driver can leave the request in an uncancelable state because completion of the request is immediate.

However, most drivers retain dequeued, inflight requests while the hardware performs an operation and thus must support cancellation.

Synchronization is the most error-prone aspect of handling I/O cancellation in WDM drivers because the driver must keep track of who owns the request and who thus is responsible for canceling it. Although cancellation is equally complex in WDF drivers, the framework object model makes possible two techniques that simplify the request tracking:

  • Synchronize cancellation by using synchronization scope.

  • Synchronize cancellation by tracking state in the request context area.

In addition, if your driver creates subrequests to gather information that is required to complete a request that the framework delivered, you should synchronize the cancellation of the parent request with that of the subrequests. The KMDF collection object enables a relatively simple technique for canceling the subrequests if the main request is canceled.

The sections that follow describe each of these synchronization techniques.

Synchronized Cancellation by Using Synchronization Scope

A driver typically completes long-term I/O requests in an asynchronous callback such as an interrupt DPC, a timer DPC, or a work item. A simple approach to synchronizing I/O cancellation is to use device or queue synchronization scope on the queue and also on the asynchronous callback object that completes the request.

The Echo sample in the WDK demonstrates this approach. This driver creates a sequential queue for read and write requests. The driver sets the synchronization scope for the device object to device scope, and the queue inherits this scope by default. The driver also sets the queue as the parent of the timer object, so by default the timer DPC function is serialized with the I/O event callbacks on the queue. Each time the queue delivers a read or write request, the driver marks the request cancelable and stores its handle in the queue context area. The timer DPC later completes the request.

Because of the synchronization scope, the framework ensures that the cancel routine and the timer DPC do not execute concurrently. Therefore, the timer DPC does not require locks to check the cancellation state of the request or to complete it, as Listing 10-7 shows.

Listing 10-7: Canceling a request using framework synchronization

image from book
 if(queueContext->CurrentRequest!= NULL) {     Status = WdfRequestUnmarkCancelable(Request);     if( Status != STATUS_CANCELLED ) {         queueContext->CurrentRequest = NULL;         Status = queueContext->CurrentStatus;         WdfRequestComplete(Request, Status);     }     else {     . . . //Code omitted     } } 
image from book

The driver keeps the handle for the current request in the context area of the queue. If the handle is NULL, the request has already been completed, so the driver must not cancel it. If the handle is valid, the driver marks the request uncancelable. WdfRequestUnmarkCancelable returns STATUS_CANCELLED if the request has been canceled, which indicates that the driver's I/O cancel callback will run after the timer DPC returns. In this case, the driver does not complete the request because the cancel callback will do so. If the request has not already been completed or canceled, the driver updates the information in the queue context area and calls WdfRequestComplete to complete the request.

Synchronized Cancellation by Tracking State in the Request Context

Another approach to cancellation is to use your own lock and state variable to track the ownership of the request. If your driver uses two sequential queues or a similar design in which only one or two requests are pending at any time, you can track ownership by maintaining a state variable for each pending request in the device or queue object context area, as the example in Listing 10-7 does.

However, the problem is more complicated if your driver uses a parallel queue and thus could have many requests in a cancelable state. In such a driver, you can track ownership by using the request object context area and a reference count.

Consider a scenario where you mark the request cancelable and start an I/O operation on the hardware. When the operation is complete, the hardware generates an interrupt to notify the driver. The driver then queues a DPC to complete the request. Assume that the driver can terminate the I/O operation when the corresponding request is canceled.

The following steps include pseudocode that shows how to implement synchronization for such a driver.

  1. Create a context area in a framework-created request context by declaring a context type in a header file, as follows:


    The IsCancelled flag in the request context indicates whether the request has been canceled, and the IsTerminateFailed flag indicates whether the driver's internal TerminateIo function returned a failure status.

    The spin lock is used to synchronize request completion. The choice of lock is critical to the performance of the driver because the lock is acquired and released in the request completion path as shown later in step 5 as well as in the cancellation path that is shown in step 4. By using a KSPIN_LOCK instead of a WDFSPINLOCK, you can avoid possible WDF lock object allocation failures in the I/O path.

  2. In EvtDriverDeviceAdd, configure a context area for every request that the framework presents to the driver:

     EvtDriverDeviceAdd(Driver, DeviceInit) {     WDF_OBJECT_ATTRIBUTES attributes;     WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&attributes, REQUEST_CONTEXT);     WdfDeviceInitSetRequestAttributes(DeviceInit, &attributes); } 

  3. In the EvtIoXxx callback, take an extra reference on the request object and mark the request cancelable:

     EvtIoDispatch(Queue, Request) {     PREQUEST_CONTEXT reqContext;     reqContext = GetRequestContext(Request);     reqContext->IsCancelled = FALSE;     reqContext->IsTerminateFailed = FALSE;     KeInitializeSpinLock(&reqContext->Lock);     WdfObjectReference(Request);     WdfRequestMarkCancelable(Request, EvtRequestCancelRoutine);     // Start the I/O operation on the hardware     . . . } 

    The framework dispatches requests from the queue to the EvtIoXxx callback. The callback initializes the IsCancelled and IsTerminateFailed flags to FALSE. Initialization of the flags is not strictly required because the framework initializes the context area to zero, but the pseudocode shows this step for clarity. The callback also initializes the spin lock and takes an additional reference on the request object. It then marks the request cancelable and starts a long-term I/O operation.

    When the I/O operation is complete, the device generates an interrupt, and the EvtInterruptDpc callback subsequently completes the request. The additional reference lets you access the request context to find the state of the request even after the request has been completed.

  4. The framework calls the driver's EvtRequestCancel callback when the request is canceled. Use the spin lock in this function to synchronize request completion:

     EvtRequestCancelRoutine(Request) {     PREQUEST_CONTEXT reqContext = GetRequestContext (Request);     BOOLEAN completeRequest;     KIRQL oldIrql;     KeAcquireSpinlock(&reqContext->Lock, &oldIrql);     reqContext->IsCancelled = TRUE;     if (TerminateIO() == TRUE) {         WdfObjectDereference(Request);         completeRequest = TRUE;     }     else {         reqContext->IsTerminateFailed = TRUE;         completeRequest = FALSE;     }     KeReleaseSpinlock(&reqContext->Lock, oldIrql);     if (completeRequest) {         WdfRequestComplete(Request, STATUS_CANCELLED);     }; } 

    The cancel callback acquires the lock and sets IsCancelled to TRUE, so that if EvtInterruptDpc runs simultaneously on another processor, it will not attempt to complete the request. Then the driver terminates the I/O operation-not the I/O request-by calling TerminateIo. If TerminateIo succeeds, then you know that the EvtInterruptDpc will not be called to complete the request. As a result, the driver can drop the extra reference that the EvtIoXxx function took and complete the request.

    If TerminateIo fails, the I/O operation is about to be completed and EvtInterruptDpc will run. The cancel callback sets the IsTerminateFailed flag to TRUE and does not complete the request because the hardware might be using the buffers in the request object. After the driver completes the request, the framework completes the underlying IRP, thus freeing the buffers. Therefore, the EvtInterruptDpc callback completes the request if the I/O operation completed, and the EvtCancelCallback completes the request if the I/O operation did not complete. If the hardware does not access the buffers in the request object, you can eliminate the IsTerminateFailed flag and simplify the logic.

  5. Check the value of the IsCancelled and IsTerminateFailed flags in the EvtInterruptDpc callback to determine whether to complete the request:

     EvtDpcForIsr(Interrupt) {     PREQUEST_CONTEXT reqContext = GetRequestContext (Request);     NTSTATUS status;     BOOLEAN completeRequest;     KIRQL oldIrql;     completeRequest = TRUE;     KeAcquireSpinlock(&reqContext->Lock, &oldIrql);     if (reqContext->IsCancelled == FALSE) {         status = WdfRequestUnmarkCancelable(Request);         if (status == STATUS_CANCELLED) {             //             // Cancel routine is about to be called or is already waiting             // to acquire lock. We will let it complete the request.             completeRequest = FALSE;         }         // Process the request and complete it after dropping the lock.         //         status = STATUS_SUCCESS;     }     else {         //Cancel routine has already run but might not have completed the request.         if (reqContext->IsTerminateFailed {             status = STATUS_CANCELLED;         }         else {             completeRequest = FALSE         }     }     KeReleaseSpinlock(&reqContext->Lock, oldIrql);     WdfObjectDereference(Request);     if (completeRequest) {         WdfRequestComplete(Request, status);     }; } 

    In the EvtInterruptDpc callback, acquire the lock and then check the value of the IsCancelled variable. If the request has not been canceled, unmark the request as cancelable. If the request is canceled immediately before you unmark the cancelable state, WdfRequestUnmarkCancelable returns STATUS_CANCELLED and the framework will call the EvtRequestCancel callback. In this case, you should let EvtRequestCancel complete the request. If the request has still not been canceled, EvtInterruptDpc processes the request as required, sets STATUS_SUCCESS, and will complete the request.

    If IsCancelled is TRUE, the cancel callback has run, but you must check the value of IsTerminateFailed to determine whether the cancel callback completed the request. If IsTerminateFailed is TRUE, the I/O operation completed before termination, so the EvtInterruptDpc should complete the request with STATUS_CANCELLED. If IsTerminateFailed is FALSE, the cancel callback completed the request.

    The driver can then release the lock and complete the request. Whether or not the driver completes the request, the driver should always drop the extra reference because the cancel callback does not drop the reference if TerminateIo fails.

image from book
Has WDF Simplified Request Cancellation?

The answer to this question depends on how your driver holds long-term requests-that is, requests that are waiting for a hardware event. If the driver uses queues to hold long-term requests, it's easy. If not, it is messy.

If you use queues to hold your long-term requests, you don't have to deal with any cancellation issues. Any requests that are in the queue will be completed by the framework when they are cancelled. If you want to be notified before the framework completes the requests, you can register the EvtIoCanceledOnQueue callback and complete the request yourself. The Osrusbfx2 sample shows how to park a request that is waiting for a hardware event to occur.

If you don't use queues to hold long-term requests and instead choose to set your own cancel routine with WdfRequestMarkCancelable, you must worry about race conditions between the cancel routine and an asynchronous callback (such as EvtInterruptDpc, EvtTimerFunc, or EvtWorkItem) that completes the request. You must make sure that only one thread has clear ownership of the request before calling WdfRequestComplete to complete it.

As an alternative to these two approaches, you could implement a third approach, in which the driver keeps the request inflight, does not mark it cancelable, and is still responsive to cancellation. To do this, the driver periodically checks the status of the request by calling WdfRequestIsCanceled. This approach is feasible if you actively poll the hardware to find the completion status of your I/O operation. However, although it seems fairly simple, it is generally not optimal because actively polling in kernel mode can lead to performance degradation.

Holding the requests in the queues and letting the framework deal with the cancellation issues is the best approach. So from that perspective, yes, WDF has indeed simplified cancellation.
-Eliyas Yakub, Windows Driver Foundation Team, Microsoft

image from book

Synchronized Cancellation of Incoming Request with Driver-Created Subrequests

Some drivers create and send one or more subrequests to gather data that is required to complete a request that the framework delivered. If the framework-delivered request is canceled, the driver must synchronize the cancellation of the subrequests.

One way to synchronize access during cancellation is to use a collection of request objects to keep track of the requests that the driver has sent to the I/O target. When the driver sends a request, it adds the request's handle to the collection by calling WdfCollectionAdd. This method can fail, so the driver must be prepared to handle such a failure. When the driver is finished with the request, it deletes the handle from the collection.

 Chapter 12 describes collection objects  The driver must protect the collection with a lock of an appropriate type for the IRQL at which the driver accesses the collection. In this case, the driver must use a spin lock, because the I/O completion callback can run at DISPATCH_LEVEL.

To cancel a request by using this synchronization technique, the driver should take these steps:

  1. Acquire the lock for the collection.

  2. Find the request object's handle in the collection.

  3. Increment the reference count on the request object.

  4. Release the lock.

  5. Cancel the request.

  6. Decrement the reference count on the request object.

The code in Listing 10-8, which is taken from the Usbsamp\Sys\Isorwr.c sample, shows how a driver can implement this synchronization.

Listing 10-8: Using a collection to synchronize request cancellation in a KMDF driver

image from book
 WdfSpinLockAcquire(rwContext->SubRequestCollectionLock); for(i = 0; i < WdfCollectionGetCount(rwContext->SubRequestCollection); i++) {     subRequest = (WDFREQUEST) WdfCollectionGetItem(rwContext->SubRequestCollection, i);     subReqContext = GetSubRequestContext(subRequest);     WdfObjectReference(subRequest);     InsertTailList(&cancelList, &subReqContext->ListEntry); } WdfSpinLockRelease(rwContext->SubRequestCollectionLock); while(!IsListEmpty(&cancelList)) {     thisEntry = RemoveHeadList(&cancelList);     subReqContext = CONTAINING_RECORD(thisEntry, SUB_REQUEST_CONTEXT, ListEntry);     subRequest = WdfObjectContextGetObject(subReqContext);     if(!WdfRequestCancelSentRequest(subRequest)) {     . . . //Error handling code omitted     }     WdfObjectDereference(subRequest); } 
image from book

The driver in Listing 10-8 breaks incoming I/O requests into smaller subrequests that it sends to an I/O target. Each subrequest is a WDFREQUEST object and has an object context area that contains a handle to the main request and a LIST_ENTRY field, along with some other request-specific data.

If the incoming request is canceled for any reason, the driver must cancel the associated subrequests. The driver marks the main request cancelable and creates a collection object into which it places the subrequests. Each time a subrequest completes, the driver removes it from the collection.

The code shown in the listing is part of the EvtRequestCancel callback for the main request. The driver protects the collection with a spin lock because the framework can call the I/O completion callback at DISPATCH_LEVEL.

If the main request is canceled, the driver acquires the lock for the collection and loops through the collection to create a linked list of all the subrequests and to take out an additional reference on each subrequest as it is added to the list. Instead of placing the entire subrequest object in the list, the driver simply links their context areas through the LIST_ENTRY field. When the list is complete, the driver releases the lock.

Developing Drivers with the Microsoft Windows Driver Foundation
Developing Drivers with the Windows Driver Foundation (Pro Developer)
ISBN: 0735623740
EAN: 2147483647
Year: 2007
Pages: 224

Similar book on Amazon
Windows Internals, Part 1: Covering Windows Server 2008 R2 and Windows 7
Windows Internals, Part 1: Covering Windows Server 2008 R2 and Windows 7
Windowsu00ae Internals: Including Windows Server 2008 and Windows Vista, Fifth Edition (Pro Developer)
Windowsu00ae Internals: Including Windows Server 2008 and Windows Vista, Fifth Edition (Pro Developer)
Advanced Windows Debugging
Advanced Windows Debugging
The Windows 2000 Device Driver Book: A Guide for Programmers (2nd Edition)
The Windows 2000 Device Driver Book: A Guide for Programmers (2nd Edition) © 2008-2017.
If you may any questions please contact us: