Windows NT如何处理I/O完成

来源:互联网 发布:商场客户流失数据 编辑:程序博客网 时间:2024/06/04 19:14
以前我们描述了在内核中如何“创建你的IRPs”(roll your own IRPs)来执行I/O操作。这是一个很强大的技术,对于那些编写设备驱动程序,文件系统驱动程序和过滤器驱动程序的人是非常有用的。这次我们讨论I/O管理器如何完成IRPs。
虽然看起来是个简单的话题,但是它包含了许多细微的主题。如果你没有完全理解I/O完成,你的驱动程序会面临危险。例如,回顾在编写驱动程序代码时我们经常遇到的一个问题是设置和清除I/O完成例程。理解I/O管理器如何执行I/O完成处理会使你明白为什么I/O完成例程必须设置成这个样子。
在这篇文章中我们描述与I/O完成处理相关的I/O栈,当调用低级驱动程序时如何设置I/O栈,I/O管理器如何为完成处理解开I/O栈,以及应该如何设置你的I/O完成例程使其能够适当地工作。
 
I/O栈
图一是一个有三个栈单元的I/O栈,同时显示了每个栈单元(I/O Stack Location,有的驱动程序资料上翻译为栈位置)内的I/O完成例程的内容。

栈单元是从上到下使用的(例如,I/O栈在内存中是向下增长的)。你可以在I/O管理器的实现函数IoSetNextIrpStackLocation(在ntddk.h中的一个宏)中看到这一点:
#define IoSetNextIrpStackLocation( Irp ) { /
   (Irp)->CurrentLocation--; /
   (Irp)->Tail.Overlay.CurrentStackLocation--; }
当IRP首次被创建时,当前IRP栈单元是无效的,调用IoGetCurrentIrpStackLocation()函数返回一个指向IRP结构内部的指针。但是,通过使用IoGetNextIrpStackLocation()函数获取的下一个IRP栈单元则是有效的,事实上第二个IRP栈单元在为第一个被调用的驱动程序设置参数时用到(设置第一个I/O栈单元的机制在"Building IRPs to Perform File I/O" , The NT Insider V4N1中已经讨论过了)

为I/O完成设置I/O栈
当处理一个IRP时要做的一件事是建立完成例程。完成例程是一段简单的程序,只要由IRP描述的I/O完成了,那么下一个低级驱动程序就会调用这个完成例程。一个完成例程可以在以下I/O请求时指派:成功,错误,取消,或者它们的组合。特定IRP的完成例程由IoSetCompletionRoutine()函数建立。如果你的驱动程序不希望在I/O完成上收到通知,那么在将IRP传递给下一个低级驱动程序之前可以指明。可以使用下面的代码:
IoSetCompletionRoutine(Irp, NULL, NULL, FALSE, FALSE, FALSE);
IoSetCompletionRoutine() 是一个宏,它在ntddk.h中定义:
#define IoSetCompletionRoutine( Irp, Routine, CompletionContext, Success, Error, Cancel ) {
         PIO_STACK_LOCATION irpSp;
       ASSERT( (Success) | (Error) | (Cancel) ? (Routine) != NULL : TRUE );
       irpSp = IoGetNextIrpStackLocation( (Irp) );
       irpSp->CompletionRoutine = (Routine);
       irpSp->Context = (CompletionContext);
       irpSp->Control = 0;
       if ((Success)) { irpSp->Control = SL_INVOKE_ON_SUCCESS; }
       if ((Error)) { irpSp->Control |= SL_INVOKE_ON_ERROR; }
       if ((Cancel)) { irpSp->Control |= SL_INVOKE_ON_CANCEL; } }
