分享

完成I/O请求

 tuohuang0303 2011-04-28

每个IRP都渴望被完成。在标准模型中,你至少有两种完成IRP的环境。DpcForIsr通常用于完成导致最近中断的IRP。派遣函数也可以在下面这两种情况下完成IRP:

  • 如果请求是错误的(可以以容易的检测方式查明,例如要求打印机倒纸请求或卸载键盘请求),则派遣例程应以失败方式完成该请求并返回适当的出错代码。   
  • 如果请求要求得到的仅是派遣函数可以容易确定的信息(例如一个询问驱动程序版本号的控制请求),则派遣例程应立即给出回答并完成请求,返回成功状态码。

完成机制

完成一个IRP必须先填充IoStatus块的StatusInformation成员,然后调用IoCompleteRequest例程。Status值就是NTSTATUS.H中定义的状态代码。表5-1简要地列出了常用的状态代码。而Information值要取决于你完成的是何种类型的IRP以及是成功还是失败。通常情况下,如果IRP完成失败(即,完成的结果是某种错误状态),你应把Information域置0。如果你成功地完成了一个数据传输IRP,通常应该把Information域设置成传输的字节量。

一些常用的NTSTATUS代码

状态代码描述
STATUS_SUCCESS正常完成
STATUS_UNSUCCESSFUL请求失败,没有描述失败原因的代码
STATUS_NOT_IMPLEMENTED一个没有实现的功能
STATUS_INVALID_HANDLE提供给该操作的句柄无效
STATUS_INVALID_PARAMETER参数错误
STATUS_INVALID_DEVICE_REQUEST该请求对这个设备无效
STATUS_END_OF_FILE到达文件尾
STATUS_DELETE_PENDING设备正处于被从系统中删除过程中
STATUS_INSUFFICIENT_RESOURCES没有足够的系统资源(通常是内存)来执行该操作

通常你常做的工作就是完成某个请求,所以我建议你编制一个辅助函数:

  1. NTSTATUS CompleteRequest(PIRP Irp, NTSTATUS status, ULONG_PTR Information)  
  2. {  
  3.   Irp->IoStatus.Status = status;  
  4.   Irp->IoStatus.Information = Information;  
  5.   IoCompleteRequest(Irp, IO_NO_INCREMENT);  
  6.   return status;  
  7. }  

该函数将返回其第二个参数给出的状态值。该函数适用于需要完成一个请求并立即返回状态码的场合。例如:

  1. NTSTATUS DispatchControl(PDEVICE_OBJECT device, PIRP Irp)  
  2. {  
  3.   PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);  
  4.   ULONG code = stack->Parameters.DeviceIoControl.IoControlCode;  
  5.   if (code == IOCTL_TOASTER_BOGUS)  
  6.     return CompleteRequest(Irp, STATUS_INVALID_DEVICE_REQUEST, 0);  
  7.   ...  
  8. }  

你也许注意到了,CompleteRequest函数的Information参数类型为ULONG_PTR。即该参数既可以是一个ULONG也可以是一个指针。

当你调用IoCompleteRequest时,应该为等待线程提供一个优先级推进值,该值将用于提高等待该请求完成的线程的优先级。一般说来,你需要根据设备类型来选择这个推进值,表5-2列出了一些设备的建议值。优先级的调整提高了那些需要频繁等待I/O操作完成的线程的吞吐量。对于那些直接响应用户的事件,如键盘或鼠标操作,应该有一个比较大的优先级推进,以提高交互任务的表现。因此,你要仔细地选择推进值。不要绝对地在声卡驱动程序完成的每个操作后都使用IO_SOUND_INCREMENT值。例如,没有必要在获取驱动程序版本控制请求后提高线程的优先级。

表5-2. IoCompleteRequest的优先级推进值

推进值常量优先级推进值
IO_NO_INCREMENT0
IO_CD_ROM_INCREMENT1
IO_DISK_INCREMENT1
IO_KEYBOARD_INCREMENT6
IO_MAILSLOT_INCREMENT2
IO_MOUSE_INCREMENT6
IO_NAMED_PIPE_INCREMENT2
IO_NETWORK_INCREMENT2
IO_PARALLEL_INCREMENT1
IO_SERIAL_INCREMENT2
IO_SOUND_INCREMENT8
IO_VIDEO_INCREMENT1

