The Standard Model for IRP Processing

The Standard Model for IRP Processing

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 will go through these steps, and some of the steps might be missing or altered depending on the type of device and the type of IRP. Notwithstanding the possible variability, however, the picture provides a useful starting point for discussion.

figure 5-5 the  standard model  for irp processing.

Figure 5-5. The standard model for IRP processing.

When you engage I/O Verification, the Driver Verifier makes a few basic checks on how you handle IRPs. Extended I/O Verification includes many more checks. Because there are so many tests, however, I didn t put the Driver Verifier flag in the margin for every one of them. Basically, if the DDK or this chapter tells you not to do something, there is probably a Driver Verifier test to make sure you don t.

Creating an IRP

The IRP begins life when an entity calls an I/O Manager function to create it. In Figure 5-5, I used the term I/O Manager to describe this entity, as though there were a single system component responsible for creating IRPs. In reality, no such single actor in the population of operating system routines exists, and it would have been more accurate to just say that somebody creates the IRP. Your own driver will be creating IRPs from time to time, for example, and you ll occupy the initial ownership box for those particular IRPs.

You can use any of four functions to create a new IRP:

  • IoBuildAsynchronousFsdRequest builds an IRP on whose completion you don t plan to wait. This function and the next are appropriate for building only certain types of IRP.

  • IoBuildSynchronousFsdRequest builds an IRP on whose completion you do plan to wait.

  • IoBuildDeviceIoControlRequest builds a synchronous IRP_MJ_DE VICE_CONTROL or IRP_MJ_INTERNAL_DEVICE_CONTROL request.

  • IoAllocateIrp builds an asynchronous IRP of any type.

The Fsd in the first two of these function names stands for file system driver (FSD). Any driver is allowed to call these functions to create an IRP destined for any other driver, though. The DDK also documents a function named IoMakeAssociatedIrp for building an IRP that s subordinate to some other IRP. WDM drivers should not call this function. Indeed, completion of associated IRPs doesn t work correctly in Microsoft Windows 98/Me anyway.

NOTE
Throughout this chapter, I use the terms synchronous and asynchronous IRPs because those are the terms used in the DDK. Knowledgeable developers in Microsoft wish that the terms threaded and nonthreaded had been chosen because they better reflect the way drivers use these two types of IRP. As should become clear, you use a synchronous, or threaded, IRP in a non-arbitrary thread that you can block while you wait for the IRP to finish. You use an asynchronous, or nonthreaded, IRP in every other case.

Creating Synchronous IRPs

Deciding which of these functions to call and determining what additional initialization you need to perform on an IRP is a rather complicated matter. IoBuildSynchronousFsdRequest and IoBuildDeviceIoControlRequest create a so-called synchronous IRP. The I/O Manager considers that a synchronous IRP belongs to the thread in whose context you create the IRP. This ownership concept has several consequences:

  • If the owning thread terminates, the I/O Manager automatically cancels any pending synchronous IRPs that belong to that thread.

  • Because the creating thread owns a synchronous IRP, you shouldn t create one in an arbitrary thread you most emphatically do not want the I/O Manager to cancel the IRP because this thread happens to terminate.

  • Following a call to IoCompleteRequest, the I/O Manager automatically cleans up a synchronous IRP and signals an event that you must provide.

  • You must take care that the event object still exists at the time the I/O Manager signals it.

Refer to IRP handling scenario number 6 at the end of this chapter for a code sample involving a synchronous IRP.

You must call these two functions at PASSIVE_LEVEL only. In particular, you must not be at APC_LEVEL (say, as a result of acquiring a fast mutex) because the I/O Manager won t then be able to deliver the special kernel asynchronous procedure call (APC) that does all the completion processing. In other words, you mustn t do this:

PIRP Irp = IoBuildSynchronousFsdRequest(...); ExAcquireFastMutex(...); NTSTATUS status = IoCallDriver(...); if (status == STATUS_PENDING) KeWaitForSingleObject(...); // <== don't do this ExReleaseFastMutex(...);

The problem with this code is that the KeWaitForSingleObject call will deadlock: when the IRP completes, IoCompleteRequest will schedule an APC in this thread. The APC routine, if it could run, would set the event. But because you re already at APC_LEVEL, the APC cannot run in order to set the event.