Figure 2
一个常见的使用完成例程的地方是驱动程序创建了自己的IRP池。这时完成例程陷入这个IRP并把它返回到驱动程序的私有IRP池,然后给I/O管理器返回一个STATUS_MORE_PROCESSING_REQUIRED。这使得I/O管理器立即中止处理该IRP的完成并把它留给驱动程序处理。
图一中关于I/O完成例程需要注意的是驱动程序的完成例程并不在它的I/O栈单元内,而是在下一个驱动程序的I/O栈单元内。为什么要这样设置?其中一个原因是,最后一个驱动程序并不需要I/O完成例程。毕竟,它是最后执行I/O完成的驱动程序(通过调用IoCompleteRequest()函数)。因此不需要I/O管理器通知最底层驱动程序I/O已经完成了。
另一个原因是IRP的初始化。为IRP分配初始内存的内核组件,例如驱动程序,不需要I/O栈单元。但是,它可能需要I/O完成例程(例如,以便IRP返回到它的私有池中)。回想一下,在调用链中最底层的驱动程序不需要I/O完成例程但是需要I/O栈单元。这样,为了空间效率,第N-1驱动程序的I/O完成例程存储在第N个驱动程序的I/O栈单元中。
因为I/O管理器自身不使用I/O完成例程,所以,如果你正在开发最高层驱动程序的话,决不会在你的I/O栈单元中看到完成例程。但是有些系统组件创建自己的IRPs并把它们传递给最高层驱动程序来来指示出它们有自己的完成例程。例如SRV(局域网文件管理服务器)和NTVDM(MS-DOS模拟层)它们都建立自己的IRPs并把它们传递给最高层驱动程序。这些组件也把自己的完成例程设置到它们所创建的IRPs中。
对I/O栈中完成例程的一个常见误解出现在当你的驱动程序把IRP传递给接下来的驱动程序时。图三解释了这种错误,它简单地把当前I/O栈单元复制到下一个I/O栈单元中。问题是复制当前I/O栈单元到下一个I/O栈单元的同时也复制了I/O完成例程。只要当前I/O栈单元内没有完成例程的话,就可以不出问题。如果提供了完成例程,就会出现两个不同的I/O栈单元,很明显完成例程会被调用两次。这通常导致蓝屏或其他使系统不稳定的灾难性结果。当然可以编写代码来处理这种情况,使其能够正常工作。但是很少有驱动程序编写者能够预见到完成例程会被正确的调用。
PIO_STACK_LOCATION IrpSp;
PIO_STACK_LOCATION NextIrpSp;
IrpSp = IoGetCurrentIrpStackLocation(Irp);
NextIrpSp = IoGetNextIrpStackLocation(Irp);
*NextIrpSp = *IrpSp;
return IoCallDriver(NextDeviceObject, Irp);
Figure 3
对于文件系统过滤器驱动程序,开始过滤由SRV服务的驱动程序时经常首先出现这种问题。SRV提供自己的I/O完成例程而且它不能防止它的完成例程被其它驱动程序的I/O栈单元调用。解决这个问题的办法是,在你的驱动程序拷贝I/O栈单元之后一定要确保调用IoSetCompletionRoutine()函数。
当然,尽管你正确地编写了过滤器驱动程序,但仍然可能存在问题。你可以看到该问题的一个曾经的例子是 NT4.0的NTFS文件系统。与近期的SP2一样,NTFS文件系统在拷贝I/O栈时处理IRP_MJ_DEVICE_CONTROL过程中没有清除完成例程。这样就会出现我们的完成例程被调用两次的情况(一次是由NTFS文件系统的设备对象调用的)。
解决该问题的办法是直截了当的。在你的设备对象扩展区中你应该保留一些识别标记:一个惟一值,你可以利用它来分辨出传递给你的完成例程的是你的设备对象而不是其它驱动程序的。如果你的完成例程由其它驱动程序的设备对象调用了,你可以简单地模拟I/O管理器使其认为不存在完成例程。如图四示出的代码证明了这一点。现在,如果你的完成例程被其它驱动程序的设备对象调用了,你的代码也能正常工作。
if(!DeviceObject>Extension||(POUR_DEVICE_EXTENSION)(DeviceObject>ExtensionMagicNumber)!=OUR_EXTENSION_MAGIC_NUMBER) {
   if (Irp->PendingReturned) {
       IoMarkIrpPending(Irp);
   }
   return STATUS_SUCCESS;
}
Figure 4
有一个虽然不常见但令人有点畏惧的问题是我们最近在NT 4.0 SP2的以前版本中,使用IRP_MJ_CLOSE的CDFS没有适当地完成中发现的。由于I/O管理器的处理机制的原因,似乎可以正常工作,即使是Checked Build版本(这就是为什么在短短的时间内微软对其做了更多的修复的原因)。
但是,对过滤器驱动程序,此问题所表现出的症状是过滤驱动程序的完成例程没有被调用。虽然在SP2中修复了该问题,但对于那些必须使他们的产品支持NT4.0的早期版本或者希望在将来再出现此类问题时能够正确地解决它的人来说,需要依靠I/O完成处理来解决。图五示出的代码是侦测和对付这种问题的办法,它的主干代码会与随后图六所示的设置事件同步对象的完成例程相结合。
RtlCopyMemory(NextIrpStack, CurrentIrpStack, sizeof(IO_STACK_LOCATION));
KeInitializeEvent(&Event, NotificationEvent, FALSE);
IoSetCompletionRoutine(Irp,&OurCloseCompletion,&Event,TRUE,TRUE,TRUE);
IoMarkIrpPending(Irp);
status = IoCallDriver(Extension->FilteredDeviceObject, Irp);
if (status != STATUS_PENDING) {
           if (KeReadStateEvent(&Event) == 0) {
                       IoCompleteRequest(Irp, IO_NO_INCREMENT);
           }
} else {
       KeWaitForSingleObject(&Event, Executive, KernelMode, FALSE, 0);
}
return STATUS_PENDING;
Figure 5
OurCloseCompletion(PDEVICE_OBJECT Device Object, PIRP Irp, PVOID Context)
{
Event = (PKEVENT)Context;
KeSetEvent(Event, 0, FALSE);
return STATUS_MORE_PROCESSING_REQUIRED;
}
Figure 6
问题是CDFS返回了STATUS_SUCCESS而没有调用IoCompleteRequest()函数。其实,这证明了对于大部分的I/O操作如果在它的分发例程中返回了除STATUS_PENDING之外的任何值,被分发的IRP就会由I/O管理器完成。当然,你决不会自己写代码做这样的事情。没有调用IoCompleteRequest()函数就完成了的IRP在驱动程序中是一种逻辑错误,它会给更高层的驱动程序带来一些令人讨厌的问题。
在除了返回STATUS_PENDING之外的所有情况中已经完成的IRPs会被传送回来,因为底层驱动程序已经调用了IoCompleteRequest()函数,它依次调用了你的驱动程序中的完成例程。这引领我们进入I/O管理器最复杂的部分——理解I/O管理器如何处理I/O完成。
 