顺便说一下,不要以专用状态代码STATUS_PENDING来完成一个IRP。派遣例程经常要使用STATUS_PENDING代码作为返回值,但你决不能在IoStatus.Status中设置这个值。所以,在checked版本的IoCompleteRequest函数中有一个ASSERT语句用于检查该函数的最终返回值是否为STATUS_PENDING。另一个常犯的错误是在返回值中使用“-1”,该值作为NTSTATUS代码没有任何意义,所以IoCompleteRequest函数中也有检查这种错误的ASSERT语句。

使用完成例程

通常,你需要知道发往低级驱动程序的I/O请求的结果。为了了解请求的结果,你需要安装一个完成例程,调用IoSetCompletionRoutine函数:

IoSetCompletionRoutine(Irp,
		       CompletionRoutine,
		       context,
		       InvokeOnSuccess,
		       InvokeOnError,
		       InvokeOnCancel);

Irp就是你要了解其完成的请求。CompletionRoutine是被调用的完成例程的地址,context是任何一个指针长度的值,将作为完成例程的参数。InvokeOnXxx参数是布尔值,它们指出在三种不同的环境中是否需要调用完成例程:

  • InvokeOnSuccess 你希望完成例程在IRP以成功状态(返回的状态代码通过了NT_SUCCESS测试)完成时被调用。
  • InvokeOnError 你希望完成例程在IRP以失败状态(返回的状态代码未通过了NT_SUCCESS测试)完成时被调用。
  • InvokeOnCancel 如果驱动程序在完成IRP前调用了IoCancelIrp例程,你希望在此时调用完成例程。IoCancelIrp将在IRP中设置取消标志,该标志也是调用完成例程的条件。一个被取消的IRP最终将以STATUS_CANCELLED(该状态代码不能通过NT_SUCCESS测试)或任何其它状态完成。如果IRP以失败方式完成,并且你也指定了InvokeOnError参数,那么是InvokeOnError本身导致了完成例程的调用。相反,如果IRP以成功方式完成,并且你也指定了InvokeOnSuccess参数,那么是InvokeOnSuccess本身导致了完成例程的调用。在这两种情况中,InvokeOnCancel参数将是多余的。如果你省去InvokeOnSuccess和InvokeOnError中的任何一个参数或两个都省去,并且IRP也被设置了取消标志,那么InvokeOnCancel参数将导致完成例程的调用。

这三个标志中至少有一个设置为TRUE。注意,IoSetCompletionRoutine是一个宏,所以你应避免使用有副作用的参数。这三个标志参数和一个函数指针参数在宏中被引用了两次。

IoSetCompletionRoutine将把完成例程地址和上下文参数安装到下一个IO_STACK_LOCATION中,即下一层驱动程序将在那个堆栈单元中找到这些参数。因此,最底层的驱动程序不应该安装一个完成例程。

一个完成例程看起来应该像这样:

NTSTATUS CompletionRoutine(PDEVICE_OBJECT device, PIRP Irp, PVOID context)
{
  if (Irp->PendingReturned)
    IoMarkIrpPending(Irp);
  ...
  return <some status code>;
}

该函数将收到一个设备对象指针和一个IRP指针,还收到一个任意上下文值,该值在IoSetCompletionRoutine调用中指出。完成例程通常在DISPATCH_LEVEL级和任意线程上下文中被调用,但有时也在PASSIVE_LEVEL或APC_LEVEL级被调用。为了适应大多数情况(DISPATCH_LEVEL),完成例程应存在于非分页内存中,并且仅使用可在DISPATCH_LEVEL级上调用的服务例程。然而,为了适应在低级IRQL上调用该例程的可能情况,完成例程不应调用像KeAcquireSpinLockAtDpcLevel这样的函数,因为这些函数假定开始执行于DISPATCH_LEVEL级上。

完成例程如何获得调用

IoCompleteRequest函数负责调用每个驱动程序安装在各自堆栈单元中的完成例程。这个调用过程见流程图5-7,开始,底层驱动程序的某段代码调用IoCompleteRequest例程以通知IRP处理结束。然后,IoCompleteRequest参考当前的堆栈单元以查明其上层驱动程序是否安装了完成例程。如果没有,它就把堆栈指针前进到上一层堆栈单元并重复测试,直到找到某个完成例程或者到达堆栈顶部。最后IoCompleteRequest函数执行其它操作(如释放IRP占用的内存)。