If you need to synchronize IRPs sent to another driver, consider the following alternatives:

  • Use a regular kernel mutex instead of an executive fast mutex. The regular mutex leaves you at PASSIVE_LEVEL and doesn t inhibit special kernel APCs.

  • Use KeEnterCriticalRegion to inhibit all but special kernel APCs, and then use ExAcquireFastMutexUnsafe to acquire the mutex. This technique won t work in the original release of Windows 98 because KeEnterCriticalRegion wasn t supported there. It will work on all later WDM platforms.

  • Use an asynchronous IRP. Signal an event in the completion routine. Refer to IRP-handling scenario 8 at the end of this chapter for a code sample.

A final consideration in calling the two synchronous IRP routines is that you can t create just any kind of IRP using these routines. See Table 5-1 for the details. A common trick for creating another kind of synchronous IRP is to ask for an IRP_MJ_SHUTDOWN, which has no parameters, and then alter the MajorFunction code in the first stack location.

Table 5-1. Synchronous IRP Types

Support Function

Types of IRP You Can Create

IoBuildSynchronousFsdRequest

IRP_MJ_READ

IRP_MJ_WRITE

IRP_MJ_FLUSH_BUFFERS

IRP_MJ_SHUTDOWN

IRP_MJ_PNP

IRP_MJ_POWER (but only for IRP_MN_POWER_SEQUENCE)

IoBuildDeviceIoControlRequest

IRP_MJ_DEVICE_CONTROL

IRP_MJ_INTERNAL_DEVICE_CONTROL

Creating Asynchronous IRPs

The other two IRP creation functions IoBuildAsynchronousFsdRequest and IoAllocateIrp create an asynchronous IRP. Asynchronous IRPs don t belong to the creating thread, and the I/O Manager doesn t schedule an APC and doesn t clean up when the IRP completes. Consequently:

  • When a thread terminates, the I/O Manager doesn t try to cancel any asynchronous IRPs that you happen to have created in that thread.

  • It s OK to create asynchronous IRPs in an arbitrary or nonarbitrary thread.

  • Because the I/O Manager doesn t do any cleanup when the IRP completes, you must provide a completion routine that will release buffers and call IoFreeIrp to release the memory used by the IRP.

  • Because the I/O Manager doesn t automatically cancel asynchronous IRPs, you might have to provide code to do that when you no longer want the operation to occur.

  • Because you don t wait for an asynchronous IRP to complete, you can create and send one at IRQL <= DISPATCH_LEVEL (assuming, that is, that the driver to which you send the IRP can handle the IRP at elevated IRQL you must check the specifications for that driver!). Furthermore, it s OK to create and send an asynchronous IRP while owning a fast mutex.

Refer to Table 5-2 for a list of the types of IRP you can create using the two asynchronous IRP routines. Note that IoBuildSynchronousFsdRequest and IoBuildAsynchronousFsdRequest support the same IRP major function codes.

Table 5-2. Asynchronous IRP Types

Support Function

Types of IRP You Can Create

IoBuildAsynchronousFsdRequest

IRP_MJ_READ

IRP_MJ_WRITE

IRP_MJ_FLUSH_BUFFERS

IRP_MJ_SHUTDOWN

IRP_MJ_PNP

IRP_MJ_POWER (but only for IRP_MN_POWER_SEQUENCE)

IoAllocateIrp

Any (but you must initialize the MajorFunction field of the first stack location)

IRP-handling scenario numbers 5 and 8 at the end of this chapter contain cookbook code for using asynchronous IRPs.

Forwarding to a Dispatch Routine

After you create an IRP, you call IoGetNextIrpStackLocation to obtain a pointer to the first stack location. Then you initialize just that first location. If you ve used IoAllocateIrp to create the IRP, you need to fill in at least the MajorFunction code. If you ve used another of the four IRP-creation functions, the I/O Manager might have already done the required initialization. You might then be able to skip this step, depending on the rules for that particular type of IRP. Having initialized the stack, you call IoCallDriver to send the IRP to a device driver:

PDEVICE_OBJECT DeviceObject; // <== somebody gives you this PIO_STACK_LOCATION stack = IoGetNextIrpStackLocation(Irp); stack->MajorFunction = IRP_MJ_Xxx; <other initialization of "stack">NTSTATUS status = IoCallDriver(DeviceObject, Irp);

The first argument to IoCallDriver is the address of a device object that you ve obtained somehow. Often you re sending an IRP to the driver under yours in the PnP stack. In that case, the DeviceObject in this fragment is the LowerDeviceObject you saved in your device extension after calling IoAttachDeviceToDeviceStack. I ll describe some other common ways of locating a device object in a few paragraphs.

The I/O Manager initializes the stack location pointer in the IRP to 1 before the actual first location. Because the I/O stack is an array of IO_STACK_LOCATION structures, you can 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.

What IoCallDriver Does

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):

NTSTATUS IoCallDriver(PDEVICE_OBJECT DeviceObject, PIRP Irp) { IoSetNextIrpStackLocation(Irp); PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp); stack->DeviceObject = DeviceObject; ULONG fcn = stack->MajorFunction; PDRIVER_OBJECT driver = DeviceObject->DriverObject; return (*driver->MajorFunction[fcn])(DeviceObject, Irp); }

As you can see, IoCallDriver simply advances the stack pointer and calls the appropriate dispatch routine in the driver for the target device object. It returns the status code that that dispatch routine returns. Sometimes I see online help requests wherein people attribute one or another unfortunate action to IoCallDriver. (For example, IoCallDriver is returning an error code for my IRP . ) As you can see, the real culprit is a dispatch routine in another driver.

Locating Device Objects

Apart from IoAttachDeviceToDeviceStack, drivers can locate device objects in at least two ways. I ll tell you here about IoGetDeviceObjectPointer and IoGetAttachedDeviceReference.

IoGetDeviceObjectPointer

If you know the name of the device object, you can call IoGetDeviceObjectPointer as shown here:

PUNICODE_STRING devname; // <== somebody gives you this ACCESS_MASK access; // <== more about this later PDEVICE_OBJECT DeviceObject; PFILE_OBJECT FileObject; NTSTATUS status; ASSERT(KeGetCurrentIrql() == PASSIVE_LEVEL); status = IoGetDeviceObjectPointer(devname, access, &FileObject, &DeviceObject);

This function returns two pointers: one to a FILE_OBJECT and one to a DEVICE_OBJECT.

To help defeat elevation-of-privilege attacks, specify the most restricted access consistent with your needs. For example, if you ll just be reading data, specify FILE_READ_DATA.

When you create an IRP for a target you discover this way, you should set the FileObject pointer in the first stack location. Furthermore, it s a good idea to take an extra reference to the file object until after IoCallDriver returns. The following fragment illustrates both these ideas:

PIRP Irp = IoXxx(...); PIO_STACK_LOCATION stack = IoGetNextIrpStackLocation(Irp); ObReferenceObject(FileObject); stack->FileObject = FileObject;<etc.> IoCallDriver(DeviceObject, Irp); ObDereferenceObject(FileObject);

The reason you put the file object pointer in each stack location is that the target driver might be using fields in the file object to record per-handle information. The reason you take an extra reference to the file object is that you ll have code somewhere in your driver that dereferences the file object in order to release your hold on the target device. (See the next paragraph.) Should that code execute before the target driver s dispatch routine returns, the target driver might be removed from memory before its dispatch routine returns. The extra reference prevents that bad result.

NOTE
Removability of devices in a Plug and Play environment is the ultimate source of the early-unload problem mentioned in the text. I discuss this problem in much greater detail in the next chapter. The upshot of that discussion is that it s your responsibility to avoid sending an IRP to a driver that might no longer be in memory and to prevent the PnP manager from unloading a driver that s still processing an IRP you ve sent to that driver. One aspect of how you fulfill that responsibility is shown in the text: take an extra reference to the file object returned by IoGetDeviceObjectPointer around the call to IoCallDriver. In most drivers, you ll probably need the extra reference only when you re sending an asynchronous IRP. In that case, the code that ordinarily dereferences the file object is likely to be in some other part of your driver that runs asynchronously with the call to IoCallDriver say, in the completion routine you re obliged to install for an asynchronous IRP. If you send a synchronous IRP, you re much more likely to code your driver in such a way that you don t dereference the file object until the IRP completes.