I/O管理器的角色
I/O管理器处理I/O完成分为两个不同的阶段。第一个阶段是独立于线程上下文的,这意味着在该阶段的处理过程中I/O管理器可以在任何线程上下文中执行。第二个阶段是依赖线程上下文的,这意味着在该阶段的处理过程中I/O管理器必须在特定线程上下文中执行,在这里这个特定线程是最初提出I/O请求的线程。
在处理I/O完成的第一个阶段中,IRP被解开并通知每个驱动程序的完成例程I/O操作已经完成。在该阶段处理期间,任何提供了完成例程的驱动程序都会被I/O管理器回调,并使这些驱动程序获得察看IRP的第二次机会。
在第二阶段处理中,把从IRP中获得的信息拷贝到线程的用户空间内存缓冲区中。这样,必须保证该操作在正确的进程上下文中完成。当然,尽管大多数数据拷贝调用操作不是必须的,但是象直接I/O这样的情况仍然需要拷贝数据。一旦完成拷贝操作,该IRP就会被拆除和丢弃,这包括与该IRP相关的MDL以及最终存储IRP自身数据的内存也会被回收。
当驱动程序调用IoCompleteRequest()函数时,I/O完成处理的第一个阶段便完成了。注意,IoCompleteRequest()函数是个void型函数,所以调用者不需要为I/O完成提供任何额外的信息。事实上,调用者甚至不知道第一阶段的I/O完成操作是否已经完成了,因为高层驱动程序可能返回
STATUS_MORE_PROCESSING_REQUIRED,它暂停了I/O完成第一阶段的进一步处理。
I/O完成的第二阶段可以在调用IoCompleteRequest()之后的任意时间点上发生。这可能意味着I/O完成的第二阶段在IoCompleteRequest()返回之时完成,或者甚至在它返回之后过一段时间才完成,这取决于当前系统的负载。
实际的顺序取决于最后一个完成例程被调用之后IRP的状态。这时,I/O管理器通过查看IRP来检查最高层驱动程序是否已经返回了STATUS_PENDING(或者将要返回STATUS_PENDING,因为此时我们不知道确切的操作顺序),然后I/O管理器通过排队一个异步过程调用(APC)来执行I/O完成的第二阶段。I/O管理器是通过查看IRP的PendingReturned字段来执行该检查的。
这里有一个有意思的地方。如果最高层驱动程序没有返回 STATUS_PENDING,那么I/O管理器只是简单地把控制权转交给调用者(例如,返回到调用IoCompleteRequest()函数的驱动程序)。回想一下该阶段的处理过程,就会发现调用IoCompleteRequest()函数的驱动程序对IRP的状态信息没有任何意识。
那么在这种情况下I/O完成的第二阶段是在何处发生的呢?在图七中,我们给出了在这种情况下(同步I/O情况)控制转移流程图。