当IoCompleteRequest函数发现含有完成例程指针的堆栈单元时,它就调用这个完成例程并检查其返回代码。如果返回代码是除了STATUS_MORE_PROCESSING_REQUIRED以外的其它值,它就把堆栈指针移动到上一层并重复前面的工作。如果返回代码是STATUS_MORE_PROCESSING_REQUIRED,IoCompleteRequest将停止前进并返回到调用者,而此时的IRP将处于一个中间状态。因此,如果完成例程在堆栈单元回卷过程中停止,那么其驱动程序有责任处理这个处于中间状态的IRP。

在完成例程内部,一个IoGetCurrentIrpStackLocation调用将获得上一层堆栈单元的指针。上层堆栈单元的完成例程不应该依赖任何下层堆栈单元中的内容。为了加强这个规则,IoCompleteRequest在调用完成例程前清除了下一个堆栈单元中的大部分内容。

图  IoCompleteRequest函数的执行过程

完成例程为什么要调用IoMarkIrpPending

你可能已经注意到了上面完成例程框架代码的前两行:

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

所有不返回STATUS_MORE_PROCESSING_REQUIRED状态的完成例程都需要这两行代码。如果你想知道为什么,读下面这些段。然而,你应该明白编写驱动程序不应该依靠有关I/O管理器是如何处理未决IRP的信息,这个处理过程在未来的Windows版本中可能会改变。

何时调用IoMarkIrpPending

上文中陈述的规则“如果Irp->PendingReturned为TRUE,那么任何不返回STATUS_MORE_PROCESSING_REQUIRED的完成例程都应该调用IoMarkIrpPending”,这几乎完全是对的,但仍有例外。如果驱动程序分配了IRP,安装了完成例程,然后在未改变堆栈指针的情况下调用IoCallDriver,那么完成例程就不应该包含这两行代码,因为没有堆栈单元与你的驱动程序关联。(这种情况与完成例程的设备对象参数为NULL的情形类似。驱动程序通常做的是分配一个带有额外堆栈单元的IRP,在第一个单元中设置DeviceObject指针,在调用IoSetCompletionRoutine和IoCallDriver前用IoSetNextIrpStackLocation函数跳过那个额外堆栈单元。如果你这样做,那么在完成例程中调用IoMarkIrpPending将不会出现问题,并且完成例程也能得到了一个有效的设备对象)

为了使系统吞吐量最大化,I/O管理器希望驱动程序推迟其耗时IRP的完成。驱动程序通过调用IoMarkIrpPending函数并在派遣例程中返回STATUS_PENDING来表示完成操作被推迟。I/O管理器的原始调用者通常希望在继续执行之前等待操作完成,所以I/O管理器在处理推迟完成时有下面类似的逻辑(不代表真正的Microsoft源代码):

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

换句话说,如果IoCallDriver返回STATUS_PENDING,则该段代码将在一个内核事件上等待。IoCompleteRequest有责任在IRP最后完被成时设置这个事件。该事件(UserEvent)的地址在IRP的一个不透明域中,所以IoCompleteRequest能够找到它。但实际的内容比这要多。

为了使问题更简单,假设请求仅涉及一个驱动程序。该驱动程序的派遣函数仅做两件事情:调用IoMarkIrpPending,返回STATUS_PENDING,而STATUS_PENDING实际上就是IoCallDriver返回的状态代码,此外,某段代码将要在一个事件上等待。IoCompleteRequest调用发生在任意线程上下文中,因此该函数将调度一个特殊的内核APC,这个APC执行在原始线程(现在正被阻塞)的上下文中。APC例程将设置那个事件,并释放任何正等待操作完成的对象。有一些原因我们现在不需要深入,例如为什么用APC来做这个工作而不是用一个简单的KeSetEvent调用。

但是,排队一个APC是相对昂贵的。设想一下,不是直接返回STATUS_PENDING,而是派遣例程自己调用IoCompleteRequest并返回某个其它状态。在这种情况下,IoCompleteRequest的调用者将与IoCallDriver的调用者处于同一个线程上下文中。因此就没有必要排队一个APC。另外,甚至没有必要调用KeSetEvent,因为如果I/O管理器没有得到派遣例程返回的STATUS_PENDING,它就不用等待某个事件。如果IoCompleteRequest恰好知道发生的这种情况,它将优化这个处理以避免调用APC,能这样做吗?这就是IoMarkIrpPending的来处。