When you no longer need the device object, dereference the file object:

                 IoDereferenceObject(FileObject);               

After making this call, don t use either of the file or device object pointers.

IoGetDeviceObjectPointer performs several steps to locate the two pointers that it returns to you:

  1. It uses ZwOpenFile to open a kernel handle to the named device object. Internally, this will cause the Object Manager to create a file object and to send an IRP_MJ_CREATE to the target device. ZwOpenFile returns a file handle.

  2. It calls ObReferenceObjectByHandle to get the address of the FILE_OBJECT that the handle represents. This address becomes the FileObject return value.

  3. It calls IoGetRelatedDeviceObject to get the address of the DEVICE_OBJECT to which the file object refers. This address becomes the DeviceObject return value.

  4. It calls ZwClose to close the handle.

Names for Device Objects

For you to use IoGetDeviceObjectPointer, a driver in the stack for the device to which you want to connect must have named a device object. We studied device object naming in Chapter 2. Recall that a driver might have specified a name in the \Device folder in its call to IoCreateDevice, and it might have created one or more symbolic links in the \DosDevices folder. If you know the name of the device object or one of the symbolic links, you can use that name in your call to IoGetDeviceObjectPointer.

Instead of naming a device object, the function driver for the target device might have registered a device interface. I showed you the user-mode code for enumerating instances of registered interfaces in Chapter 2. I ll discuss the kernel-mode equivalent of that enumeration code in Chapter 6, when I discuss Plug and Play. The upshot of that discussion is that you can obtain the symbolic link names for all the devices that expose a particular interface. With a bit of effort, you can then locate the desired device object.

The reference that IoGetDeviceObjectPointer claims to the file object effectively pins the device object in memory too. Releasing that reference indirectly releases the device object.

Based on this explanation of how IoGetDeviceObjectPointer works, you can see why it will sometimes fail with STATUS_ACCESS_DENIED, even though you haven t done anything wrong. If the target driver implements a one handle only policy, and if a handle happens to be open, the driver will cause the IRP_MJ_CREATE to fail. That failure causes the ZwOpenFile call to fail in turn. Note that you can expect this result if you try to locate a device object for a serial port or SmartCard reader that happens to already be open.

Sometimes driver programmers decide they don t want the clutter of two pointers to what appears to be basically the same object, so they release the file object immediately after calling IoGetDeviceObjectPointer, as shown here:

status = IoGetDeviceObjectPointer(...); ObReferenceObject(DeviceObject); ObDereferenceObject(FileObject);

Referencing the device object pins it in memory until you dereference it. Dereferencing the file object allows the I/O Manager to delete it right away.

Releasing the file object immediately might or might not be OK, depending on the target driver. Consider these fine points before you decide to do it:

  • Deferencing the file object will cause the I/O Manager to send an immediate IRP_MJ_CLEANUP to the target driver.

  • IRPs that the target driver queues will no longer be associated with a file object. When you eventually release the device object reference, the target driver will probably not be able to cancel any IRPs you sent it that remain on its queues.

  • In many situations, the I/O Manager will also send an IRP_MJ_CLOSE to the target driver. (If you ve opened a disk file, the file system driver s use of the system cache will probably cause the IRP_MJ_CLOSE to be deferred.) Many drivers, including the standard driver for serial ports, will now refuse to process IRPs that you send them.

  • Instead of claiming an extra reference to the file object around calls to IoCallDriver, you ll want to reference the device object instead.

NOTE
I recommend avoiding an older routine named IoAttachDevice, which appears superficially to be a sort-of combination of IoGetDeviceObjectPointer and IoAttachDeviceToDevice Stack. The older routine does its internal ZwClose call after attaching your device object. Your driver will receive the resulting IRP_MJ_CLOSE. To handle the IRP correctly, you must call IoAttachDevice in such a way that your dispatch routine has access to the location you specify for the output DEVICE_OBJECT pointer. It turns out that IoAttachDevice sets your output pointer before calling ZwClose and depends on you using it to forward the IRP_MJ_CLOSE to the target device. This is the only example I ve seen in many decades of programming where you re required to use the return value from a function before the function actually returns.

IoGetAttachedDeviceReference