在对I/O管理器作进一步讨论之前,最后复习一下以下这些步骤:
1.应用程序向I/O管理器发出I/O操作请求(读、写等操作)。
2.I/O管理器通过建立一个IRP来分发I/O请求并把它传递给调用链中第一个驱动程序。
3.第一个驱动程序通过向第二个驱动程序发出I/O请求以便继续处理。
4.第二个驱动程序通过向第三个驱动程序发出I/O请求以便继续处理。
5.第三个驱动程序完成I/O操作。它调用IoCompleteRequest()函数并开始执行I/O完成的第一阶段。
6.在第一阶段执行期间,第二个驱动程序的完成例程(如果有的话)会被回调。
7.在第一阶段执行期间,第一个驱动程序的完成例程(如果有的话)会被回调。
8.接下来,IoCompleteRequest()函数会把控制权返回给第三个驱动程序。I/O完成的第一阶段处理结束。
9.第三个驱动程序把一个状态值(例如,SUCCESS)返回给第二个驱动程序。
10.第二个驱动程序把一个状态值返回给第一个驱动程序。
11.第一个驱动程序把一个状态值返回给I/O管理器。
12.I/O管理器注意到必须执行I/O完成的第二阶段,并开始执行。
13.第二阶段I/O完成例程返回I/O操作的状态信息并把数据拷贝到应用程序的地址空间中。一旦完成了这些操作,I/O管理器拆除IRP并将其丢弃。
14.第二阶段I/O完成例程把控制权返回给I/O管理器。
15.现在,I/O操作已经完成,I/O管理器把控制权返回给调用者。
该图中一个有意思的地方是第12步,I/O管理器开始执行I/O完成的第二阶段。这可以正确的执行,因为调用是被同步地完成的,而且I/O管理器“知道”是在正确的线程上下文中执行以及I/O完成的第一阶段处理已经结束了。
回想一下我们刚才讨论过的NT4.0中的CDFS的问题,这种特殊情况是CDFS调用IoCompleteRequest()失败但它的分发函数却返回了STATUS_SUCCESS。它可以正确执行的原因是I/O管理器执行了上面所讨论的两阶段I/O完成处理,这可以确保信息传回给应用程序同时I/O管理器解除了IRP。当然,任何中间层驱动程序决不会知道I/O完成了,也不会执行第一阶段的处理操作(图中步骤5到8)。
对于异步I/O情况,这些步骤的发生顺序很不一样。如图八所示。

下面是每个步骤地描述:
1.应用程序向I/O管理器发出I/O操作请求(读、写等操作)。
2.I/O管理器通过建立一个IRP来分发I/O请求并把它传递给调用链中第一个驱动程序。
3.第一个驱动程序通过向第二个驱动程序发出I/O请求以便继续处理。
4.第二个驱动程序通过向第三个驱动程序发出I/O请求以便继续处理。
5.第三个驱动程序完成I/O操作。它调用IoCompleteRequest()函数并开始执行I/O完成的第一阶段。
6.在第一阶段执行期间,第二个驱动程序的完成例程(如果有的话)会被回调。
7.在第一阶段执行期间,第一个驱动程序的完成例程(如果有的话)会被回调。
8.接下来,IoCompleteRequest()函数排队一个APC来执行I/O完成的第二阶段。第一阶段结束。
9.I/O完成APC被交付,I/O完成第二阶段开始。注意,这是在引起I/O操作的线程上下文中完成的。
10.拷贝I/O状态和数据到用户地址空间。
11.接着,IoCompleteRequest()函数返回到第三个驱动程序。
12.第三个驱动程序把一个状态值(例如,SUCCESS 或 PENDING)返回各第二个驱动程序。
13.第二个驱动程序把一个状态值返回给第一个驱动程序。
14.第一个驱动程序把一个状态值返回给I/O管理器。
15.现在,I/O操作已经完成,I/O管理器把控制权返回给调用者。
对于这种情况(异步I/O),I/O完成处理的第二阶段(步骤9到10)相对于控制权从IoCompleteRequest()返回到应用程序(步骤11到14)所执行的操作,可以以任意顺序执行。
实际返回到应用程序(步骤15)的操作可能被I/O管理器排序了,例如这种情况,即应用程序请求同步I/O。当然,I/O管理器是以跟Windows NT内核模式代码同样的方式执行该操作的——通过等待一个事件对象(这里,该事件对象是针对IRP的)。当第二阶段完成时,该事件对象被设置成有信号状态,I/O操作的状态就会被返回给应用程序(步骤15)。
也要注意驱动程序到驱动程序的调用,以及接下来的返回步骤,可能是异步执行的。这时,如果调用链中第一个驱动程序希望将I/O请求排队并异步地处理它,那么它就会给I/O管理器返回一个STATUS_PENDING(步骤14)。
如果调用者不要求同步,那么在步骤15中I/O管理器就会把控制权返还给调用者,表示I/O正在处理。在I/O完成处理的第二阶段期间,完成I/O后操作的状态值被拷贝回适当的地址空间中。接着应用程序通过使用一个现有机制,例如,APC例程,在特定事件上等待,或者使用轮询来检查I/O是否完成了。在这期间,最初引起I/O请求的线程可以继续执行,甚至开始执行另外的I/O操作。

