正确的退出要求线程从wait函数返回后执行CancelIo操作取消掉你的IRP,这需要驱动配合在IRP里设置CancelRoutine。假如没有CancelRoutine,那么CancelIo操作是失败的,前面讲的那些恐怖故事还是会发生。关于这一点,我打算下次再说。

当一个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做个例子,所有的代码都在内核态,免得各位看官看示例代码还要做上下文切换。


通过实验,我现在已基本搞清了原因. 与大家交流

1.普通WIN32用户态程序与驱动程序交互:
(1).当用户程序打开设备时, IO管理器向驱动程序发IRP_MJ_CREATE.
(2) 用户程序退出时,不论此时是否有未完成的IRP, IO管理器都会首先向驱动发IRP_MJ_CLEANUP.
(3) 如果在发送了IRP_MJ_CLEANUP之后,仍有未完成的IRP(例如驱动程序未提供CleanUp例程或在CleanUp例程中未清除所有未完成的IRP,就会造成这种情况), 则IO管理器会依次调用这些IRP的OnCancel例程(如果驱动程序既未提供CleanUp例程又未挂接Cancel例程就糟了,这些IRP将无法得到清除).最后,IO管理器向驱动发IRP_MJ_CLOSE.

2.上层驱动程序与下层驱动程序的交互.
(1).当上层驱动调用IoGetDeviceObjectPointer()时,该函数会向下层驱动发送IRP_MJ_CREATE和IRP_MJ_CLEANUP.
(2).当上层驱动调用ObDereferenceObject()时,该函数只会向下层驱动发送IRP_MJ_CLOSE.

3.故障原因:
我的上层驱动在Unload例程中调用了ObDereferenceObject(), 但是由于仍有未完成的IRP,因此即使我的上层驱动卸载了,但这些IRP仍然留在下层驱动程序的队列中未得到清除,以后当下层驱动一旦去完成这些IRP时就会造成BUG_CHECK, 因为IRP中的FileObject域指的文件对象已不存在.




dazzy 2001-07-27 08:45
分析透彻!一般的驱动都要注册并实现CLEANUP的dispatch.