关于NT内核cancel irp的问题

http://www.cxy.me/doc/4434.htm

 

NT内核中IRP的cancel是一个复杂的问题,很容易出错导致系统崩溃,ddk中的文档其实对这部分说的很详细,只是需要认真体会,osr网站上以前在NT insider杂志中有过2篇文章研究这个问题,总结这些资料,写个贴子罐水如下:
1.为什么要取消irp?
ddk文档中说的很清楚:"Any driver in which IRPs can be held in a pending state for an indefinite interval must have one or more Cancel routines. For example, a keyboard driver might wait indefinitely for a user to press a key."就是说如果你的驱动对于一些irp可能很长时间得不到完成,需要pending相当长的时间那么你就需要写个cancel例程在适合的时候把这些一直pending的irp取消.最常见的一种情况是:issue(触发)这些irp的线程终止了,而这些irp还没来得及完成,这时候就需要取消这些未完成的irp.
2.NT内核如何cancel irp?
根据nt insider的说法,NT IO manager取消irp的过程分成三步.当一个线程要终止了,内核会调用NtTerminateThread()这个native API处理终止线程的事情.NtTerminateThread()会检查该thread的ETHREAD结构中有一个IrpList域,这个是一个list,连接着所有该线程触发的未完成irp.通过遍历这个链表,对每个未完成的irp调用IoCancelIrp()检查有没有注册cancel例程,如果有运行之,这样就完成了三步曲的第一步.
接着开始cancel irp的第二步:这一步会执行一个等待,等待线程的irplist变成空,这说明所有未完成的irp都被各自的cancel routine处理完了(取消了,并从这个irplist链表中remove掉了),但如果有的驱动程序cancel routine出了问题,不能取消irp,则可能导致线程的irplist永远也不会为空.为了避免这种情况导致的死循环,NT内核做了个限定,如果等待了5分钟这个irplist还没有变空,则认为某些驱动出错了,这种情况下,NT会执行三步曲的第3步.
第3步内核会强行将这些未完成的irp从线程的irplist上remove掉,然后尽可能的释放掉线程占用的资源,最终让这个线程终止,但是这些未完成的irp却不会被释放,它们保存在内存种,这样会导致系统内存资源的减少,这些被irp站用的内存永远不会被释放.
3.什么时候需要编写cancel routine?
(1)ddk document说的很清楚:"if a driver will never queue more IRPs than it can complete in five minutes, it probably does not need a Cancel routine. ".就是说如果一个驱动能保证它的irp总是能及时完成,或者说在短时间内(比较几秒)总是会完成,那么根本不需要编写cancel routine.
(2)即使需要编写cancel routine,也不需要弄很复杂的算法,不需要这个cancel routine的效率有多高,弄个简单的cancel routine足够了,原因是:发生这种必须要取消irp的可能性是很少的,不值得为了这偶尔发生的事情花那么大气力去写一个高效的cancel routine
ddk document中给出了一个指导原则:
"The highest-level driver in a chain of layered drivers must have at least one Cancel routine if it queues IRPs or otherwise holds IRPs in a cancelable state. It can have more than one Cancel routine, if necessary.

Lower-level drivers in which IRPs can be held in a cancelable state for relatively long intervals also should have one or more Cancel routines.

If a driver manages its own internal queues of IRPs, it should have a separate Cancel routine for each of its queues. "
4.编写cancel routine的原则
ddk document种说的很清楚,一定遵循:"pending---IoSetCancelRoutine--Queue"的顺序.你首先要在分派例程中pending一个irp,因为只有pending的irp才有可能需要取消,如果你总是能在分派例程中完成irp,那么就就不需要取消它.pending irp 的方法在"关于NT内核irp pending的注意事项"中说了,必须调用IoMarkIrpPending()后返回STATUS_PENDING.
pending 后再调用IoSetCancelRoutine设置一个取消例程(cancel routine).再把它插入队列中(系统全局队列或者你自己的私有队列),这个过程一定要hold spin lock(或者是global cancel spinlock,或者是你自己定义的保护私有irp队列的spinlock),否则会导致竞争,系统会崩溃.
示例:
NTSTATUS
CancelReadWrite(
    IN PDEVICE_OBJECT DeviceObject,
    IN PIRP Irp
    )