设置你的完成例程
因为在特定类型的驱动程序中使用I/O完成例程是非常常见的,理解Window NT如何处理I/O完成的方式可以帮助开发者编写更健壮的驱动程序,以及定位驱动程序的错误。一个常见的例子是我们的代码无数次地在一个文件系统中使用 IRP_MJ_CREATE调用。一个过滤器驱动程序的新手写出如图九所示的代码,想要在他们的完成例程中做像图十所示的事情:
DbgPrint("Createfor%Z/n",&IrpSp>FileObject>FileName;
return IoCallDriver(NextDeviceObject, Irp);
Figure 9
// Build a work item for additional processing
ExQueueWorkItem(&WorkItem);
return STATUS_MORE_PROCESSING_REQUIRED;
Figure 10
通过阅读DDK文档可以使你满怀信心地认为它会正常地工作。事实证明并不是这样的。I/O管理器把 IRP_MJ_CREATE看作的同步I/O操作(回想一下图七中控制流程图)。事实上你的完成例程返回的是STATUS_MORE_PROCESSING_REQUIRED(步骤7)而没有得到返回到最底层驱动程序的通知
(IoCompleteRequest()是一个void型函数,所以它不给调用者返回任何东西)。但是,它把创建操作的结果依次返回给I/O管理器的调用驱动程序。I/O管理器推测出由于这是同步I/O的情况而且返回值不是STATUS_PENDING,必须执行I/O完成的第二阶段。
这有个例外——如果你的驱动程序的工作例程已经完成了,一切都会顺利进行,因为在你的工作例程中调用了IoCompleteRequest()函数。如果你的驱动程序的工作例程正在处理中你却调用了IoCompleteRequest(),那么工作例程的处理就不会完成,这将导致系统崩溃并使用MULTIPLE_IRP_COMPLETIONS标志蓝屏死机。
所以从完成例程中返回STATUS_MORE_ PROCESSING_REQUIRED是不够的。此外在分发例程的入口点中还必须做以下两件事中的一个:
A.或者返回STATUS_PENDING或者
B.使用同步I/O请求
这样做可以足够保证你的完成例程安全地返回STATUS_MORE_PROCESSING_REQUIRED。如果驱动程序返回了STATUS_PENDING(情况A)那么代码应该类似于图十一:
DbgPrint("Createfor%Z/n",&IrpSp->FileObject->FileName);
IoMarkIrpPending(Irp);
(void) IoCallDriver(NextDeviceObject, Irp);
return STATUS_PENDING;
Figure 11

回想一下上面我们提到的,如果最高层驱动程序返回了STATUS_PENDING,那么I/O管理器排队一个APC来执行I/O完成的第二阶段。但是,当I/O管理器检查STATUS_PENDING是否已经返回了以及STATUS_PENDING实际上是否真的从驱动程序返回了,在这两者之间并没有固定的顺序。事实上
,我们所知道的是STATUS_PENDING在某个时间点上(过去,或者将来)从最高层驱动程序返回。
这个信息(最高层驱动程序会返回STATUS_PENDING)存储在该驱动程序的I/O栈单元内。它由 SL_PENDING_RETURNED位表示,存储在控制字段中。在你的驱动程序中通过调用IoMarkIrpPending()函数设置该位。I/O管理器轮流处理每个栈单元时,它通过设置Irp->PendingReturned字段来指示出SL_PENDING_RETURNED位是否已在下一个低层驱动程序的控制字段中设置了。调用最后一个驱动程序的完成例程后,I/O管理器使用在
Irp->PendingReturned字段中的信息来决定是否需要为I/O完成的第二阶段排队一个APC,这就是为什么你的驱动程序必须同时调用IoMarkIrpPending()并返回STATUS_PENDING的原因。如果你的驱动程序只做了其中的一件事,那么系统就会出现行为异常。
如果驱动程序使用同步I/O方式(情况B),那么就像图十二所示的代码一样。驱动程序的完成例程或者被完成例程排队的工作例程设置一个事件(通常它作为一个上下文信息传递给完成例程)。
DbgPrint("Create for %Z/n", &IrpSp->FileObject->FileName);
Status = IoCallDriver(NextDeviceObject, Irp);
KeWaitForSingleObject(&Event, Executive, KernelMode, FALSE, 0);
Figure 12
当做同步I/O(情况B)的时,这是最简单的选择,它不能用于所有的情况,因为它干扰了异步I/O请求操作,会导致一系列的异步I/O操作发生。这些异步I/O操作的其中之一,将引起试图做同步I/O操作的暂停。
同样,像情况A那样做异步I/O会引起某个异步I/O不正确,因为它们把返回值STATUS_PENDING当作一个成功标志了。一个例子是文件系统中的“oplocks”。这个操作锁把STATUS_PENDING当作对该锁的一个授权。不幸地是,如果驱动程序(例如文件系统过滤器驱动程序)返回了STATUS_PENDING,这将引起高速缓存的一致性问题以及网络客户端的数据过期。
我们以前提到一个注意事项,而这里的第二个注意事项是,如果你的驱动程序返回了STATUS_PENDING就必须也调用IoMarkIrpPending()。因此,考虑一个驱动程序如果这样做:
return IoCallDriver(NextDeviceObject, Irp);
那么,如果下面的驱动程序返回了STATUS_PENDING,这个驱动程序就必须调用IoMarkIrpPending()函数。接着许多程序员这样写:
Status = IoCallDriver(NextDeviceObject, Irp);
if (Status == STATUS_PENDING) IoMarkIrpPending(Irp);
return Sta
这可以正常执行——大多数情况下。但有时这会引起系统崩溃、线程挂起或者其他令人讨厌的行为。记住,一旦你把IRP传递给下一个驱动程序(通过IoCallDriver()函数)该IRP就像消失了一样。因为IoCallDriver()返回到驱动程序后并不知道I/O请求的状态。但是,你能做的是获得第二次察看IRP的机会——在你的完成例程中。
因此,你所能做的,而不像上面代码那样,就是修改你的完成例程,就像这样:
if (Irp->PendingReturned) IoMarkIrpPending(Irp);
这表示如果你的驱动程序没有完成例程,那么I/O管理器会为你完成。但是,一旦提供了自己的完成例程I/O管理器不会接手干预,这样你可以做任何想要做的——包括做错!
总结
现在,你可以问问自己,提供自己的I/O完成例程却带来这么多的麻烦是否值得。答案是肯定的。通过使用Windows NT为同步和异步I/O提供丰富的操作方式,你的驱动程序可以监视和处理与I/O相关的各种事件。例如,能够处理错误情况对于建立软件RAID解决方案(例如,FTDISK)是非常重要的。这样,驱动程序可以显示地从I/O错误中恢复。对于过滤器驱动程序,使用完成例程可以使正在处理的完成状态与最初提出I/O请时的相匹配。
为了总结本文所讨论的主题,我写了一个“规则”清单,在你把IRP传递给另一个驱动程序时都可以遵照它来执行:
1.总是设置你自己的I/O完成例程。如果你不想得到I/O完成的通知,至少你应该使用 IoSetCompletionRoutine(Irp, NULL, NULL, FALSE, FALSE, FALSE);把它清除掉。
2.如果你提供了I/O完成例程,你的责任就是给调用链中的驱动程序传递回一个IRP的未决状态。这意味着如果你的驱动程序返回了STATUS_PENDING,那么你必须也得把IRP标示为未决的。
3.你的完成例程可以在IRQL <= DISPATCH_LEVEL时被调用。这时你必须或者执行可在DISPATCH_LEVEL下执行的操作,或者使用一个工作线程(运行在PASSIVE_LEVEL)。
4.如果你编写的是最底层驱动程序,通过调用IoCompleteRequest()函数来完成适当的请求。否则,你会引起高层驱动程序的问题。

转自http://gameboytxt.spaces.live.com/Blog/cns!C792824C7D49C4E4!1505.entry

原创粉丝点击