每个IRP都渴望被完成。在标准模型中,你至少有两种完成IRP的环境。DpcForIsr通常用于完成导致最近中断的IRP。派遣函数也可以在下面这两种情况下完成IRP:
如果请求是错误的(可以以容易的检测方式查明,例如要求打印机倒纸请求或卸载键盘请求),则派遣例程应以失败方式完成该请求并返回适当的出错代码。
如果请求要求得到的仅是派遣函数可以容易确定的信息(例如一个询问驱动程序版本号的控制请求),则派遣例程应立即给出回答并完成请求,返回成功状态码。
完成机制
完成一个IRP必须先填充IoStatus块的Status和Information成员,然后调用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没有足够的系统资源(通常是内存)来执行该操作
注意在设置IRP的IoStatus.Information域时,我们应该首先参考DDK文档中的正确设置。例如,在某些IRP_MJ_PNP中,这个域用于指向一个数据结构,而该数据结构由PnP管理器负责释放。如果你在处理这些IRP_MJ_PNP请求失败后把Information域置成0,将造成资源丢失。
通常你常做的工作就是完成某个请求,所以我建议你编制一个辅助函数:
view plaincopy to clipboardprint?NTSTATUS CompleteRequest(PIRP Irp, NTSTATUS status, ULONG_PTR Information)
{
Irp->IoStatus.Status = status;
Irp->IoStatus.Information = Information;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return status;
}
该函数将返回其第二个参数给出的状态值。该函数适用于需要完成一个请求并立即返回状态码的场合。例如:
view plaincopy to clipboardprint?NTSTATUS DispatchControl(PDEVICE_OBJECT device, 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);
...
}
你也许注意到了,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级上。
注意完成例程使用的设备对象指针参数就是I/O堆栈单元中DeviceObject域中的指针。通常IoCallDriver设置该值。有时,在创建IRP时还同时创建一个额外的堆栈单元,以便能向完成例程传递参数而不用创建一个额外的上下文结构。如果此时创建者不去设置这个额外的DeviceObject域,那么完成例程得到的设备对象指针将为NULL。
完成例程如何获得调用
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 yourselfstatus = 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指针无效。