Particle physics involves the "standard model" of the universe, as does WDM. Figure 5-5 shows the ownership process of a typical IRP in each processing phase. Not every type of IRP goes through these steps, because some steps of the device type and IRP type change or do not exist at all. Although there may be various forms of change in this process, this figure provides a good starting point for our discussion.
More complicated than you think...
When you first encounter the standard model of IRP processing, you may think this is a complicated concept. However, unfortunately, this concept cannot satisfy all problems, such as hot plugging devices, dynamic resource redistribution, and power management. In the subsequent sections, I will describe other IRP queuing and cancellation methods to handle these extra problems. The standard model is only a clear reference model for your reading!
Aside from these special problems, many devices can still use this standard model. If your device cannot be deleted or reconfigured while the system is running and the I/O request is rejected in a low power supply, you can use this standard model.
Create IRP
IRP starts when an object calls the I/O manager function to create it. In, I use the term "I/O manager" to describe this entity, although there is indeed a separate system part in the system for creating IRPs. In fact, it is more accurate to say that an entity has created an IRP, not an operating system routine. For example, your driver sometimes creates IRP, and the entity that appears in the first box in the figure should be your driver.
You can use any of the following functions to create IRP:
- IobuildasynchronousfsdrequestCreate an asynchronous IRP (you do not need to wait for it to complete ). This function and the next function are only applicable to creating certain types of IRPs.
- IobuildsynchronousfsdrequestCreate a synchronous IRP (wait until it completes ).
- IobuilddeviceiocontrolrequestCreate a synchronous irp_mj_device_control or irp_mj_internal_device_control request.
- IoallocateirpCreate IRPs of other types not supported by the preceding three functions.
In the first two functionsFSDIt indicates that these functions are dedicated to the file system driver (FSD ). Although FSD is the main user of these two functions, other drivers can also call these functions. The DDK also disclosesIomakeassociatedirpFunction. The WDM driver should not use this function.
It is more complicated to decide which function to call and what extra initialization should be performed on the IRP. I will return to this issue at the end of this chapter.
Dispatch routine
You can callIogetnextirpstacklocationFunction to obtain the pointer of the first stack unit of the IRP. Then initialize the stack unit. At the end of the initialization process, you need to fill in the majorfunction code. After the stack unit Initialization is complete, you can callIocalldriverThe function sends the IRP to the device driver:
PDEVICE_OBJECT DeviceObject; //something gives you thisPIO_STACK_LOCATION stack = IoGetNextIrpStackLocation(Irp);stack->MajorFunction = IRP_MJ_Xxx;
NTSTATUS status = IoCallDriver(DeviceObject, Irp);
The first parameter of the iocalldriver function is the address of the device object you obtain somewhere. At the end of this chapter, I will describe two common methods for getting device object pointers. Here, let's assume that you already have this pointer.
The first stack unit pointer in IRP is initialized to the stack unit before it points to the stack unit, because the I/O stack is actually an array of the io_stack_location structure, you can think that this pointer is initialized to point to a non-existent "-1" element. Therefore, when we want to initialize the first stack unit, what we actually need is "Next" stack unit. Iocalldriver will find 0th table items along the stack pointer, and extract the main function code we put there, in the above example, irp_mj _Xxx. The iocalldriver function then uses the driverobject pointer to find the majorfunction table in the device object. Iocalldriver uses the main function code to index the table, and finally calls the found address (dispatch function ).
You can think of the iocalldriver function as the following code:
NTSTATUS IoCallDriver(PDEVICE_OBJECT device, PIRP Irp){ IoSetNextIrpStackLocation(Irp); PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp); stack->DeviceObject = device; ULONG fcn = stack->MajorFunction; PDRIVER_OBJECT driver = device->DriverObject; return (*driver->MajorFunction[fcn])(device, Irp);}
Responsibilities of the dispatch routine
The prototype of the IRP dispatch routine looks like the following:
Ntstatus dispatchxxx (pdevice_object device, pirp) {pio_stack_location stack = iogetcurrentirpstacklocation (IRP); deviceextension;
- You usually need to access the current stack unit to determine the parameter or sub-function code.
- You may also need to access the device extensions you created.
- You will return an ntstatus code to the iocalldriver function, and the iocalldriver function will return this status code to its caller.
In this book, I useDispatchXxx(For exampleDispatchread,DispatchpnpAnd so on) to represent the dispatch routine in the example driver. Others may use another convention, but Microsoft recommends this method. For example, if your driver is named Random. sys, you should name the irp_mj_read dispatch functionRandomdispatchread. This method makes debugging tracing easier for the driver, but it also requires you to enter more text. Because these names are invisible outside the driver's namespace, you can decide whether to use the naming scheme recommended by Microsoft or the naming method that you think is more meaningful.
In the preceding dispatch function prototype, there are three options:
- The dispatch function immediately completes the IRP.
- Transmits the IRP to the underlying driver in the same stack.
- Queue the IRP for processing by other routines in the driver.
I will discuss these three options in detail in this chapter, but here I will only discuss the possibility of queuing, as this process is described in the standard IRP processing model. You know, when there are a large number of read/write requests entering the device, you usually need to put these requests into a queue to serialize hardware access.
Each device object comes with a request queue object. The following is a standard method for using this queue:
Ntstatus dispatchxxx (...) {... iomarkirppending (IRP );
- Whenever your dispatch routine returns the status_pending status code, you should first call this iomarkirppending function to help the I/O manager avoid internal competition. We must do this before giving up IRP ownership.
- If the device is busy,IostartpacketPut the request in the queue. If the device is idle, iostartpacket will keep the Device Busy and callStartioRoutine. The third parameter of iostartpacket is the ulong address used to sort the queue. For example, the disk driver will specify a cylindrical address here to provide a queue for sequential search. If you specify a null value here, the request is added to the end of the queue. The last parameter is the address of the cancel routine. I will discuss the cancellation routine later in this chapter, which is complicated.
- Return status_pending to notify the caller that this IRP has not been completed.
Note that once we call the iostartpacket function, do not touch the IRP. Before this function is returned, IRP may have been completed and the memory occupied by it may be released. However, our own IRP pointer may be invalid.
Startio routineEach time an IRP is processed, the I/O manager calls it once.StartioRoutine:
VOID StartIo(PDEVICE_OBJECT device, PIRP Irp){ PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp); PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) device->DeviceExtension; ...}
Startio routines get control at the dispatch_level level, which means that this function cannot generate any page faults. In additionCurrentirpDomain andIRPAll parameters point to the IRP sent by the I/O manager.
Startio starts to process IRP. It depends entirely on your device. You usually need to access the hardware registers, but there may be other routines, such as your interrupt service routine, or other routines in the driver also need to access these registers. In fact, sometimes the easiest way to start a new operation is to save some status information in the device extension, and then forge an interruption. Since the execution of these methods must be protected by a spin lock, and this spin lock is the same as the spin lock used to protect your ISR, the correct method is to callKesynchronizeexecutionFunction. For example:
VOID StartIo(...){ ... KeSynchronizeExecution(pdx->InterruptObject, TransferFirst, (PVOID) pdx);}BOOLEAN TransferFirst(PVOID context){ PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) context; ... return TRUE;}
HereTransferfirstA routine is an example of synchcritsection. This is because startio needs to be synchronized with ISR. In chapter 7, I will discuss in detail the concept of synchcritsection.
Once startio makes the Device Busy processing new requests, it returns immediately. When the device completes transmission and interrupts the transmission, you will see the next request.
Service interruption routineAfter the device completes data transmission, it will send a notification in the form of hardware interruption. In chapter 7, I will explain how to useIoconnectinterruptThe function "hooks" an interrupt. One Parameter of this function is the ISR address. Therefore, when an interruption occurs, the hardware abstraction layer (HAL) calls your ISR. ISR runs on dirql and is protected by the spin lock dedicated by ISR. The ISR function prototype is as follows:
BOOLEAN OnInterrupt(PKINTERRUPT InterruptObject, PVOID context){ ...}
The first parameter of Isr is the address of the interrupt object. The interrupt object is created by the ioconnectinterrupt function, but this parameter is unlikely to be used. The second parameter is any context value you specify when you call ioconnectinterrupt. It may be a device object or an extended device address, which is entirely determined by you.
I will discuss in detail the responsibilities of Isr in Chapter 7. To continue the discussion of the standard model, I would like to tell you that the most likely task of an ISR is to schedule the DPC routine (to postpone the process call ). The purpose of DPC is to let you do something, such as callingIocompleterequestAnd the call cannot run at the level of dirql that ISR runs. Therefore, your ISR will have the following line of statements (DeviceIs a pointer to a device object ):
IoRequestDpc(device, device->CurrentIrp, NULL);
Next time, you will see this IRP in the DPC routine. This DPC routine is used in the adddevice function.IoinitializedpcrequestStored. The traditional DPC routine name isDpcforisrBecause it is requested by ISR.
DPC routineDpcforisrThe routine gets control at the dispatch_level level. Generally, it completes the IRP (resulting in recent interruptions ). However, it generally calls the iocompleterequest function to hand over the remaining work to the completion routine.
Void dpcforisr (pkdpc DPC, pdevice_object device, pirp, pdevice_extension PDX) {... iostartnextpacket (device, false );
- IostartnextpacketExtracts the next IRP from the device queue and sends it to startio. The false parameter indicates that the IRP cannot be canceled as usual.
- IocompleterequestComplete the IRP specified by the first parameter. The second parameter is the priority of the waiting thread to be increased. Note that you need to fill the iostatus block in the IRP before calling iocompleterequest.
Calling iocompleterequest is the standard method for processing I/O requests. After this call, the I/O manager (or any entity that creates the IRP at the beginning) will have the IRP again. Finally, the IRP is destroyed by this entity and the blocking status of the waiting thread is removed.
Custom queueSome devices require multiple request queues for operations. A common example is the serial port, which can simultaneously and separately process the input/output request stream. Iostartpacket and iostartnextpacket functions (and other equivalent functions containing the key sorting function) use the queue that comes with the device object. It is easier to create additional queues that work in the same way as standard queues.
To make it easier for us to discuss the issue, let's assume that you need a separate queue to manage irp_mj_special (this primary function code does not exist and is used to make the issue more specific) requests. You will write two auxiliary routines that function like startio and dpcforisr, but are dedicated to processing these hypothetical IRPs:
- Functions similar to startio --- we call itStartiospecial--- It starts the next irp_mj_special request.
- Functions similar to DPC --- we call itDpcspecial--- It processes the irp_mj_special request.
You also need to create a kdevice_queue object in your device extension and initialize this queue object in the adddevice routine:
NTSTATUS AddDevice(...){ ... KeInitializeDeviceQueue(&pdx->dqSpecial); ...}
DqspecialIs the name of the kdevice_object object, used to queue for irp_mj_special requests. A device queue object is a three-state object (see Figure 5-6 ). These three statuses reflect how the device queue routine operates the device queue:
- IdleThe status indicates that the device is not busy processing any requests and the queue is empty.KeinsertdevicequeueOrKeinsertbykeydevicequeueThe function marks the queue as busy-empty and returns false. You should not call this function when the queue is in the idle status.KeremovedevicequeueOrKeremovebykeydevicequeueFunction.
- Busy-emptyThe status indicates that the device is busy but has no IRP in the queue. The keinsertdevicequeue and keinsertbykeydevicequeue functions Add a new IRP to the end of the queue so that the queue enters the busy-not empty state and returns true. The keremovedevicequeue or keremovebykeydevicequeue function returns NULL and enters the idle status of the queue.
- Busy-not emptyThe status indicates that the device is busy and at least one IRP exists in the queue. The keinsertdevicequeue and keinsertbykeydevicequeue functions Add a new IRP to the end of the queue and return true, but the queue status remains unchanged. The keremovedevicequeue or keremovebykeydevicequeue function extracts the first IRP of the queue and returns its address. If the queue is empty, these functions set the queue to the busy-empty state.
In the following code, we use these support routines and our dedicated device queues in the dispatch routine and DPC routine:
Ntstatus dispatchspecial (pdevice_object fdo, piririrp) {iomarkirppending (IRP); deviceextension; If (! Keinsertdevicequeue (& PDX-> dqspecial, & IRP-> tail. Overlay. devicequeueentry) dqspecial );
- As a "regular" dispatch routine, we mark this IRP as pending because we want it to enter the queue, and then the dispatch routine returns the status_pending status.
- The keinsertdevicequeue function and our startiospecial routine hope to be called at the dispatch_level level. So we upgraded IRQL explicitly, and then we will call it again soon.KelowerirqlThe function reduces IRQL to the original level (probably passive_level ).
- KeinsertdevicequeueThe function adds the IRP to the dedicated queue. If the returned value is true, it indicates that the IRP has been added to the queue, so we do not need to do anything about the IRP. If the device is idle, the returned value should be false and IRP does not need to be queued. We call the startiospecial routine directly.
- This in DPCKeremovedevicequeueTwo results are generated by calling. If the queue is empty, the return value is null and we do not need to start a new request. Otherwise, the return value is the address of an IRP embedded connection domain. We can use the containing_record macro to obtain the real IRP address of the peripheral. Then we pass the address to the startiospecial routine. Note that this DPC routine is already running at the dispatch_level level, so we do not need to adjust IRQL before deleting the team list item or calling startio.
The startpacket and startnextpacket functions I described previously useDevicequeueThe kdevice_queue object. This object is an opaque field in the device object. its working principle is the same as managing a private device queue.