이게 도대체 이해가 안되어서 자료를 많이 찾아보았는데요... 가장 좋은 설명은 저 유명한 "Programming the Microsoft® Windows® Driver Model 1st Edition" 에 나와 있는 것 같습니다. 그런데... 이 설명이 2nd Edition에서는 누락되거나 다르게 바뀌어 있다는 것이 좀 이상하네요... 어쨌든... 이 문제가 이해가 안되시는 분들은 이 책을 참고하시면 될것 같습니다.


http://www.microsoft.com/mspress/books/sampchap/2507c.aspx


Why Completion Routines Call IoMarkIrpPending

You may have noticed these two lines at the beginning of the skeleton completion routine I just showed you:

if (Irp->PendingReturned)
  IoMarkIrpPending(Irp);

This particular piece of boilerplate is required in any completion routine that doesn't return STATUS_MORE_PROCESSING_REQUIRED. If you'd like to know why, read the rest of this section. However, be aware that you should not develop drivers that rely on the information related to how the I/O Manager processes pending IRPs—that process is likely to change in future versions of Windows.


This explanation is complicated!

To maximize system throughput, the I/O Manager expects drivers to defer the completion of IRPs that take a long time to complete. A driver indicates that completion will be deferred by calling IoMarkIrpPending and returning STATUS_PENDING from the dispatch routine. Often, though, the original caller of the I/O Manager wants to wait until the operation finishes before proceeding. The I/O Manager will therefore have logic similar to this (not the actual source code of any particular Microsoft Windows NT function) to deal with the deferred completion:

Irp->UserEvent = pEvent; //  don't do this yourself
status = IoCallDriver(...);
if (status == STATUS_PENDING)
  KeWaitForSingleObject(pEvent, ...);

In other words, if IoCallDriver returns STATUS_PENDING, this code will wait on a kernel event. IoCompleteRequest is responsible for setting this event when the IRP finally completes. The address of the event (UserEvent ) is in one of the opaque fields of the IRP so that IoCompleteRequest can find it. But there's more to the story than that.

To keep things simple for the moment, suppose that there were just one driver involved in processing this request. Its dispatch function does the two things we've discussed: it calls IoMarkIrpPending, and it returns STATUS_PENDING. That status code will be the return value from IoCallDriver as well, so you can see that something is now going to wait on an event. The eventual call to IoCompleteRequest occurs in an arbitrary thread context, so IoCompleteRequest will schedule aspecial kernel APC to execute in the context of the original thread (which is currently blocked). The APC (asynchronous procedure call) routine will set the event, thereby releasing whatever is waiting for the operation to finish. There are reasons we don't need to go into right now for why an APC is used for this purpose instead of a simple call to KeSetEvent.

But queuing an APC is relatively expensive. Suppose that, instead of returning STATUS_PENDING, the dispatch routine were to call IoCompleteRequest and return some other status. In this case, the call to IoCompleteRequest is in the same thread context as the caller of IoCallDriver. It's not necessary to queue an APC, therefore. Furthermore, it's not even necessary to call KeSetEvent since the I/O Manager isn't going to be waiting on an event if it doesn't get STATUS_PENDING back from the dispatch routine. If IoCompleteRequest just had a way to know this case were occurring, it could optimize its processing to avoid the APC, couldn't it? That's where IoMarkIrpPending comes in.

What IoMarkIrpPending does—it's a macro in WDM.H, so you can see this for yourself—is set a flag named SL_PENDING_RETURNED in the current stack location. IoCompleteRequest will set the IRP's PendingReturned flag equal to whatever value it finds in the topmost stack location. Later on, it inspects this flag to see whether the dispatch routine has returned or will return STATUS_PENDING. If you do your job correctly, it won't matter whether the return from the dispatch routine happens before or after IoCompleteRequest makes this determination. "Doing your job correctly," in this particular case, means calling IoMarkIrpPending before you do anything that might result in the IRP getting completed.