To send an IRP to all the drivers in your own PnP stack, use IoGetAttachedDeviceReference, as shown here:

PDEVICE_OBJECT tdo = IoGetAttachedDeviceReference(fdo);  ObDereferenceObject(tdo);

This function returns the address of the topmost device object in your own stack and claims a reference to that object. Because of the reference you hold, you can be sure that the pointer will remain valid until you release the reference. As discussed earlier, you might also want to take an extra reference to the topmost device object until IoCallDriver returns.

Duties of a Dispatch Routine

An archetypal IRP dispatch routine would look similar to this example:

NTSTATUS DispatchXxx(PDEVICE_OBJECT fdo, PIRP Irp) { 

PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);

PDEVICE_EXTENSION pdx =

(PDEVICE_EXTENSION) device->DeviceExtension; return STATUS_Xxx; }

  1. You generally need to access the current stack location to determine parameters or to examine the minor function code.

  2. You also generally need to access the device extension you created and initialized during AddDevice.

  3. You ll be returning some NTSTATUS code to IoCallDriver, which will propagate the code back to its caller.

Where I used an ellipsis in the foregoing prototypical dispatch function, a dispatch function has to choose between three courses of action. It can complete the request immediately, pass the request down to a lower-level driver in the same driver stack, or queue the request for later processing by other routines in this driver.

Completing an IRP

Someplace, sometime, someone must complete every IRP. You might want to complete an IRP in your dispatch routine in cases like these:

  • If the request is erroneous in some easily determined way (such as a request to rewind a printer or to eject the keyboard), the dispatch routine should cause the request to fail by completing it with an appropriate status code.

  • If the request calls for information that the dispatch function can easily determine (such as a control request asking for the driver s version number), the dispatch routine should provide the answer and complete the request with a successful status code.

Mechanically, completing an IRP entails filling in the Status and Information members within the IRP s IoStatus block and calling IoCompleteRequest. The Status value is one of the codes defined by manifest constants in the DDK header file NTSTATUS.H. Refer to Table 5-3 for an abbreviated list of status codes for common situations. The Information value depends on what type of IRP you re completing and on whether you re causing the IRP to succeed or to fail. Most of the time, when you re causing an IRP to fail (that is, completing it with an error status of some kind), you ll set Information to 0. When you cause an IRP that involves data transfer to succeed, you ordinarily set the Information field equal to the number of bytes transferred.

Table 5-3. Some Commonly Used NTSTATUS Codes

Status Code

Description

STATUS_SUCCESS

Normal completion.

STATUS_UNSUCCESSFUL

Request failed, but no other status code describes the reason specifically.

STATUS_NOT_IMPLEMENTED

A function hasn t been implemented.

STATUS_INVALID_HANDLE

An invalid handle was supplied for an operation.

STATUS_INVALID_PARAMETER

A parameter is in error.

STATUS_INVALID_DEVICE_REQUEST

The request is invalid for this device.

STATUS_END_OF_FILE

End-of-file marker reached.

STATUS_DELETE_PENDING

The device is in the process of being removed from the system.

STATUS_INSUFFICIENT_RESOURCES

Not enough system resources (often memory) to perform an operation.

NOTE
Always be sure to consult the DDK documentation for the correct setting of IoStatus.Information for the IRP you re dealing with. In some flavors of IRP_MJ_PNP, for example, this field is used as a pointer to a data structure that the PnP Manager is responsible for releasing. If you were to overstore the Information field with 0 when causing the request to fail, you would unwittingly cause a resource leak.

Because completing a request is something you do so often, I find it useful to have a helper routine to carry out the mechanics:

NTSTATUS CompleteRequest(PIRP Irp, NTSTATUS status, ULONG_PTR Information) { Irp->IoStatus.Status = status; Irp->IoStatus.Information = Information; IoCompleteRequest(Irp, IO_NO_INCREMENT); return status; }

I defined this routine in such a way that it returns whatever status value you supply as its second argument. That s because I m such a lazy typist: the return value allows me to use this helper whenever I want to complete a request and then immediately return a status code. For example:

NTSTATUS DispatchControl(PDEVICE_OBJECT fdo, PIRP Irp) { PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp); ULONG code = stack->Parameters.DeviceIoControl.IoControlCode; if (code == IOCTL_TOASTER_BOGUS) return CompleteRequest(Irp, STATUS_INVALID_DEVICE_REQUEST, 0);  }

