Depending on the complexity of the device operation and the kind of I/O request, driver Dispatch routines range from trivial to quite difficult to implement. This section explains how to code these routines. Execution ContextAll Dispatch routines share the same function signature. (A function signature includes the number and type of parameters, and its calling convention.) Table 7.2 shows the prototype for all Dispatch routines. Like the driver's initialization and unload routines, Dispatch routines run at PASSIVE_LEVEL IRQL, which means they can access paged system resources. The I/O Manager invokes Dispatch routines in response to user-mode or kernel-mode requests. Before calling, the I/O Manager builds and fills the IRP with valid data. This includes the pointer to the user's buffer. The user buffer is validated by the I/O Manager to ensure that each page address spanned by the buffer is readable or writeable within the context of the requestor. If the request is for Buffered I/O, the I/O Manager first allocates a nonpaged pool buffer and, if a write request, copies data from the user buffer into the pool. If the request is for Direct I/O, the I/O Manager faults the entire user buffer into physical memory and locks it down. A Dispatch routine can usually track the state of an I/O request using only the IRP. If a Dispatch routine uses any data structures outside the IRP, the driver must ensure that proper synchronization steps are taken. This would mean using a spin lock to coordinate with other driver routines running at DISPATCH_LEVEL or below IRQL, and KeSynchronizeExecution to synchronize with Interrupt Service code. The IRP is shared data, albeit serially, with the I/O Manager. In particular, the I/O Manager uses fields of the Parameters union to complete the I/O request. For example, after a Buffered I/O request, it needs to copy data from the nonpaged pool into the user buffer. It must then deallocate the pool buffer. A field within the Parameters union points to this buffer. Therefore, changing the value of this buffer pointer would lead to disastrous results. In general, if a Dispatch routine needs to modify an IRP field, it should make working copies on the stack or in the device extension.
What Dispatch Routines DoThe exact behavior of a Dispatch routine will depend on the function it supports. However, the general responsibilities of these routines include the following.
Exiting the Dispatch RoutineWhen a Dispatch routine processes an IRP, there are only three possible outcomes.
Each of these possible outcomes is described in more detail in the following sections. SIGNALING AN ERRORIf a Dispatch routine uncovers a problem with the IRP, it needs to reject the request and notify the caller. The following steps describe how to reject an IRP.
The code fragment below shows how a Dispatch routine rejects an I/O request. NTSTATUS DispatchWrite( IN PDEVICE_OBJECT pDO, IN PIRP pIrp ) { : // If the request is not supported by this device // report it and reject the request pIrp->IoStatus.Status = STATUS_NOT_SUPPORTED; // report that no bytes were transferred pIrp->IoStatus.Information = 0; // Mark the IRP as "complete", no priority increment IoCompleteRequest( pIrp, IO_NO_INCREMENT); return STATUS_NOT_SUPPORTED; } Note that after marking the IRP as complete, the I/O Manager is free to release the IRP memory storage from nonpaged pool. As such, it would be incorrect to return pIrp->IoStatus.Status; since the memory pointed to by pIrp has already been released. COMPLETING A REQUESTSome I/O requests can be handled without performing any actual device operations. Opening a handle to a device or returning information stored in the device object are examples of these kinds of requests. To complete such a request in the Dispatch routine, do the following:
The code fragment below shows how a Dispatch routine completes a request. NTSTATUS DispatchClose( IN PDEVICE_OBJECT pDO, IN PIRP pIrp ) { : pIrp->IoStatus.Status = STATUS_SUCCESS; // Indicate that zero bytes of data were transferred pIrp->IoStatus.Information = 0; // "Mark" the IRP as complete - no further processing IoCompleteRequest( pIrp, IO_NO_INCREMENT ); return STATUS_SUCCESS; } SCHEDULING A DEVICE OPERATIONThe last action a Dispatch routine might take is the most likely that it will need to interact with the actual device to fulfill the request. Examples include a data transfer, a control function, or an informational query. In this case, the Dispatch routine must queue the IRP for ultimate processing by the driver's Start I/O routine and then promptly return to the I/O Manager stating that the request is pending. To schedule (queue) a device operation, do the following:
The following code fragment shows how a Dispatch routine schedules a device operation. NTSTATUS DispatchWrite( IN PDEVICE_OBJECT pDO, IN PIRP pIrp ) { : // Mark the IRP as "in progress" IoMarkIrpPending( pIrp ); // Now queue (schedule) the IRP for eventual passage // to the driver's Start I/O routine. // Third parameter allows insertion into the queue // other than at the tail // Fourth parameter allows specification of a // Cancel routine IoStartPacket( pDO, pIrp, 0, NULL ); return STATUS_PENDING; } It is a little-known fact that the I/O Manager automatically completes any IRP that isn't marked pending as soon as the Dispatch routine returns. Unfortunately, this automatic mechanism does not call I/O Completion routines attached to the IRP by higher-level drivers. Consequently, it is important that a driver either calls IoCompleteRequest or IoMarkIrpPending to explicitly set the status of the IRP.
|