So, anyway, IoCompleteRequest looks at the PendingReturned flag. If it's set, and if the IRP in question is of the kind that normally gets completed asynchronously, IoCompleteRequest simply returns to its caller without queuing the APC. It assumes that it's running in the originator's thread context and that some dispatch routine is shortly going to return a nonpending status code to the originator. The originator, in turn, avoids waiting for the event, which is just as well because no one is ever going to signal that event. So far, so good.

Now let's put some additional drivers into the picture. The top-level driver has no clue what will happen below it. It simply passes the request down using code such as the following. (See the next section, "Passing Requests Down to Lower Levels.")

IoCopyCurrentIrpStackLocationToNext(Irp);
IoSetCompletionRoutine(Irp, ...);
return IoCallDriver(...);

In other words, the top-level driver installs a completion routine, calls IoCallDriver, and then returns whatever status code IoCallDriver happens to return. This process might now repeat additional times as other drivers pass the request down to whatever is really destined to service it. When the request reaches that level, the dispatch routine calls IoMarkIrpPending and returns STATUS_PENDING. The STATUS_PENDING value then percolates all the way back up to the top and out into the originator of the IRP, which will promptly decide to wait for something to signal the event.

But notice that the driver that called IoMarkIrpPending only managed to set SL_PENDING_RETURNED in its own stack location. The drivers above it actually returned STATUS_PENDING, but they didn't call IoMarkIrpPending on their own behalf because they didn't know they'd end up returning STATUS_PENDING as proxies for the guy at the bottom of the stack. Sorting this out is where the boilerplate code in the completion routine comes in, as follows. As IoCompleteRequest walks up the I/O stack, it pauses at each level to set the IRP's PendingReturned flag to the value of the current stack's SL_PENDING_RETURNED flag. If there's no completion routine at this level, it then sets the next higher stack's SL_PENDING_RETURNED if PendingReturned is set and repeats its loop. It doesn't change SL_PENDING_RETURNED if PendingReturned is clear. In this way, SL_PENDING_RETURNED gets propagated from the bottom to the top of the stack, and the IRP's PendingReturned flag ends up TRUE if any of the drivers ever called IoMarkIrpPending.

IoCompleteRequest does not automatically propagate SL_PENDING_RETURNED across a completion routine, however. The completion routine must do this itself by testing the IRP's PendingReturned flag (that is, did the driver below me return STATUS_PENDING?) and then calling IoMarkIrpPending. If every completion routine does its job, the SL_PENDING_RETURNED flag makes its way to the top of the stack just as if IoCompleteRequest had done all of its work.

Now that I've explained these intricacies, you can see why it's important for dispatch routines to call IoMarkIrpPending if they're going to explicitly return STATUS_PENDING and why completion routines should conditionally do so. If a completion routine were to break the chain, you'd end up with a thread waiting in vain on an event that's destined never to be signalled. Failing to see PendingReturned, IoCompleteRequest would act as if it were dealing with a same-context completion and therefore would not queue the APC that's supposed to signal the event. The same thing would happen if a dispatch routine were to omit the IoMarkIrpPending call and then return STATUS_PENDING.

On the other hand, it's okay, albeit slightly inefficient, to call IoMarkIrpPending and then complete the IRP synchronously. All that will happen is that IoCompleteRequest will queue an APC to signal an event on which no one will ever wait. (Logic is in place to make sure that the event object can't cease to exist before the call to KeSetEvent, too.) This is slower than need be, but it's not harmful.

Do not, by the way, be tempted, in the hope of avoiding the boilerplate call to IoMarkIrpPending inside your completion routine, to code like this:

status = IoCallDriver(...);
if (status == STATUS_PENDING)
  IoMarkIrpPending(...);     //  DON'T DO THIS!

The reason this is a bad idea is that you must treat the IRP pointer as poison after you give it away by calling IoCallDriver. Whatever receives the IRP can complete it, allowing something to call IoFreeIrp, which will render your pointer invalid long before you regain control from IoCallDriver.



Posted by kuaaan

댓글을 달아 주세요



사랑합니다. 편안히 잠드소서