{
    PDEVICE_EXTENSION devExtension = (PDEVICE_EXTENSION) DeviceObject->DeviceExtension;
    KIRQL irql;

    //
    // mark the irp pending NOW before we queue this IRP
    //

    IoMarkIrpPending(Irp);

    //
    // serialize all driver activity for this device object
    //

    KeAcquireSpinLock(&devExtension->lock, &irql);

    //
    // set the cancel routine
    //

    IoSetCancelRoutine(Irp, CancelCancel);

    //
    // queue the IRP
    //

    InsertTailList(&devExtension->irpList, &Irp->Tail.Overlay.ListEntry);

    //
    // release the spinlock
    //

    KeReleaseSpinLock(&devExtension->lock, irql);

    //
    // always return status pending -
    // note that we might very well have already completed,
    // or cancelled this IRP before we get here.
    //

    return STATUS_PENDING;

}
在你的取消例程中完成取消irp的操作,比如把irp从队列中remove,并调用IoCompleteRequest()完成它,这里需要返回STATUS_CANCELLED.示例:
VOID CancelCancel (
    IN PDEVICE_OBJECT DeviceObject,
    IN PIRP Irp
    )

{
    PDEVICE_EXTENSION devExtension = DeviceObject->DeviceExtension;
    PLIST_ENTRY nextEl = NULL;
    PIRP cancelIrp = NULL;
    KIRQL irql;
    KIRQL cancelIrql = Irp->CancelIrql;

    //
    // release the cancel spinlock now
    //

    IoReleaseCancelSpinLock(cancelIrql);

    //
    // A thread has terminated and we should find a
    // cancelled Irp in our queue and complete it
    //

    KeAcquireSpinLock(&devExtension->lock, &irql);

    //
    // search our queue for an Irp to cancel
    //

    for (nextEl = devExtension->irpList.Flink;
        nextEl != &devExtension->irpList; )

    {

        cancelIrp = CONTAINING_RECORD(nextEl, IRP, Tail.Overlay.ListEntry);
        nextEl = nextEl->Flink;
        if (cancelIrp->Cancel) {

            //
            // dequeue THIS irp
            //

            RemoveEntryList(&cancelIrp->Tail.Overlay.ListEntry);

            //
            // and stop right here
            //
            break;

        }
        cancelIrp = NULL;

    }
   KeReleaseSpinLock(&devExtension->lock, irql);

    //
    // now if we found an irp to cancel, cancel it
    //

    if (cancelIrp) {

        //
        // this is our IRP to cancel
        //

        cancelIrp->IoStatus.Status = STATUS_CANCELLED;
        cancelIrp->IoStatus.Information = 0;
        IoCompleteRequest(cancelIrp, IO_NO_INCREMENT);

    }

    //
    // we are done.
    //

}

最后一点:如果你pending的irp最后被成功完成了,则你不再需要取消它了,那么你一定在完成它的时候调用IoSetCancelRoutine(Irp, NULL);清除你指定取消例程.示例:
//
// always remove cancel routine
// or we bugcheck in free build, assert in checked
//

(void) IoSetCancelRoutine(Irp, NULL);

//
// indicate we are finished
//

Irp->IoStatus.Status = STATUS_SUCCESS;

//
// no actual data transfer.
//

Irp->IoStatus.Information = 0;

//
// complete the request
//

IoCompleteRequest(Irp, IO_NO_INCREMENT);

这通常在完成例程中做这件事.
5.对于使用system cancel spinlock来说,有良种情况,为了防止race condition,在设置取消例程的时候,要hold spinlock.ddk document说:"If a device driver has a StartIo routine, its dispatch routines can register a Cancel routine by supplying its address as input to IoStartPacket.

If a driver does not have a StartIo routine, its dispatch routines must do the following before queuing an IRP for further processing by other driver routines:

Call IoAcquireCancelSpinLock.
Call IoSetCancelRoutine with the input IRP and the entry point for a driver-supplied Cancel routine.
Call IoReleaseCancelSpinLock. "
很清楚了.

你可能感兴趣的:(关于NT内核cancel irp的问题)