[Previous] [
Particle physics has its "standard model" for the universe, and so does WDM. Figure 5-5 illustrates a typical flow of ownership for an IRP as it progresses through various stages in its life. Not every type of IRP would go through these steps, and some of the steps might be missing or
Figure 5-5. The "standard model" for IRP processing.
It's Even More Complicated than You Thought…The first time you encounter the concepts that make up the standard model for IRP processing, they'll probably seem pretty complicated. Unfortunately, the standard model is also not quite sufficient to handle all the problems that can arise in a
regime that includes hot pluggable devices, dynamic resource reconfiguration, and power management. In later chapters, I'll describe another way of queuing andcancelling IRPs that deals with these extra problems. The standard model will seem like a model of clarity when you're done reading about that!Despite the problems that some devices present, many devices can still
employ the standard model (which is, of course, why I'm bothering to explain it here). If your device cannot be removed or reconfigured while the system is running and can reject I/Orequests while in a low-power state, you can use the standard model.
The IRP begins life when some entity calls an I/O Manager function to create it. In the figure, I used the
You can use any of four functions to create a new IRP:
The
Fsd
in the first two of these function
Deciding which of these functions to call and determining what additional initialization you need to perform on an IRP is a rather complicated matter. I'll come back to this subject, therefore, at the end of this chapter.
After you create an IRP, you call
IoGetNextIrpStackLocation
to obtain a pointer to the first stack location. Then you initialize just that first location. At the very least, you need to fill in the MajorFunction code. Having
PDEVICE_OBJECTDeviceObject;// |
The first argument to IoCallDriver is the address of a device object that you've obtained somehow. I'll describe two common ways of getting a device object pointer at the very end of this chapter in "Where Do Device Object Pointers Come From?" For the time being, imagine that these pointers just come to you out of the blue.
The initial stack location pointer in the IRP gets initialized to one
before
the actual first location. Since the I/O stack is an array of IO_STACK_LOCATION structures, you could think of the stack pointer as being initialized to point to the "-1" element, which doesn't exist. (In fact, the stack "grows" from high toward low addresses, but that detail shouldn't obscure the concept I'm trying to describe here.) We therefore ask for the "next" stack location when we want to initialize the first one. IoCallDriver will advance the stack pointer to the 0 entry and extract the major function code that we left there. That's the made-up value IRP_MJ_
Xxx
in this example. Then, IoCallDriver will follow the DriverObject pointer inside the device object to the MajorFunction table
You can imagine IoCallDriver as looking something like this (but I hasten to add that this is not a copy of the actual source code):
NTSTATUSIoCallDriver(PDEVICE_OBJECTdevice,PIRPIrp)
{
IoSetNextIrpStackLocation(Irp);
PIO_STACK_LOCATIONstack=IoGetCurrentIrpStackLocation(Irp);
stack->DeviceObject=device;
ULONGfcn=stack->MajorFunction;
PDRIVER_OBJECTdriver=device->DriverObject;
return(*driver->MajorFunction[fcn])(device,Irp);
}
|
An archetypal IRP dispatch routine would look similar to this example:
1 |
NTSTATUSDispatch
Xxx
(PDEVICE_OBJECTdevice,PIRPIrp)
{
PIO_STACK_LOCATIONstack=IoGetCurrentIrpStackLocation(Irp);
PDEVICE_EXTENSIONpdx=(PDEVICE_EXTENSION)device->DeviceExtension;
...
returnSTATUS_
Xxx
;
}
|
In this book, I'll be using names of the form
Dispatch
Xxx
(for example,
DispatchRead
,
DispatchPnp
, and so forth) for the dispatch functions in my sample drivers. Other authors use different conventions for these names. Microsoft recommends, for example, that you use a
Where I used an ellipsis in the
Every device object gets a request queue object "for free," and there's a standard way of using this queue:
1 |
NTSTATUSDispatch
Xxx
(...)
{
...
IoMarkIrpPending(Irp);
IoStartPacket(device,Irp,NULL,NULL);
returnSTATUS_PENDING;
}
|
It's very important not to touch the IRP once we call IoStartPacket. By the time that function returns, the IRP may have been completed and the memory it occupies released. The pointer we have might, therefore, now be invalid.
The I/O Manager calls your StartIo routine to process one IRP at a time:
VOIDStartIo(PDEVICE_OBJECTdevice,PIRPIrp)
{
PIO_STACK_LOCATIONstack=IoGetCurrentIrpStackLocation(Irp);
PDEVICE_EXTENSIONpdx=(PDEVICE_EXTENSION)device->DeviceExtension;
...
}
|
Your StartIo routine receives control at DISPATCH_LEVEL, meaning that it must not generate any page faults. In addition, the CurrentIrp field of the device object and the Irp argument will both point to the IRP that's being submitted to you for processing.
Your job in StartIo is to commence the IRP you've been handed. How you do this depends entirely on your device. Often you will need to access hardware registers that are also used by your interrupt service routine (ISR) and, perhaps, by other routines in your driver. In fact, sometimes the
VOIDStartIo(...)
{
...
KeSynchronizeExecution(pdx->InterruptObject,
TransferFirst,(PVOID)pdx);
}
BOOLEANTransferFirst(PVOIDcontext)
{
PDEVICE_EXTENSIONpdx=(PDEVICE_EXTENSION)context;
...
returnTRUE;
}
|
The TransferFirst routine shown here is an example of the generic class of SynchCritSection routines, so called because they are synchronized with the ISR. I'll discuss the SynchCritSection concept in more detail in Chapter 7, "Reading and Writing Data."
Once StartIo gets the device busy handling the new request, it returns. You'll see the request next when your device interrupts to signal that it's done with whatever transfer you started.
When your device is finished transferring data, it might signal a hardware interrupt. In Chapter 7, I'll show you how to use
IoConnectInterrupt
to "hook" the interrupt. One of the arguments to IoConnectInterrupt is the address of your ISR. When the interrupt occurs, the hardware abstraction layer (HAL) calls your ISR. The ISR runs at the device IRQL (DIRQL) of your particular device and under the protection of a spin lock associated
BOOLEANOnInterrupt(PKINTERRUPTInterruptObject,PVOIDcontext)
{
...
}
|
The first argument of your ISR is the address of the interrupt object created by IoConnectInterrupt, but you're
I'll discuss the duties of your ISR in detail in Chapter 7 in connection with reading and writing data, the subject to which interrupt handling is most relevant. To carry on with this discussion of the standard model, I need to tell you that one of the likely things for the ISR to do is to schedule a deferred procedure call (DPC). The purpose of the DPC is to let you do things, like calling IoCompleteRequest , that can't be done at the rarified DIRQL at which your ISR runs. So, supposing you develop a pointer named device to your device object inside the ISR, you'd have a line of code like this one:
IoRequestDpc(device,device->CurrentIrp,NULL); |
You'll next see the IRP in the DPC routine you registered inside AddDevice with your call to IoInitializeDpcRequest . The traditional name for that routine is DpcForIsr because it's the DPC routine your ISR requests.
The
DpcForIsr
routine
1 |
VOIDDpcForIsr(PKDPCDpcPDEVICE_OBJECTdevice,PIRPIrp,PVOIDcontext)
{
...
IoStartNextPacket(device,FALSE);
IoCompleteRequest(Irp,boost);
}
|
The call to IoCompleteRequest is the end of this standard way of handling an I/O request. After that call, the I/O Manager (or whatever created the IRP in the first place) owns the IRP once more. That entity will destroy the IRP and might unblock a thread that has been waiting for the request to complete.
Some devices
To make it easier to discuss things, let's suppose that you need a separate queue to manage IRP_MJ_SPECIAL requests. (There's no such major function code—I made it up just so that we'd have a concrete topic for the discussion.) You would write two helper functions that would do for these special IRPs pretty much the same thing as the StartIo and DpcForIsr routines I mentioned earlier:
You'll also create a KDEVICE_QUEUE object in your device extension. You'd initialize this object during AddDevice:
NTSTATUSAddDevice(...)
{
...
KeInitializeDeviceQueue(&pdx->dqSpecial);
...
}
|
where
dqSpecial
is the name of the KDEVICE_OBJECT we'll use for IRP_MJ_SPECIAL requests. A device queue object is a
Figure 5-6. States of a KDEVICE_QUEUE queue.
We use these support routines and the special device queue in our dispatch and DPC routines, as
1 |
NTSTATUSDispatchSpecial(PDEVICE_OBJECTfdo,PIRPIrp)
{
IoMarkIrpPending(Irp);
KIRQLoldirql;
KeRaiseIrql(DISPATCH_LEVEL,&oldirql);
PDEVICE_EXTENSIONpdx=(PDEVICE_EXTENSION)fdo->DeviceExtension;
if(!KeInsertDeviceQueue(&pdx->dqSpecial,
&Irp->Tail.Overlay.DeviceQueueEntry))
StartIoSpecial(fdo,Irp);
KeLowerIrql(oldirql);
returnSTATUS_PENDING;
}
VOIDDpcSpecial(...)
{
...
PKDEVICE_QUEUE_ENTRYqep=KeRemoveDeviceQueue(&pdx->dqSpecial);
if(qep)
StartIoSpecial(fdo,CONTAINING_RECORD(qep,IRP,
Tail.Overlay.DeviceQueueEntry));
...
}
|
It's no

The Windows 2000 Device Driver Book: A Guide for Programmers (2nd Edition)

Windows System Programming (4th Edition) (Addison-Wesley Microsoft Technology Series)

Developing Drivers with the Windows Driver Foundation (Pro Developer)

Windowsu00ae Internals: Including Windows Server 2008 and Windows Vista, Fifth Edition (Pro Developer)