1. I/O Queues
The IWDFIoQueue
interface exposes a queue object that presents requests from UMDF to
the driver. Queues control the flow of I/O through the driver.
A
driver typically creates one or more I/O queues, each of which can
accept one or more types of requests. The driver configures the queues
when it creates them. For each queue, the driver can specify:
The types of requests that are placed in the queue.
The power management options for the queue.
The
dispatch method for the queue, which determines whether the framework
calls the driver to dispatch a request or whether the driver calls the
framework to dispatch a request. The dispatch method also determines
whether the driver services multiple requests from the queue at a given
time.
Whether the queue accepts read and write requests that have a zero-length buffer.
A driver can have any number
of queues, which can all be configured differently. For example, a
driver might have a parallel queue for read requests and a sequential
queue for write requests.
Although a request is in a queue
and has not yet been presented to the driver, the queue is considered
the “owner” of the request. After the request has been dispatched to the
driver, it is “owned” by the driver. Internally, each queue object
keeps track of which requests it owns and which requests it has
dispatched to the driver. A driver can forward a request from one queue
to another by calling a method on the request object.
1.1. Dispatch Type
A queue’s dispatch type
determines how and when I/O requests are delivered to the driver and, as
a result, whether multiple I/O requests from a queue are active in the
driver at one time. Drivers can control the concurrency of I/O requests
by configuring the dispatching method for their queues. UMDF supports
three dispatch types:
Sequential—
A queue that is configured for sequential dispatching delivers I/O
requests to the driver one at a time. The queue does not deliver another
request to the driver until the previous request has been completed or
forwarded to another queue.
Parallel—
A queue that is configured for parallel dispatching delivers I/O
requests to the driver as soon as possible, whether or not another
request is already active in the driver.
Manual— A
queue that is configured for manual dispatching does not deliver I/O
requests to the driver. Instead, the driver retrieves requests at its
own pace by calling a method on the queue.
The dispatch type controls
only the number of requests that are active within a driver at one time.
It has no effect on whether the queue’s I/O event callbacks are invoked
sequentially or concurrently; instead, the concurrency of callbacks is
controlled by the synchronization model (locking constraint) of the
device object. Even if the synchronization model does not allow
concurrent callbacks, a parallel queue nevertheless might have many
requests active in the driver at one time.
All I/O requests that a
driver receives from a queue are inherently asynchronous. The driver
can complete the request within the event callback or sometime later,
after returning from the callback. The driver is not required to mark
the request pending, as in a Kernel Mode WDM Driver; UMDF handles this
on behalf of the driver.
1.2. Queues and Power Management
UMDF integrates
support for queues with Plug and Play/power management state machine.
Power management is configurable on a per-queue basis. A driver can use
both power-managed and nonpower-managed queues and can sort requests
based on the requirements for its power model.
1.2.1. Power-Managed Queues
By default, I/O queues are
power managed, which means that the state of the queue can trigger
power-management activities. Such queues have a couple of advantages, as
the following scenarios show:
If an I/O request
arrives while the system is in the working state (SO) but the device is
not, UMDF notifies its Plug and Play and power handler so that it can
restore device power.
If
the device power state begins to change while the driver “owns” an I/O
request that was dispatched from a power-managed queue, UMDF can notify
the driver through the IQueueCallbackIoStop::OnIoStop
callback. The driver must complete, cancel, or acknowledge all of the
I/O requests that it owns before the device can exit from the working
state.
For
power-managed queues, UMDF pauses the delivery of requests when the
device leaves the working state (DO) and resumes delivery when the
device returns to the working state. Although delivery stops while the
queue is paused, queuing does not. If UMDF receives a request while the
queue is paused, UMDF adds the request to the queue for delivery after
the queue resumes. If an I/O request arrives while the system is
transitioning to a sleep state, however, UMDF does not return the device
to the working state until the system returns to the working state. The
request remains in the queue until the system and the device have
returned to the working state.
For requests to be
delivered, both the driver and device power state must allow processing.
The driver can pause delivery manually by calling IWDFIoQueue::Stop or IWDFIoQueue::StopSynchronously and later resume delivery by calling WdfIoQueue::Start.
1.2.2. Nonpower-Managed Queues
If a queue is not power
managed, the state of the queue has no effect on power management, and
conversely, UMDF delivers requests to the driver any time the system is
in the working state, regardless of the power state of the device.
Drivers should use nonpower-managed queues to hold requests that the
driver can handle even while its device is not in the working state.
2. I/O Request Objects
The IWDFIoRequest
interface exposes an I/O request object, which describes a read, write,
or device I/O control request. When an I/O request arrives from the
reflector, the I/O handler creates an I/O request object and adds the
object to the queue that the driver configured for requests of that
type. The driver receives a pointer to IWDFIoRequest
interface for the object when UMDF calls the I/O event callback
function or, if the queue supports manual dispatching, when the driver
requests the object from the queue.
The driver can then call
methods on the interface to retrieve information about the request, such
as the request type, parameters, data buffers, and associated file
object, among others.
Like all other UMDF objects, the
I/O request object has a reference count. When the driver completes the
I/O request that the object represents, UMDF automatically drops its
reference on the object and any child objects
such as memory buffers. After the driver that was called completes the
request, it must not attempt to access the request object or any of its
child objects.
2.1. Retrieving Buffers from I/O Requests
The IWDFMemory
interface exposes a memory object, which encapsulates an I/O buffer
that is associated with an I/O request. The memory object can be used to
copy data from the driver to the buffer and vice versa. The driver can
also create its own memory object by calling IWDFDriver::CreatePreallocatedWdfMemory and can then associate that memory object with the buffer that is supplied in an I/O request.
Like other UMDF objects,
memory objects have reference counts and persist until all references to
them have been removed. The buffer that underlies the memory object,
however, might not be “owned” by the object itself. For example, if the
issuer of the I/O request allocated the buffer or if the driver called CreatePreallocatedWdfMemory
to assign an existing driver-created buffer to the object, the memory
object does not “own the buffer.” In this case, the buffer pointer
becomes invalid when the associated I/O request has been completed, even
if the memory object still exists.
Each memory object contains the length of the buffer that it represents. IWDFMemory
methods that copy data to and from the buffer validate the length of
every transfer to prevent buffer over runs and under runs, which can
result in corrupt data or security breaches.
Each memory object also
controls access to the buffer and allows the driver to write only
buffers that support I/O from the device to the buffer. A buffer that is
used to receive data from the device (as in a read request) is
writable. The memory object does not allow write access to a buffer that
only supplies data (as in a write request).
2.2. Sending I/O Requests to an I/O Target
If a driver cannot satisfy
an I/O request by itself, it typically forwards the request to an I/O
target. An I/O target represents a device object to which the driver
sends an I/O request. The default I/O target is typically the next lower
driver in the device stack. A UMDF driver can access the default I/O
target through the IWDFIoTarget interface; it gets a pointer to this interface by calling the IWDFDevice::GetDefaultIoTarget method.
In
addition to forwarding existing I/O requests, some UMDF drivers issue
I/O requests by creating or reusing an I/O request object and sending
the request to an I/O target. Drivers can send requests either
synchronously or asynchronously and can specify a time-out value for
either type of request. If the time-out period expires, UMDF cancels the
request.
In addition to using the
default I/O target, a driver can create additional I/O targets. An I/O
target can be a UMDF driver, a KMDF driver, a WDM driver, or any other
Kernel Mode Driver. UMDF defines two interfaces that create targets:
IWDFFileHandleTargetFactory creates an I/O target that is associated with a file handle that the driver has already opened. The driver calls the Win32 CreateFile
function to open the handle, and then calls methods in this interface
to create the I/O target. This mechanism enables a driver to send I/O
requests to a different device stack.
IWDFUsbTargetFactory creates a USB device object and an associated I/O target.
To create an I/O target, the driver queries the device object for the IWDFFileHandleTargetFactory or IWDFUsbTargetFactory interface and then calls the creation method that is supported by the interface.
If the driver is the originator of the request, it creates an I/O request object by calling IWDFDevice::CreateRequest. If the driver is merely forwarding an existing request, this step is not required.
Whether this is a new or
existing request, the driver must format it before sending it. To format
a request for the default I/O target or for a file handle-based target,
the driver calls methods in the IWDFIoTarget interface. To format an I/O request for a USB target, the driver calls methods in the IWDFUsbTargetDevice, which inherits from IWDFIoTarget.
Formatting the request is important because it specifies the buffers
and buffer lengths that the target should use in performing the I/O.
The driver then can call IWDFIoRequest::Send to send the request. If the driver implements the IRequestCallbackCompletion::OnCompletion and IRequestCallbackCancel::OnCancel interfaces, UMDF calls the driver if the request is completed or canceled.
The I/O target object racks
queued and sent requests and can cancel them when the state of the
target device of the issuing driver changes. UMDF does not free the I/O
target object until all of the I/O requests that have
been sent to it are complete. If the driver created the I/O request, it
must release its reference to the request before deleting it.
By default, UMDF sends a
request only when the target is in the proper state to receive it.
However, a driver can request that UMDF ignore the state of the target
and send the request anyway. If the target device has been stopped (but
not removed), UMDF queues the request to send later after the target
device resumes. If the driver that forwarded the request specifies a
time-out value, the timer starts when the request is added to the queue.
To manage an I/O target, the driver can call methods in the IWDFIoTargetStateManagement interface. These methods enable the driver to start, stop, and remove the target and to query its current state.
2.3. Creating Buffers for I/O Requests
Drivers that issue I/O requests must supply buffers with those requests. A driver can
Allocate the buffer from memory by using the C++ new operator or a Win32 memory allocation function and the call IWdfDriver::CreatePreallocatedWdfMemory to associate the buffer with a memory object. The driver must ensure that the buffer persists until the request has completed.
Call IWdfDriver::CreateWdfMemory
to create a memory object with a specified buffer size. UMDF ensures
that the buffer persists until the I/O request has completed back to the
issuing driver.
Retrieve a memory object from an incoming I/O request for use in a new request.
If the driver uses a memory
object, UMDF takes out a reference on that object on behalf of the new
I/O target when it formats the memory object to send to the I/O target.
This reference persists until one of the following occurs:
The request has been completed.
The driver reformats the request object by calling IWDFRequest::FormatUsingCurrentType or any of the IWDFIoTarget::FormatRequestForXxx methods.
The request has been deleted.
The
driver can retrieve a memory object from an incoming I/O request and
then reformat it for use in a new request to a new I/O target. However,
if the driver has not yet completed the original request, the driver
still has a reference on the memory object. The driver should implement
an I/O completion callback (the IRequestCallbackRequestCompletion interface) for the new I/O request, and in this callback must call Release on the memory object before it completes the original request.
2.4. Canceled and Suspended Requests
Windows I/O is inherently
asynchronous. The system can request that a driver stop processing an
I/O request at any time for many reasons, most commonly:
The thread or process that issued the request cancels it or exits.
A system Plug and Play or power event such as hibernation occurs.
The device is being, or has been, removed.
The action that a driver takes
to stop processing an I/O request depends on the reason for suspension
or cancellation. In general, the driver can either cancel the request or
complete it with an error. In some situations, the system might request
that a driver suspend (temporarily pause) processing; the system
notifies the driver later when to resume processing.
To provide a good user
experience, drivers should provide callbacks to handle cancellation and
suspension of any I/O request that might take a long time to complete or
that might not complete, such as a request for asynchronous input.
2.4.1. Request Cancellation
How UMDF proceeds to cancel an I/O request depends on whether the request has already been delivered to the target driver:
If the request has
never been delivered—either because UMDF has not yet queued it or
because it is still in a queue—UMDF cancels or suspends it automatically
without notifying the driver.
If
the request has been delivered but the driver forwards it to a
different queue, UMDF automatically cancels the request without
notifying the driver.
If
the request has been delivered and is owned by the driver, UMDF does
not cancel it. However, if the driver explicitly marks the request
cancelable by calling the IWDFIoRequest::MarkCancelable method and registering a cancellation callback (IRequestCallbackCancel::OnCancel), UMDF notifies the driver that the request was canceled.
A driver should mark a request cancelable and register an I/O cancellation callback if either of the following is true:
The request involves a long-term operation.
The request might never succeed; for example, the request is waiting for synchronous input.
In the OnCancel
callback, the driver must perform any tasks that are required to cancel
the request, such as stopping any device I/O operations that are in
progress and canceling any related requests that it has already
forwarded to an I/O target. Eventually, the driver must complete the
request with the status ERROR_CANCELLED.
Requests that the driver has
marked cancelable cannot be forwarded to another queue. Before
requeuing a request, the driver must first make it noncancelable by
calling IWDFIoRequest::UnmarkCancelable.
After the request has been added to the new queue, UMDF again considers
it cancelable until that queue dispatches it to the driver.
2.4.2. Request Suspension
When the system transitions to
a sleep state—typically because the user has requested hibernation or
closed the lid on a laptop—a driver can complete, requeue, or continue
to hold any requests that it is currently processing. UMDF notifies the
driver of the impending power change by calling the IQueueCallbackIoStop::OnIoStop
callback for each such request. Each call includes flags that indicate
the reason for stopping the queue and whether the I/O request is
currently cancelable.
Depending on the value of the
flags, the driver can complete the request, requeue the request,
acknowledge the event but continue to hold the request, or ignore the
event if the current request will complete in a timely manner. If the
queue is stopping because the device is being removed, either by an
orderly removal or a surprise removal, the device must complete the
request immediately.
Drivers should implement the OnIoStop
method for any request that might take a long time to complete or that
might not complete, such as a request for asynchronous input. OnIoStop provides a good user experience for laptops and other power-managed systems.
2.5. Completing I/O Requests
To complete an I/O request, a driver calls IWDFIoRequest::Complete or CompleteWithInformation.
In response, UMDF completes the underlying I/O request from the system
and then deletes the I/O request object and any child objects. If the
driver implements the IObjectCleanup::OnCleanup
method for the request object, UMDF invokes that method before
completing the underlying system I/O request, so that the system I/O
request itself is still valid when the callback runs. Because the
underlying request is still valid, the UMDF driver has access to its
parameters and memory buffers.
After Complete or CompleteWithInformation
returns, the I/O request object and its resources have been released.
The driver must not attempt to access the object or any of its
resources, such as parameters and memory buffers that were passed in the
request.
If the request was
dispatched from a sequential queue, the driver’s call to complete the
request might cause UMDF to deliver the next request in the queue. (If
the queue is configured for parallel dispatching, UMDF can deliver
another request at any time.) If the driver holds any locks while it
calls Complete or CompleteWithInformation,
it must ensure that its event callback methods for the queue do not use
the same locks because a deadlock might occur. In practice, this is
difficult to ensure, so the best practice is not to call Complete or CompleteWithInformation while holding a lock.
2.6. Adaptive Time-outs
Drivers for Windows 7 should follow the Windows guidelines for I/O completion and cancellation, which require that drivers:
Support cancellation for I/O requests that might take an indefinite period of time to complete.
Complete I/O requests within a reasonable period (generally, 10 seconds or less) after cancellation.
Do
not block I/O thread for an unreasonable period while performing I/O.
UMDF I/O threads are a limited resource, so blocking on such a thread
for a long time can decrease driver performance.
To aid User Mode Drivers in
conforming to these guidelines, UMDF supports adaptive time-outs. UMDF
tracks the progress on critical I/O operations that can hold up the
system if delayed. Critical operations include cleanup, close,
cancellation, and Plug and Play and power requests.
When the reflector passes a
critical request to the driver host process, it watches for progress to
ensure that I/O operations are proceeding. While such a request is
pending, the User Mode Driver must complete an operation at regular
intervals until it has completed the critical request. If the time-out
period expires, the reflector terminates the host process and reports
the problem through Window Error Reporting (WER). By default, the
time-out is currently one minute. If the driver must perform operations
that take a long time to complete, it should handle them asynchronously,
create a separate thread to handle them, or handle them in a user work
item.