在这里你简单的按步处理请求和完成它。如果你没有返回STATUS_PENDING,那是最好的。在前面多数的例子中,我们就是这样处理所有的驱动请求的。我们处理他们,当我们做完时,我们简单的调用IoCompleteRequest。
创建IRP
在前面的文章中,对于如何创建和发送IRP有简短的描述。我们会再详细的回顾一下那些步骤。我们也会学习到不同的创建IRP的API.。
步骤1:创建IRP有很少的api可以用于创建IRP。正如我们已经了解的。我们必须了解这些API之间的不同。
IRP分为异步和同步两种。 如果你使用IoAllocateIrp或者IoBuildAsynchronousFsdRequest创建irp,那么你创建的是一个异步的irp. 这意味着你应该设置一个完成例程,并且当IRP完成时,你需要调用IoFreeIrp。你负责管理这些IRP,并且你需要在适当的时候处理他们。
如果你使用IoBuildDeviceIoControlRequest或者IoBuildSynchronousFsdRequest来创建IRP。那么你创建的是一个同步的IRP. 记住,TdiBuildInternalDeviceControlIrp是一个宏,它创建的是同步的irp. 这些irp是由I/O管理器来拥有和管理的。不要释放他们。这些irp一定要使用IoCompleteRequest来完成。如果你传递这样的IRP给IoCallDriver,你不需要完成它,下层的驱动会替你完成它。如果你使用完成例程中途拦截,那么你需要在你用完之后调用IoCompleteRequest来完成。
需要注意,在你考虑创建一个IRP之前,需要确认你已经了解了你的代码将会在怎样的IRQL下被调用。使用IoAllocateIrp的好处是,它可以用用在DISPATCH_LEVEL级别。而IoBuildDeviceIoControlRequest却不能。
步骤2: 设置IRP参数
就拿TDI例子来讲,它很简单。宏TdiBuildSend展示了怎样处理它。我们使用IoGetNextIrpStackLocation,然后设置参数。我们也可以设置我们在处理IRP中需要的MDL和其他属性。
步骤3:发送到驱动栈
这个很简单,我们前面一遍遍的用过。我们使用IoCallDriver来将IRP沿着栈向下传递。
步骤4:等待和清除
如果你做的驱动返回除"STATUS_PENDING“之外的任何状态,如果你创建了一个异步IRP。你要么在完成例程中释放IRP,要么设置它返回更多的处理并且使用IoFreeIrp释放它。
如果你创建一个同步IRP, 要么让I/O管理器处理它,要么你设置完成例程返回更多的处理。
如果返回状态是STATUS_PENDING",你的选择会比较少。 你或者在这里等待IRP,或者你离开并异步完成它。这些都依赖于你的架构。如果你异步创建了IRP,在你设置的完成例程中,你必须检查这个IRP是否被设置成"Pending",然后设置你的事件。这也是为什么你不需要在事件上等待除非返回STATUS_PENDING。想象下如果所有的调用都等待这个事件,会多么慢?
如果IRP是同步创建的,I/O管理器会为你设置事件。你不需要做任何事情,除非你设置了完成例程,希望从中对返回更多的处理。
非分页驱动代码
如果你还记得,在第一节中,我们学过使用#pragma 把我们的驱动代码放到不同的节中。有INIT节(这个节加载后会被释放),还有分页的节(把代码放进了可分页内存区域)。代码怎样获取自旋锁呢? 如果代码必须要在非分页内存加载,我们怎么做呢?我们只需要不用#pragma指定就行了。默认的驱动加载就是在非分页内存。当它不需要在非分页内存中时,我们使用#pragma是为了强制让他从系统的物理内存中转移出来。
如果你看下面这些代码,你就会注意到一些#pragma语句被注释了。这些函数当他们使用自旋锁并且运行在大于APC_LEVEL的层级时,他们需要运行在非分页区。
/* #pragma alloc_text(PAGE, HandleIrp_FreeIrpListWithCleanUp) */
/* #pragma alloc_text(PAGE, HandleIrp_AddIrp) */
/* #pragma alloc_text(PAGE, HandleIrp_RemoveNextIrp) */
#pragma alloc_text(PAGE, HandleIrp_CreateIrpList)
#pragma alloc_text(PAGE, HandleIrp_FreeIrpList)
/* #pragma alloc_text(PAGE, HandleIrp_PerformCancel) */
完成例程怎样工作的?
每一个设备的STACK LOCATION或许都有一个相关联的完成例程。目前被调用的完成例程是上层驱动的,而不是当前驱动的。当前驱动完成irp后,他自己知道什么时候完成的。所以当驱动完成完成后,就会在当前栈域中查看完成例程,如果存在就调用。在调用之前,当前的IO_STACK_LOCATION就会移到指向上层驱动的域。这是很重要的,稍候我们将会看到。如果驱动没有完成IRP,他就会调用IoMarkIrpPending向上传递未决的状态。这是因为驱动返回STATUS_PENDING,他一定要标记IRP为未决的。上层驱动如果不是返回与底层驱动同样的状态,就不需要标记IRP为未决状态。或许他会中途拦截STATUS_PENDING,并且等待完成。然后它会停止IRP的完成,当返回状态不是STATUS_PENDING.时,再完成它。
现在如果你的驱动创建IRP,你不用被迫把IRP标记为未决的。你知道这是为什么?因为你没有IO_STACK_LOCATION,你没有在设备栈上。实际上如果你这么做了,你会破毁内存。
注意这个例子代码会展示一个的完成例程,它会调用IoMarkIrpPending,即便是完成例程中创建的IRP。这也是不应该发生的。实际上,如果你看了真实的代码,如果创建了同步的IRP,通常完成例程是不存在的,或者存在而仅仅返回更多处理的状态。
我在TDI客户端实现了一个完成例程,我们这里创建了同步的IRP。你可以象下面这样察看调试信息。
kd> kb
ChildEBP RetAddr Args to Child
fac8ba90 804e4433 00000000 80d0c9b8 00000000
netdrv!TdiFuncs_CompleteIrp [.\tdifuncs.c @ 829]
fac8bac0 fbb20c54 80d1d678 80d0c9b8 00000000 nt!IopfCompleteRequest+0xa0
fac8bad8 fbb2bd9b 80d0c9b8 00000000 00000000 tcpip!TCPDataRequestComplete+0xa4
fac8bb00 fbb2bd38 80d0c9b8 80d0ca28 80d1d678 tcpip!TCPDisassociateAddress+0x4b
fac8bb14 804e0e0d 80d1d678 80d0c9b8 c000009a
tcpip!TCPDispatchInternalDeviceControl+0x9b
fac8bb24 fc785d65 ffaaa3b0 80db4774 00000000 nt!IofCallDriver+0x3f
fac8bb50 fc785707 ff9cdc20 80db4774 fc786099
netdrv!TdiFuncs_DisAssociateTransportAndConnection+0x94 [.\tdifuncs.c @ 772]
fac8bb5c fc786099 80db4774 ffaaa340 ff7d1d98
netdrv!TdiFuncs_FreeHandles+0xd [.\tdifuncs.c @ 112]
fac8bb74 804e0e0d 80d33df0 ffaaa340 ffaaa350
netdrv!TdiExample_CleanUp+0x6e [.\functions.c @ 459]
fac8bb84 80578ce9 00000000 80cda980 00000000 nt!IofCallDriver+0x3f
fac8bbbc 8057337c 00cda998 00000000 80cda980 nt!IopDeleteFile+0x138
fac8bbd8 804e4499 80cda998 00000000 000007dc nt!ObpRemoveObjectRoutine+0xde
fac8bbf4 8057681a ffb3e6d0 000007dc e1116fb8 nt!ObfDereferenceObject+0x4b
fac8bc0c 80591749 e176a118 80cda998 000007dc nt!ObpCloseHandleTableEntry+0x137
fac8bc24 80591558 e1116fb8 000007dc fac8bc60 nt!ObpCloseHandleProcedure+0x1b
fac8bc40 805916f5 e176a118 8059172e fac8bc60 nt!ExSweepHandleTable+0x26
fac8bc68 8057cfbe ffb3e601 ff7eada0 c000013a nt!ObKillProcess+0x64
fac8bcf0 80590e70 c000013a ffa25c98 804ee93d nt!PspExitThread+0x5d9
fac8bcfc 804ee93d ffa25c98 fac8bd48 fac8bd3c nt!PsExitSpecialApc+0x19
fac8bd4c 804e7af7 00000001 00000000 fac8bd64 nt!KiDeliverApc+0x1c3
kd> dds esp
fac8ba94 804e4433 nt!IopfCompleteRequest+0xa0
fac8ba98 00000000 ; This is the PDEVICE_OBJECT, it's NULL!!
fac8ba9c 80d0c9b8 ; This is IRP
fac8baa0 00000000 ; This is our context (NULL)
kd> !irp 80d0c9b8
Irp is active with 1 stacks 2 is current (= 0x80d0ca4c)
No Mdl Thread ff7eada0: Irp is completed. Pending has been returned
cmd flg cl Device File Completion-Context
[ f, 0] 0 0 80d1d678 00000000 fc786579-00000000
\Driver\Tcpip netdrv!TdiFuncs_CompleteIrp
Args: 00000000 00000000 00000000 00000000
你看到现在我们在IO_STACK_LOCATION #2位置,这个是不存在的。所以实际上这个IRP从一个不存在的高的IO_STACK_LOCATION位置开始。是否你还记得,我们需要调用IoGetNextIrpStackLocation来设置参数。这就是说,如果我们在这里调用IoMarkIrpPending,实际上我们访问的不是我们应该访问的内存。因为IoMarkIrpPending实际上是在IO_STACK_LOCATION上设置。无独有偶,设备对象也是NULL。这是因为我们的栈域不存在。既然我们并不是设备栈中的一部分,所以我们不会有关联的设备对象。
什么是STATUS_PENDING?
如果我还没有使你困惑,我们就继续谈谈STATUS_PENDING和IoMarkIrpPending。 他们的用途是什么? 用途就是我们可以处理异步IRP并且让上层驱动和I/O管理器了解。 第一部分STATUS_PENDING返回是最佳的,所以如果我们想等,我们仅仅需要为异步操作返回它。第二部分是IoMarkIrpPending实际上是传播IRP上的未决返回状态。这种方式我们不需要总是调用KeSetEvent,只需要做的就是在返回STATUS_PENDING的情况下处理。
另外的用途就是中间层的驱动可以改变STATUS_PENDING状态为STATUS_SUCCESS ,不需要自始至终地沿着驱动栈传递全部未决的状态。
重叠I/O
STATUS_PENDING体系是如何实现重叠I/O的本质。本篇例子使用ReadFileEx和WriteFileEx,并不是说ReadFile和WriteFile不能用在这里。他们也可以。如果你看CreateFile这个API,我添加了一个使能重叠I/O的标志。如果你去掉这个标志,I/O管理器就会阻塞在STATUS_PENDING而不是返回到应用。这需要设置一个事件,直到I/O完成。这是用户程序使用异步I/O的本质。