You might notice that the Information argument to the CompleteRequest function is typed as a ULONG_PTR. In other words, this value can be either a ULONG or a pointer to something (and therefore potentially 64 bits wide).

When you call IoCompleteRequest, you supply a priority boost value to be applied to whichever thread is currently waiting for this request to complete. You normally choose a boost value that depends on the type of device, as suggested by the manifest constant names listed in Table 5-4. The priority adjustment improves the throughput of threads that frequently wait for I/O operations to complete. Events for which the end user is directly responsible, such as keyboard or mouse operations, result in greater priority boosts in order to give preference to interactive tasks. Consequently, you want to choose the boost value with at least some care. Don t use IO_SOUND_INCREMENT for absolutely every operation a sound card driver finishes, for example it s not necessary to apply this extraordinary priority increment to a get-driver-version control request.

Table 5-4. Priority Boost Values for IoCompleteRequest

Manifest Constant

Numeric Priority Boost

IO_NO_INCREMENT

0

IO_CD_ROM_INCREMENT

1

IO_DISK_INCREMENT

1

IO_KEYBOARD_INCREMENT

6

IO_MAILSLOT_INCREMENT

2

IO_MOUSE_INCREMENT

6

IO_NAMED_PIPE_INCREMENT

2

IO_NETWORK_INCREMENT

2

IO_PARALLEL_INCREMENT

1

IO_SERIAL_INCREMENT

2

IO_SOUND_INCREMENT

8

IO_VIDEO_INCREMENT

1

Don t, by the way, complete an IRP with the special status code STATUS_PENDING. Dispatch routines often return STATUS_PENDING as their return value, but you should never set IoStatus.Status to this value. Just to make sure, the checked build of IoCompleteRequest generates an ASSERT failure if it sees STATUS_PENDING in the ending status. Another popular value for people to use by mistake is apparently -1, which doesn t have any meaning as an NTSTATUS code at all. There s a checked-build ASSERT to catch that mistake too. The Driver Verifier will complain if you try to do either of these bad things.

Before calling IoCompleteRequest, be sure to remove any cancel routine that you might have installed for an IRP. As you ll learn later in this chapter, you install a cancel routine while you keep an IRP in a queue. You must remove an IRP from the queue before completing it. All the queuing schemes I ll discuss in this book clear the cancel routine pointer when they dequeue an IRP. Therefore, you probably don t need to have additional code in your driver as in this sample:

IoSetCancelRoutine(Irp, NULL); // <== almost certainly redundant IoCompleteRequest(Irp, ...);

So far, I ve just explained how to call IoCompleteRequest. That function performs several tasks that you need to understand:

  • Calling completion routines that various drivers might have installed. I ll discuss the important topic of I/O completion routines later in this chapter.

  • Unlocking any pages belonging to Memory Descriptor List (MDL) structures attached to the IRP. An MDL will be used for the buffer for an IRP_MJ_READ or IRP_MJ_WRITE for a device whose device object has the DO_BUFFERED_IO flag set. Control operations also use an MDL if the control code s buffering method specifies one of the METHOD_XX_DIRECT methods. I ll discuss these issues more fully in Chapter 7 and Chapter 9, respectively.

  • Scheduling a special kernel APC to perform final cleanup on the IRP. This cleanup includes copying input data back to a user buffer, copying the IRP s ending status, and signaling whichever event the originator of the IRP might be waiting on. The fact that completion processing includes an APC, and that the cleanup includes setting an event, imposes some exacting requirements on the way a driver implements a completion routine, so I ll also discuss this aspect of I/O completion in more detail later.

Passing an IRP Down the Stack

The whole goal of the layering of device objects that WDM facilitates is for you to be able to easily pass IRPs from one layer down to the next. Back in Chapter 2, I discussed how your AddDevice routine would contribute its portion of the effort required to create a stack of device objects with a statement like this one:

pdx->LowerDeviceObject = IoAttachDeviceToDeviceStack(fdo, pdo);

