IRP这个话题太大,基本上这是一个框,什么都能往里面装。我也不知道我漏掉了多少内容,总之呢,想到哪儿就说到哪儿吧。
一个PnP设备被删除时有两个事件会发生,一个是IRP_MN_SURPRISE_REMOVAL,另一个是IRP_MN_REMOVE_DEVICE。看名字就知道这两个事件都是告诉程序员设备已经不在了让他赶紧做点善后工作。我知道你的第一反应时什么:为什么要有两个事件呢,清理资源难道不是清理一次就够了吗?这事就说来话长了,windows跟linux有个很大的不同点就是,linux删除文件的时候不会管这个文件是什么状态,即使是使用中的文件也照删不误(所以一个流行段子说某个linux程序的卸载脚本写的有问题,rm –rf /home/xxx/yyy被它写成了rm –rf / home/xxx/yyy,结果卸载这个软件就把整个操作系统给删了),但是windows使用不同的策略,如果某个文件有人在用,那么它是删不掉的。这个设计规则可以扩展到任何可以用CreateFile生成HANDLE的系统资源上,驱动自然也不例外。在所有的HANDLE被关闭之前IRP_MN_REMOVE_DEVICE是不会被调到的,所以这事就得分两步走:先通知你卡已经消失了,赶紧做善后工作特别是把HANDLE都关了;然后在通知真正的删除事件。
需要两个事件的原因虽然勉强说通了,但你以为这样就完事大吉了吗?太天真了少年。咱的第一反应都是对的,这本来就是一件事,由于某些原因被强行拆成了两部分,那么中间必然有空子可钻:我们假设当前PDO的Device Instance是Instance0,它会在IRP_MN_REMOVE_DEVICE里被删除;我们再假设我们的程序员是个新手,他在IRP_MN_SURPRISE_REMOVAL里写了个超级大循环需要10分钟才能处理完。然后问题就来了:这10分钟里可以发生太多事情了,比如我们又重新把卡插了回去。由于前一次IRP_MN_REMOVE_DEVICE还没被调到所以Instance0还存在,插卡之后系统发现Instance还在所以还是用旧的(也就是Instance0)。10分钟以后,IRP_MN_SURPRISE_REMOVAL终于执行完了,所有的HANDLE也都关闭了,IRP_MN_REMOVE_DEVICE终于开始执行并删掉了Instance0,因为它不可能知道卡已经被重新插上了。这时候我们得到了这样一个状态:卡还插着,却没有Device Instance可用,这驱动就跟没有一样。
我们从中能得到的教训有两个:第一,IRP_MN_SURPRISE_REMOVAL可不能花10分钟才执行完,它应该越快越好;第二,Device Instance也应该在IRP_MN_SURPRISE_REMOVAL里删除,否则不管IRP_MN_SURPRISE_REMOVAL执行的多快用户还是能在IRP_MN_REMOVE_DEVICE执行之前把卡插回去,你可千万不要对用户行为做太多假设。
WDK文档上对于Symbol Link的说法里有这么一条:不要为PnP设备创建Symbol Link,不过这规则好像也没什么人在遵守,因为用SetupDiXxx那一套设计不良的屎一样的API去获得Device Instance Path实在很麻烦,微软又因为兼容性包袱太重没敢直接把它禁了,所以大家都还是在偷懒用Symbol Link访问驱动。
大体来讲,Symbol Link是Device Instance Path的别名,它们指向同一个PDO。比如我们有一个PDO,系统为它生成的Device Instance Path是 Instance0,而我们在IRP_MN_CREATE处理函数里生成了一个Symbol Link是\\MyDriverName, 那么系统的名字空间里就有两个路径指向这个PDO。还记得名字空间吗?我们聊过一次这东西,基本上你可以把它想象成一个文件系统,PDO是存放在某处的一个文件,Device Instance Path是该文件的绝对路径,Symbol Link就是一个快捷方式。这两者最大的不同是Device Instance Path由系统生成并有系统管理,一个PnP设备被拔出后这个名字也就消失了(如前面所说,由IRP_MN_SURPRISE_REMOVAL负责删除);而Symbol Link由程序员手动生成,名字基本上也是定死的(比较常见的做法是在一个.h文件里#define 一个字符常量,然后拿它当IoCreateSymbolicLink的参数),并且PnP设备被拔出后该名字不会自动消失。再以文件系统举例子,你在C盘根目录里放了一个文件叫a.txt,然后在桌面上创建了一个快捷方式MyStory.txt.lnk指向c:\a.txt。那么删掉a.txt后c:\a.txt这个路径自然就消失了,而在桌面上的快捷方式MyStory.txt.lnk可不会自动消失,它一直在那儿除非你手动删除,并且更糟的是,在a.txt重新创建之前任何访问MyStory.txt.lnk的尝试都是徒劳的,必定失败的。更更糟的是,PnP设备每次插入后系统给生成的Device Instance名字是不一样的,这一次叫a0.txt,下次它就叫a1.txt,你都没法猜,所以MyStory.txt.lnk这个快捷方式在拔卡动作完成之后就彻底没用了,怎么也不可能指向正确的内容。这个例子当然不是百分百贴切,不过大家领会意思就行。
解决方案也是非常简单:既然拔卡后Symbol Link就废了,那就在拔卡处理函数里把它删了好了。所以这就是Symbol Link和IRP_MN_SURPRISE_REMOVAL也要搅在一起的原因:如果你在IRP_MN_CREATE里创建过Symbol Link,那么在IRP_MN_SURPRISE_REMOVAL里就必须删除它。
简单的讲就是任何时候都要写IRP_MN_SURPRISE_REMOVAL的响应函数。别以为你的设备不是PnP设备,或者你的机器有特殊设置用户不可能拔你的卡,你就偷懒不处理IRP_MN_SURPRISE_REMOVAL了。因为这个事件并不只是在检测到硬件不在了才发,事实上,驱动栈里的任何一个驱动发出一个错误状态时都会触发该事件。比如你在IRP_MN_QUERY_PNP_DEVICE_STATE响应函数里将状态改为PNP_DEVICE_FAILED,那么有任何驱动调用IoInvalidateDeviceState 之后都会引起IRP_MN_SURPRISE_REMOVAL事件。
基本上这些就是今晚我想说的,其他内容等我想到了(或者又从哪儿学到了新知识)再继续讲。