在将IRP发送给底层驱动或者其他驱动前,可以对IRP设置-一个完成例程。一旦底层
驱动将IRP完成后,IRP完成例程立刻被触发。通过设置完成例程可以方便地使程序员了
解其他驱动对IRP进行的处理。
完成例程是驱动程序编写中的一种常用技巧。 当驱动程序调用自己的底层驱动,可以通过完成例程获得通知。不管是调用自
己的底层驱动或者调用其他驱动,都是使用内核函数loCallDriver.
当loCallDriver将IRP的控制权交给被动驱动时,有两种情况。第一种情况, 即调用
的设备是同步完成这个IRP的,从loCallDriver返回的时刻,即代表此IRP已经完成。
第二种情况,就是调用的设备是异步操作,IoCallDriver 会立刻返回loCallDriver, 但此时并
没有真正的完成IRP…
在第二种情况下,调用loCallDriver前,先对IRP注册一个完成例程,当底层驱动或
者其他驱动完成此IRP时,此完成例程立刻被调用。其实注册IRP的完成例程就是在当前
的堆栈(IO_STACK_LOCATION)中的CompletionRoutine 子域。
IRP 完成后,一层层堆栈向上弹出,如果遇到IO_STACK_LOCATION CompletionRoutine非空,则调用这个函数,另外传进这个完成例程的是I0_ STACK_ LOCATION 的子域Context。
如果使用完成例程,就不能使用内核宏loSkipCurrentIrpStackLocation,即不能将本层
IRP作为下层I/O堆栈。而必须使用IoCopyCurrentIrpStackLocationToNext,将本层I/O堆
栈拷贝到下一层的I/O堆栈中。对于初级程序员来说不必关心以上细节,DDK已经提供了
内核宏,即IoSetCompletionRoutine,其声明如下:
void IoSetCompletionRoutine(
PIRP Irp, //要设置的IRP
PIO_COMPLETION_ROUTINE CompletionRoutine, //设置的IRP完成函数,如果为NULL,则代表没有IRP完成函数.
PVOID Context,//传给完成函数的参数
BOOLEAN InvokeOnSuccess, //指定是否IRP被成功完成后进入完成函数
BOOLEAN InvokeOnError, //指定是否IRP被错误完成后进入完成函数
BOOLEAN InvokeOnCancel //指定是否IRP被取消完成后进入完成函数
);
当IRP处于某个设备栈时,IRP被IoCompleteRequest完成的时候,会一层层出栈。 出
栈的时候如果遇到该栈有完成例程,则调用该栈的完成例程。因此,完成例程可以作为通
知IRP完成的一个标志。并且,在完成例程中可以很清楚地知道IRP的完成情况,例如,
IRP的status、读写的情况,操作字节的数量等。另外,在完成例程中,提供了一个机会重
新获取对IRP的“控制”。
当调用loCallDriver 之后,当前的驱动就失去了对IRP的控制,如果这时候设置IRP
的属性,会引起系统的崩溃。完成例程只有两种返回的可能,一种STATUS_SUCCESS
或STATUS_ CONTINUE COMPLETION。其实这两个状态数值一样,是等价的,STATUS_CONTINUE COMPLETION是STATUS_ SUCCESS的别名。这种情况下,驱动不会再得到
IRP的控制。另一种情况是完成例程返回STATUS_ MORE PROCESSING _REQUIRED,这时候本层设备堆栈会重新获得IRP的控制权,并且设备栈不会向上弹出,也就是向上“回
卷"设备栈停止。此时可以选择再次向底层发送IRP。
假设有这样一个设备栈,IRP传递的方向是从设备栈顶传到设备栈底,
那么在IRP处理完成后是从设备栈底开始往上返回的,处理流程如下图.
传播Pending位
每当低级驱动完成IRP后,将IRP的堆栈向上回卷时,底层I/O堆栈中Control 域的
SL_PENDING_RETURNED位必须被传播到上一-层。如果本层没有设置完成例程,那么这种传播是自动的,即不用程序员指定。如果本层堆栈设置了完成例程,则这种传播需要程序员自己实现。例如,调用IoCallDriver低层驱动时,返回的是STATUS PENDING。如果没有设置完成例程,需要如下编写代码:
//向下复制堆栈
IoCopyCurrentIrpStacklocationToNext(Irp);
//向下传递IRP
status=IoCallDriver(nextDevice, Irp);
//直接返回底层驱动状态
return status ;
此时没有调用IoMarkIrpPending,是因为没有设置完成例程,底层驱动会自动将堆栈
的Control域的SL PENDING_ RETURNED位复制到本层堆栈。
然而如果是设置了完成例程,传播Pending位的任务就留给了程序员完成。对于设置
了完成例程,下面的代码似乎是正确的:
//向下复制准栈
IoCopyCurrentIrpstackLocationToNext( Irp );
//向下传递IRP
status = IoCallDriver( nextDevice, Irp );
//下面的代码是有错误的!
//因为调用IoCallDriver后,IRP就不在归本层所有,因此再操作IRP会导致错误
if(status ==STATUS_PENDING){
IoMarkIrpPending( Irp );
}
//直接返回底层驱动状态
return status ;
由于调用IoCallDriver后,IRP就不再归当前堆栈所有,因此以上的代码有一点问题。
为此,传播Pending位的任务就留给了完成例程。以下的代码是正确的:
NTSTATUS
MyIoCompletion(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp,
IN PVOID Context
)
{
//进入此函数标志底层驱动设备将IRP完成
KdPrint(("Enter MyIoCompletion\n"));
if (Irp->PendingReturned)
{
//如果底层驱动是否有Pending位 如果有则当前堆栈也设置Pending位
IoMarkIrpPending( Irp );
}
//完成函数的其他附加行为
return STATUS_SUCCESS;//同STATUS_CONTINUE_COMPLETION
}
完成例程返回STATUS_SUCCESS
当IRP被IoComplectcRequest完成时,IRP就会沿着一-层层的设备 堆栈向上回卷。如果
途经遇到某设备堆栈的完成例程,则进入该完成例程。完成例程如果返回
STATUS_ SUCCESS (别名是STATUS_CONTINUE_COMPLETION),则继续向上回卷。此时的完成例程仅仅就是一个通知,表明IRP已完成。
NTSTATUS
MyIoCompletion(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp,
IN PVOID Context
)
{
//进入此函数标志底层驱动设备将IRP完成
KdPrint(("Enter MyIoCompletion\n"));
if (Irp->PendingReturned)
{
//如果底层驱动是否有Pending位 如果有则当前堆栈也设置Pending位 在本层堆栈设置了完成例程,需要自己手动传递,
IoMarkIrpPending(Irp);
}
//完成函数的其他附加行为
//设置这个标志,驱动不会再得到IRP的控制权 STATUS_SUCCESS 就是一个通知,表明IRP已完成
return STATUS_SUCCESS;//同STATUS_CONTINUE_COMPLETION
}
#pragma PAGEDCODE
NTSTATUS IrpReadProc(IN PDEVICE_OBJECT pDevObj,
IN PIRP pIrp)
{
DbgPrint("设备栈顶层设备不处理此IRP 转发给下一层处理\n");
NTSTATUS status = STATUS_SUCCESS;
//得到设备扩展
PDEVICE_EXTENSION pDeviceExtension = pDeviceObject->DeviceExtension;
//将当前I/O堆栈拷贝至底层堆栈
IoCopyCurrentIrpStackLocationToNext(pIrp);
//设置完成例程
IoSetCompletionRoutine(pIrp,
MyIoCompletion, //完成例程
NULL, //传递给完成例程的参数
TRUE,
TRUE,
TRUE
);
//调用底层驱动
status = IoCallDriver(pDeviceExtension->AttachedDeciveObjectPointer, pIrp);
//当调用IoCallDriver后,并且完成例程返回的是STATUS_SUCCESS IRP就不再属于派遣函数,就不能对IRP进行操作了.
if (status==STATUS_PENDING)
{
DbgPrint("STATUS_PENDING\n");
}
//将状态设置为挂起
status = STATUS_PENDING;
return status;
}
完成例程返回STATUS_MORE_PROCESSING_REQUIRED
当IRP被IoCompleteRequest完成时,IRP就会沿着一-层层的设 备堆栈向上回卷。如果
途经遇到某设备堆栈的完成例程,则进入该完成例程。完成例程如果返回STATUS_MORE PROCESSING_ REQUIRED,则停止向上回卷。这时的本层堆栈又重新获得IRP的
控制,并且该IRP 从完成状态又变成了未完成状态,需要再次完成,即需要再次执行
IoCompleteRequest。
重新获得的IRP可以再次发往底层驱动,也可以自己标志完成,即调用IoComplete
Request。
以下给出了使用完成例程的例子,该完成例程返回STATUS_MORE_PROCESSING_REQUIRED。在调用loCallDriver 之后,当前设备想等待底层驱动将设备完成后再继续执行。因此,loCallDriver 初始化了个事件,并将事件指针传递给了完成例程。IRP 被完成后进入完成例程,并触发事件。
这种技巧被广泛应用于驱动程序的编写中,代码如下:
NTSTATUS
MyIoCompletion(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp,
IN PVOID Context
)
{
if (Irp->PendingReturned == TRUE)
{
//设置事件
KeSetEvent((PKEVENT)Context,IO_NO_INCREMENT,FALSE);
}
return STATUS_MORE_PROCESSING_REQUIRED;
}
#pragma PAGEDCODE
NTSTATUS HelloDDKRead(IN PDEVICE_OBJECT pDevObj,
IN PIRP pIrp)
{
KdPrint(("DriverB:Enter B HelloDDKRead\n"));
NTSTATUS ntStatus = STATUS_SUCCESS;
//将自己完成IRP,改成由底层驱动负责
PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
//本层堆栈也可能会有一个完成函数,如果直接使用本层堆栈的话,设置完成函数的时候回覆盖掉.所以都是先把堆栈复制到下一层,然后设置完成函数到下一层堆栈中
IoCopyCurrentIrpStackLocationToNext(pIrp);
KEVENT event;
//初始化事件
KeInitializeEvent(&event, NotificationEvent, FALSE);
//设置完成例程 最后三个参数指明了 在 成功 发生错误 或者取消的情况下都会调用完成例程.
IoSetCompletionRoutine(pIrp,MyIoCompletion,&event,TRUE,TRUE,TRUE);
//调用底层驱动
ntStatus = IoCallDriver(pdx->TargetDevice, pIrp);
if (ntStatus == STATUS_PENDING)
{
KdPrint(("IoCallDriver return STATUS_PENDING,Waiting ...\n"));
KeWaitForSingleObject(&event,Executive,KernelMode ,FALSE,NULL);
ntStatus = pIrp->IoStatus.Status;
}
//虽然在底层驱动已经将IRP完成了,但是由于完成例程返回的是
//STATUS_MORE_PROCESSING_REQUIRED,因此需要再次调用IoCompleteRequest!
IoCompleteRequest (pIrp, IO_NO_INCREMENT);
KdPrint(("DriverB:Leave B HelloDDKRead\n"));
return ntStatus;
}