wdk tips (7.2): IRP的CancelRoutine

上回我们留下一个未解的问题,就是当一个IRP的CancelRoutine没有被设置时,CancelIo操作会失败,系统中有可能会留下永远都不会被complete的IRP。在Threaded IRP和non-threaded IRP一节中我们有谈到irp分为线程相关和非线程相关两种。倘若一个永远不complete的irp是非线程相关的,情况会稍微好一点,顶多系统中泄露了一个资源。倘若该irp是线程相关的,那事情就大了。thread IRP由IoManager生成并保留在线程的IRP队列里,负责处理该IRP的驱动在收到下层驱动的Complete事件后不会主动收回IRP的资源而是继续complete给IoManager,由IoManager负责回收,并从线程IRP列表中删除该IRP。一个线程在退出前会遍历等待IRP队列里所有的IRP,直到它们全部被complete为止。倘若其中有一个irp永远不complete,那么线程就永远不退出,无论是ExitThread也好还是_endthreadex也好还是什么邪恶的暴力擦除数据强退也好,全都不顶用。线程不退出,进程也不能销毁(题外话:进程资源的回收动作由最后一个线程退出后发起,所谓的杀进程,其实是用apc给所有线程发起退出操作)。更糟糕的是,操作系统的关机过程都会被堵住,除了关电源,没有其他办法恢复,这一点简直比BSOD还糟糕。我们知道由user mode发起的IO操作最后都会翻译成threaded irp,这就是为什么我在7.1大谈特谈user mode线程的原因:这个陷阱连user mode程序也会掉进去。Bad dog!
要解决这一点方法很简单目标很明确,那就是防止“永远不complete的irp”这种东西出现。一般的做法是加个线程或者timer并设置超时时间,时间一到就cancel这个irp。如果irp由user mode程序发起,那么就调用CancelIo;如果irp由驱动发起,则是调用IoCancelIrp。所有这些动作要生效的大前提是你的irp有CancelRoutine的存在,否则一切都是白搭。所以这里我有个经验要跟大家分享:任何时候都给你的irp设置CancelRoutine,并在CancelRoutine里Complete你的IRP!为方便起见我们选non-threaded irp做个例子,所有的代码都在内核态,免得各位看官看示例代码还要做上下文切换。以下便是代码:

Sending thread:

IoSetCancelRoutine(Irp, MyCancelRoutine);
devext->SentIrp = Irp;

Canceling thread:

if (devext->AllocatedIrp != NULL) {
   IoCancelIrp(devext->SentIrp);
}

cancel routine里的内容都是标准步骤,不赘述。看起来已经完美无缺了,可惜拿到测试组一跑就BSOD,系统抱怨说一个irp被free了两次,肯定是有地方被疏忽了,对,我们很好的处理的例外情况,却漏掉了常规情况:irp也是可以正常complete的!假如我们的CompleteRoutine是这样的:

Completion routine:

PIRP irp;
irp = devext->SentIrp;
devext->SentIrp = NULL;
IoFreeIrp(irp);


它和CancelRoutine里用到了同一个irp,这是典型的多线程重入问题,需要加锁保护。修改后的代码如下:

Sending thread:
KeAcquireSpinLock(&devext->SentIrpLock, ...);
devext->SentIrp = Irp;
KeReleaseSpinLock(&devext->SentIrpLock, ...);


Canceling thread:
KeAcquireSpinLock(&devext->SentIrpLock, ...);
if (devext->AllocatedIrp != NULL) {
   IoCancelIrp(devext->SentIrp);
}
KeReleaseSpinLock(&devext->SentIrpLock, ...);

Completion routine:
PIRP irp;

KeAcquireSpinLock(&devext->SentIrpLock, ...);
irp = devext->SentIrp;
devext->SentIrp = NULL;
KeReleaseSpinLock(&devext->SentIrpLock, ...);

IoFreeIrp(irp);

return STATUS_MORE_PROCESSING_REQUIRED;


又是一个完美的程序,半分钟修掉一个BSOD,还有比这更爽的吗?结果一测试问题更大:系统挂起没有任何反应了。经验告诉我们这是一个死锁:Cancel thread获得spin lock后调用IoCancelIrp,IoCancelIrp最终进入CancelRoutine,而CancelRoutine则调用了IoCompleteIrp并进入Complete routine并试图再次获得spin lock,而该死的spin lock在同一条线程里也是会死锁的,这就是最终原因。
问题出在rip的完成上。设置了Cancel routine和Complete routine后,有两个点可以做irp的完成和回收动作,而这两个点只能有一个被执行。借用网上某牛的代码描述我们可以看到有以下几种情况:

// No cancellation:
//   Cancelable-->Completed
//
// Cancellation, IoCancelIrp returns before completion:
//   Cancelable --> CancelStarted --> CancelCompleted --> Completed
//
// Canceled after completion:
//   Cancelable--> Completed -> CancelStarted
//
// Cancellation, IRP completed during call to IoCancelIrp():
//   Cancelable --> CancelStarted -> Completed --> CancelCompleted


这跟同步还是两回事,同步是指两个点不能同时摸这个irp,一个摸完换另一个则是可以的,而我们要达到的目标是只要irp被其中的任何一个摸过了,另一个就不能再去摸它。为了达到这个目的,我们需要增加额外的变量记录irp被摸了几次这个信息.改造后的cancel过程如下
        if (InterlockedExchange((PVOID)&touched, IRPLOCK_CANCEL_STARTED) == IRPLOCK_CANCELABLE) {

          //
          // You got it to the IRP before it was completed. You can cancel
          // the IRP without fear of losing it, because the completion routine
          // does not let go of the IRP until you allow it.
          //
          IoCancelIrp(irp);

          //
          // Release the completion routine. If it already got there,
          // then you need to complete it yourself. Otherwise, you got
          // through IoCancelIrp before the IRP completed entirely.
          //
          if (InterlockedExchange(&touched, IRPLOCK_CANCEL_COMPLETE) == IRPLOCK_COMPLETED) {
            IoCompleteRequest(irp, IO_NO_INCREMENT);
          }
        }

而改造后的complete过程则如下
  if (InterlockedExchange((PVOID)&touched, IRPLOCK_COMPLETED) == IRPLOCK_CANCEL_STARTED) {
    //
    // Main line code has got the control of the IRP. It will
    // now take the responsibility of completing the IRP.
    // Therefore...
    IoFreeIrp(Irp);
    return STATUS_MORE_PROCESSING_REQUIRED;
  }

简单点说就是在中间加入能表示状态信息的变量touched表征现在所处的状态,cancelable, cancel started, cancel complete, completed四个状态相互协调保证complete rip不会被调用两次。如同在tip 5里提到过的,这也是口耳相传下来的范式,基本上有cancel rip的地方都得这么写。
posted @ 2011-03-28 01:09  gussing  阅读(2383)  评论(0编辑  收藏  举报