IO_STACK_LOCATION很重要,再多聊一点也无妨。上上回我们谈了IO_STACK_LOCATION和那几个重要的函数,当然,我的目的不是扫盲,而是记下一些容易犯错的地方(实际上都是工作中碰到过的钉子)以方便自己回顾。我的记性是如此的差以至于几月不看就会忘记。如果你对这东西没概念,我建议你先多查查WDK文档。
上回我们聊了IoCopyCurrentIrpStackLocationToNext和IoSkipCurrentIrpStackLocation的差别(你看我的记性是不是很差,其实是上上回说的),结果把要聊的核心内容给忘了。IO_STACK_LOCATION这坨东西出现的原因很大程度上就是为了解决异步IO中出现的问题,所谓的异步IO,指的是受到irp请求后,并不在同一个线程上下文里就把所有的事情做完,而是先把irp放在队列里,然后返回pending,在其他线程里做完真正的事情后在complete这个irp。这里涉及到一个重要的函数IoMarkIrpPending,它会在irp中置一个SL_PENDING_RETURNED的flag告诉IoManager事情没做完,别急着回收资源。IoManager在调用irp分发函数返回时,会检查分发函数的返回值,如果返回的不是STATUS_PENDING而SL_PENDING_RETURNED flag已经被置上,那么BSOD将会发生。我不知道你是怎么想的,反正我看到两个域,两个线程,一致这些词同时出现就觉得麻烦又要来了,事实证明这档子还真有那么点麻烦。
让我们研究点具体的问题。“先把irp放在队列里,然后返回pending,在其他线程里做完真正的事情”这句话翻译成代码大概会是这么个样子<1>
DriverDispatch: IoMarkIrpPending (1) InsertToList return STATUS_PENDING work item routine: IoSkipCurrentIrpStackLocation (2) IoCallDriver
这些代码看上去没错,实际隐含了一个问题:IoCallDriver将irp分发给下层驱动后,而下层驱动的行为你是没法控制的。我们假设下层驱动的处理函数是这么写的
Irp->IoStatus.Status = STATUS_SUCCESS; IoCompleteRequest(Irp, 0); return STATUS_SUCCESS; (3)
代码(1)中将SL_PENDING_RETURNED flag置上,代码(2)调用的是skip,也就是说下层驱动还是用同一层location,SL_PENDING_RETURNED也还存在,代码(3)却返回了一个STATUS_SUCCESS,bang!BSOD发生了。你的驱动是没问题,但你把人家的弄蓝屏了这怎么行。也许你要抱怨下层驱动为何变成同步了,但是我也老实跟你讲,别对人家的代码做任何假设,特别是人家的逻辑完全没错的情况下。那怎么办呢,我首先想到的是把skip函数换成copy函数,如下<2>
DriverDispatch: IoMarkIrpPending InsertToList return STATUS_PENDING Thread: IoCopyCurrentIrpStackLocationToNext (4) IoSetCompleteRoutine IoCallDriver
这么写我还是担心下层驱动会BSOD,因为代码(4)把本层的所有信息都拷贝到下层去了,SL_PENDING_RETURNED依然存在嘛。或许改成如下形式可行<3>
DriverDispatch: IoCopyCurrentIrpStackLocationToNext IoSetCompleteRoutine IoMarkIrpPending InsertIntoList return STATUS_PENDING; Thread: IoCallDriver
这段代码能work,但是很丑陋,IoCopyCurrentIrpStackLocationToNext的逻辑明显是跟Thread要做的事情有关系的,事实上这个函数一般是紧跟着IoCallDriver一起使用的。这下我就没什么办法了,怎么做都不对,只好去新闻组里问问题。结果确实出乎意料的,这个问题早就有人问过了,并且答案也很简单,<2>其实是对的,我对IoCopyCurrentIrpStackLocationToNext函数的理解有误,我以为它只是单纯的拷贝内容,实际上这个函数除了拷贝还做了另外一些事情,它的实现如下
PIO_STACK_LOCATION irpSp; PIO_STACK_LOCATION nextIrpSp; irpSp = IoGetCurrentIrpStackLocation(Irp); nextIrpSp = IoGetNextIrpStackLocation(Irp); RtlCopyMemory( nextIrpSp, irpSp, FIELD_OFFSET(IO_STACK_LOCATION, CompletionRoutine)); nextIrpSp->Control = 0; (5)
仔细看代码(5),它把Control域里的flag清空了。到此为止IoMarkIrpPending引起的问题事情圆满得到解决。
聊到这里我要发发感慨:要自己山寨一个调用栈,还想让它工作在多线程环境下是多么的不容易。在这里我不得不说点难听的:这几个函数其实没有好好design过,看那写个长的吓人的名字,还有那么多side effect,正确性全靠口耳相传的规则保证,山寨这两字用在这上面再贴切不过。引用新闻组里某牛的说法,当我看到IoSkipCurrentIrpStackLocation这个函数时,我猜想它的行为应该是告诉IoManager本层location的东西我完全没动,下层驱动也千万别来改我的东西,谁会想到下层驱动是可以改本层内容的。倘若我们自己来设计一个分层的模块,各层之间数据相互独立应该是个常识,诸位写MVC的恐怕不会让C保留一个V的指针随便修改吧。并且<2>这种代码片段应该是异步IO的范式,其他写法几乎都是错的,那为何不直接抽象出一个函数做这些事情呢,靠口耳相传算怎么回事。
说到这里我又有新的感慨,坦率讲啥事都封装好让程序员来调用的做法我是不欣赏的,因为这会阻碍程序员的进步。如果这种函数真的存在,我这篇博文恐怕就没机会写了,我甚至都可能不知道有这么回事。看着.net,java等框架那么多设计良好封装严实的接口,有谁知道里面都藏了些什么故事,又有几个人会去关心一个行为正确的api后面有多少的汗水和无奈。想反,封装不好的函数则会强迫你去理解问题的本质,这对程序员的自我提升是很有好处的。我承认如果想把IT这个行当工业化,封装等事是必须做的,程序员的水平好坏差别巨大,完全没办法用统一尺度估量生产力,生产力不能估量那么进度,质量等的估量也无从谈起,而封装起到的作用是把所有的程序员尽可能拉倒同一水平级,上下偏差尽量小,让估量的工作有那么点实施的可能。站在老板的角度我当然希望程序员是完全无差别的,就跟流水线上的机器人手臂一样最好。但我不是老板,我是程序员,我可不希望当什么劳什子的机器人手臂,从某种角度将,我倒是希望完全没现成api最好,逼着自己什么都得知道。当然,这种想法有点极端,工业化还是要做的,否则IT项目那么高的失败率就没法将下来。幸运的是折中的方法也还是存在的,那就是借助开源:工业化接着搞,api接着封装,机器人手臂接着当,但有时间有精力多看看开源代码,看看那堆api后面到底都有些什么玩意儿,这对我们自己有好处。设计良好的类库是老板的好朋友,而开源是程序员的好朋友,没事别到xxBeta或者啥啥地方去当喷子,现在这世道替老板说话的人已经太多了,而替程序员说话的却极少,偶尔有时候也该替自己多想想。