IoMarkIrpPending是什么,它是WDM.H中的一个宏,这你可以自己去看,它在当前的堆栈单元中设置了一个名为SL_PENDING_RETURNED的标志。IoCompleteRequest将把IRP的PendingReturned标志设置为它在顶级堆栈单元中找到的任何值。然后,它查看这个标志以确定派遣例程是否已返回或将返回STATUS_PENDING。如果你做的正确,那么派遣例程在IoCompleteRequest做这个检查之前返回或在之后返回都无关紧要。在这种情况下的“正确做法”就是指你在做任何使IRP完成的操作之前都调用IoMarkIrpPending。

所以,无论如何,IoCompleteRequest都将查看PendingReturned标志。如果该标志设置,并且如果IRP是那种可以以异步方式完成的IRP,那么IoCompleteRequest将简单地返回其调用者并不排队APC。它假定自己运行在IRP发起者的线程上下文中,并且派遣例程很快会返回一个非未决状态的代码给请求发起者。请求发起者也不用等待那个事件,因为没有代码使那个事件进入信号态。到目前为止一切顺利。

现在,让我们把其它驱动程序加入到假想图中。顶级驱动程序不了解下面发生了什么,它只简单地把请求传递到下面,就象下面代码:

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

换句话说,顶级驱动程序安装了一个完成例程并调用IoCallDriver,然后返回从IoCallDriver得到的任何值。这个过程被重复几次,经过中间的驱动程序,当IRP到达能处理它的那个驱动程序级时,派遣例程就调用IoMarkIrpPending并返回STATUS_PENDING。然后该STATUS_PENDING值按原路返回到顶级驱动程序,最后回到IRP的发起者。而发起者将立即在那个事件上等待,直到某个代码使那个事件变为信号态。

但要注意,调用IoMarkIrpPending的驱动程序仅在它自己的堆栈单元中设置了SL_PENDING_RETURNED标志。上面的驱动程序实际上仅返回STATUS_PENDING状态代码,它们没有调用IoMarkIrpPending,因为它们不知道底层驱动程序到底发生了什么。这就是完成例程中那两行代码的来处。当IoCompleteRequest沿着I/O堆栈向上走时,它在每一层都停下来并把每层中的SL_PENDING_RETURNED标志设置为PendingReturned标志。如果某一层没有完成例程它就前进到上一层。这样,SL_PENDING_RETURNED标志就被自下向上传播到堆栈的顶层,并且如果任何驱动程序曾调用过IoMarkIrpPending,则IRP的PendingReturned标志最终为TRUE。

然而,IoCompleteRequest不能自动传播SL_PENDING_RETURNED。完成例程必须自己测试IRP的PendingReturned标志并调用IoMarkIrpPending来作到这个。如果每个完成例程都做了这个工作,那么SL_PENDING_RETURNED将顺利地从下而上传播到顶层驱动程序,就象IoCompleteRequest自己做了所有工作。

现在,我已经解释完这些复杂的细节,如果派遣例程要明确返回STATUS_PENDING,那么返回前它必须调用IoMarkIrpPending,并且在某些情况下,完成例程也应该这样做。如果完成例程打破了这个链条,那么线程将在事件上空等待,而且这个事件注定永远也不会被置成信号态。如果没有发现PendingReturned标志,那么IoCompleteRequest在处理完成过程时就象在同一个上下文中,因此它也不排队使事件改变状态的APC。这与派遣例程忽略了IoMarkIrpPending调用而直接返回STATUS_PENDING的结果一样。

另一方面,调用IoMarkIrpPending然后同步完成IRP是正确的,尽管效率会低一点。这样做的结果是IoCompleteRequest将排队APC,这个APC将改变事件的状态,但没有任何线程在这个事件上等待(其目的是使在调用KeSetEvent前保证这个事件存在)。这会降低一些效率,但不会有什么害处。

另外,不要在完成例程中尝试避开IoMarkIrpPending调用,就象下面代码:

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

原因是如果你调用了IoCallDriver并给出了IRP指针,那么该函数返回后这个IRP指针可能是无效的。能完成IRP的接收者可能会调用IoFreeIrp,而该函数将使IRP指针无效。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多