where fdo is the address of your own device object and pdo is the address of the physical device object (PDO) at the bottom of the device stack. IoAttachDeviceToDeviceStack returns to you the address of the device object immediately underneath yours. When you decide to forward an IRP that you received from above, this is the device object you ll specify in the eventual call to IoCallDriver.

Before passing an IRP to another driver, be sure to remove any cancel routine that you might have installed for the IRP. As I mentioned just a few paragraphs ago, you ll probably fulfill this requirement without specifically worrying about it. Your queue management code will zero the cancel routine pointer when it dequeues an IRP. If you never queued the IRP in the first place, the driver above you will have made sure the cancel routine pointer was NULL. The Driver Verifier will make sure that you don t break this rule.

When you pass an IRP down, you have the additional responsibility of initializing the IO_STACK_LOCATION that the next driver will use to obtain its parameters. One way of doing this is to perform a physical copy, like this:

 IoCopyCurrentIrpStackLocationToNext(Irp); status = IoCallDriver(pdx->LowerDeviceObject, Irp); 

IoCopyCurrentIrpStackLocationToNext is a macro in WDM.H that copies all the fields in an IO_STACK_LOCATION except for the ones that pertain to the I/O completion routines from the current stack location to the next one. In previous versions of Windows NT, kernel-mode driver writers sometimes copied the entire stack location, which would cause the caller s completion routine to be called twice. The IoCopyCurrentIrpStackLocationToNext macro, which is new with the WDM, avoids the problem.

If you don t care what happens to an IRP after you pass it down the stack, use the following alternative to IoCopyCurrentIrpStackLocationToNext:

NTSTATUS ForwardAndForget(PDEVICE_OBJECT fdo, PIRP Irp) { PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension; IoSkipCurrentIrpStackLocation(Irp); return IoCallDriver(pdx->LowerDeviceObject, Irp); }

IoSkipCurrentIrpStackLocation retards the IRP s stack pointer by one position. IoCallDriver will immediately advance the stack pointer. The net effect is to not change the stack pointer. When the next driver s dispatch routine calls IoGetCurrentIrpStackLocation, it will retrieve exactly the same IO_STACK_LOCATION pointer that we were working with, and it will thereby process exactly the same request (same major and minor function codes) with the same parameters.

CAUTION
The version of IoSkipCurrentIrpStackLocation that you get when you use the Windows Me or Windows 2000 build environment in the DDK is a macro that generates two statements without surrounding braces. Therefore, you mustn t use it in a construction like this:

if (<expression>) IoSkipCurrentIrpStackLocation(Irp); // <== don't do this!

The explanation of why IoSkipCurrentIrpStackLocation works is so tricky that I thought an illustration might help. Figure 5-6 illustrates a situation in which three drivers are in a particular stack: yours (the function device object [FDO]) and two others (an upper filter device object [FiDO] and the PDO). In the picture on the left, you see the relationship between stack locations, parameters, and completion routines when we do the copy step with IoCopyCurrent IrpStackLocationToNext. In the picture on the right, you see the same relationships when we use the IoSkipCurrentIrpStackLocation shortcut. In the right-hand picture, the third and last stack location is fallow, but nobody gets confused by that fact.

figure 5-6 comparison of copying vs. skipping i/o stack locations.

Figure 5-6. Comparison of copying vs. skipping I/O stack locations.

Queuing an IRP for Later Processing

The third alternative action for a dispatch routine is to queue the IRP for later processing. The following code snippet assumes you re using one of my DEVQUEUE queue objects for IRP queuing. I ll explain the DEVQUEUE object later in this chapter.

NTSTATUS DispatchSomething(PDEVICE_OBJECT fdo, PIRP Irp) {  

IoMarkIrpPending(Irp);

StartPacket(&pdx->dqSomething, fdo, Irp, CancelRoutine);

return STATUS_PENDING; }

  1. Whenever we return STATUS_PENDING from a dispatch routine (as we re about to do here), we make this call to help the I/O Manager avoid an internal race condition. We must do this before we relinquish ownership of the IRP.

  2. If our device is currently busy or stalled because of a PnP or Power event, StartPacket puts the request in a queue. Otherwise, StartPacket marks the device as busy and calls our StartIo routine. I ll describe the StartIo routine in the next section. The last argument is the address of a cancel routine. I ll discuss cancel routines later in this chapter.

  3. We return STATUS_PENDING to tell our caller that we re not done with this IRP yet.

