Usually, the DispatchDeviceControl routine of a higher-level NT driver simply sets up the I/O stack location in the IRP for the next-lower-level driver and passes it on with IoCallDriver. Such a DispatchDeviceControl routine seldom checks the validity of parameters in the input IRP because the underlying device driver is assumed to have better information about how to handle each device-type-specific I/O control request on its own device.
A possible exception to this general rule is the DispatchDeviceControl routine in the class driver of a class/port driver pair. For more information about handling device control requests in paired class/port drivers, see Section 6.3.4.3.
Any new higher-level NT driver that is not closely associated with a particular device driver should simply set up the I/O stack location for the next-lower-level driver and pass the IRP_MJ_DEVICE_CONTROL request on for further processing.
A device control request is usually handled synchronously. That is, a higher-level driver’s DispatchDeviceControl routine can frequently return control to the system as follows:
: : return IoCallDriver(DeviceObject->NextDeviceObject, Irp);
However, a higher-level driver cannot use the preceding technique if a lower driver might return STATUS_PENDING for such a request. In these circumstances, the higher-level driver should set its IoCompletion routine in the IRP. When its IoCompletion routine is called with the IRP, it can check the I/O status block to determine whether the IRP is still pending. If so, the IoCompletion routine might retry the request or, possibly, call IoMarkIrpPending with the IRP before it calls IoCompleteRequest and returns STATUS_PENDING. A higher-level driver must not complete an IRP with STATUS_PENDING unless it has called IoMarkIrpPending with that IRP first.
If the underlying device driver must process much data transferred from the device before it completes the request, then a higher-level driver might handle such a device control request asynchronously. That is, the higher-level driver might set its IoCompletion routine in the IRP, pass the IRP on to lower drivers, and return control from its own DispatchDeviceControl routine.
Almost all system-defined I/O control codes require the underlying device driver to transfer very modest amounts of data, usually far less than a PAGE_SIZE amount. As a general rule, higher-level NT drivers should handle these requests synchronously, as shown in the preceding code fragment, because the lower drivers return control so quickly. That is, the overhead of calling the higher-level driver’s IoCompletion routine does not compensate for whatever additional IRP processing that driver can get done in such a short interval.
A higher-level NT driver that allocates IRPs with IoBuildDeviceIoControlRequest for an underlying device driver can handle these device control requests synchronously. Such a higher-level driver can wait on an optional Event, passed to IoBuildDeviceIoControlRequest and associated with the driver-allocated IRP. Such a driver must supply storage for the event object and must initialize its Event with KeInitializeEvent before calling IoBuildDeviceIoControlRequest with that Event pointer.