It s important not to touch the IRP once we call StartPacket. By the time that function returns, the IRP might have been completed and the memory it occupies released. The pointer we have might, therefore, now be invalid.

The StartIo Routine

IRP-queuing schemes often revolve around calling a StartIo function to process IRPs:

VOID StartIo(PDEVICE_OBJECT device, PIRP Irp) { PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp); PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) device->DeviceExtension;  }

A StartIo routine generally receives control at DISPATCH_LEVEL, meaning that it must not generate any page faults.

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 easiest way to commence a new operation is to store some state information in your device extension and then fake an interrupt. Because either of these approaches needs to be carried out under the protection of the same spin lock that protects your ISR, the correct way to proceed is to call KeSynchronizeExecution. For example:

VOID StartIo(...) {  KeSynchronizeExecution(pdx->InterruptObject, TransferFirst, (PVOID) pdx); } BOOLEAN TransferFirst(PVOID context) { PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) context; <initialize device for new operation> return TRUE; }

The TransferFirst routine shown here is an example of the generic class of SynchCritSection routines, so called because they re synchronized with the ISR. I ll discuss the SynchCritSection concept in more detail in Chapter 7.

In Windows XP and later systems, you can follow this template instead of calling KeSynchronizeExecution:

VOID StartIo(...) { KIRQL oldirql = KeAcquireInterruptSpinLock(pdx->InterruptObject); <initialize device for new operation> KeReleaseInterruptSpinLock(pdx->InterruptObject, oldirql); }

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.

The Interrupt Service Routine

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 an interrupt occurs, the system calls your ISR. The ISR runs at the device IRQL (DIRQL) of your particular device and under the protection of a spin lock associated specifically with your ISR. The ISR has the following skeleton:

BOOLEAN OnInterrupt(PKINTERRUPT InterruptObject, PDEVICE_EXENSION pdx) { if (<my device didn't interrupt>) return FALSE;  return TRUE; }

The first argument of your ISR is the address of the interrupt object created by IoConnectInterrupt, but you re unlikely to use this argument. The second argument is whatever context value you specified in your original call to IoConnectInterrupt; it will probably be the address of your device extension, as shown in this fragment.

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, such as calling IoCompleteRequest, that can t be done at the rarified DIRQL at which your ISR runs. So you might have a line of code like this one:

IoRequestDpc(pdx->DeviceObject, NULL, pdx);

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.

Deferred Procedure Call Routine

The DpcForIsr routine requested by your ISR receives control at DISPATCH_LEVEL. Generally, its job is to finish up the processing of the IRP that caused the most recent interrupt. Often that job entails calling IoComplete Request to complete this IRP and StartNextPacket to remove the next IRP from your device queue for forwarding to StartIo.

VOID DpcForIsr(PKDPC Dpc PDEVICE_OBJECT fdo, PIRP junk, PDEVICE_EXTENSION pdx) {  

StartNextPacket(&pdx->dqSomething, fdo);

IoCompleteRequest(Irp, boost); }

  1. StartNextPacket removes the next IRP from your queue and sends it to StartIo.

  2. IoCompleteRequest completes the IRP you specify as the first argument. The second argument specifies a priority boost for the thread that has been waiting for this IRP. You ll also fill in the IoStatus block within the IRP before calling IoCompleteRequest, as I explained earlier, in the section Completing an IRP.

I m not (yet) showing you how to determine which IRP has just completed. You might notice that the third argument to the DPC is typed as a pointer to an IRP. This is because, once upon a time, people often specified an IRP address as one of the context parameters to IoRequestDpc, and that value showed up here. Trying to communicate an IRP pointer from the function that queues a DPC is unwise, though, because it s possible for there to be just one call to the DPC routine for any number of requests to queue that DPC. Accordingly, the DPC routine should develop the current IRP pointer based on whatever scheme you happen to be using for IRP queuing.

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 whichever entity 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.



Programming the Microsoft Windows Driver Model
Programming the Microsoft Windows Driver Model
ISBN: 0735618038
EAN: 2147483647
Year: 2003
Pages: 119
Authors: Walter Oney

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net