[翻译]Rolling Your Own
发表于:
2008-2-15 13:49
16697
在这篇文章中我将描述如何自己构造IRP还有I/O管理器提供一些分配和管理IRPs的例程。
经常听到NT驱动程序开发者问的问题是如何在他们自己所驱动中执行一个I/O操作。这个问题可以总
结为以下两点:当仅可用到一个文件对象但是ZwXXX例程需要一个文件句柄时如何处理I/O;为什么
ZwCreateFile返回的句柄不能用在他们的驱动中。实际的问题是如何在他们的驱动中处理I/O操作,典型
的在多线程上下文中。
当然,这有很多种可行的方法解决这个问题,但一个重要的方法是在你的驱动中自己构造IRP。这个
IRP包含一个I/O操作在任意上下文中完成所需要的任何信息。IRPs依赖于在任意上下文中都有效的
FILE_OBJECTs和DEVICE_OBJECTs,而不是只在特定进程上下文中有效的Fille Handle。这篇文章描述和示
范了很多方法,每一种方法都是基于DDK的,你不必使用没有文档化的Kernels APIs。
有很多原因使你需要构建一个IRPs。可能的原因有与文件系统交互执行一个文件的I/O操作,或者是
文件系统支持的内核特征。也许你的驱动要增加一个存在设备的功能,如处理NT容错设备(FTDISK).也许
你有两个协作的驱动需要要它们之间通信。或者也许是你在Windows NT上执行一个物理文件系统需要和
Media Driver或Transport Driver交互。不管什么原因,在Windows NT下完成这些任务最好的方法是自己
构建IPRs。
Allocation
可以用两种方式分配IRPs。最简单的方法是调用IoAllocateIrp(...)。I/O管理器将用适当的I/O栈单
元(你在调用中指定的)分配一个IPR,这是最简单的并且是大多数分配I/O请求的方法。警告注意:如何你
调用IoAllocateIrp(...)不用调用IoInitializeIrp(...)。在这点上DDK文档导致许多无辜的受害者误入
歧途。
你的驱动可能想要创建自己的IRPs。一个方法是用ExAllocatePool(...)在非分页内存中分配内存。
你通过IoInitializeIrp(...)初始化IRP的形式。你的驱动可以通过IoSizeOfIrp(...)计算出分配的IRP的
大小。只要你在非分页内存中分配了一个IRP,你就能调用IoInitializeIrp设置IRP中的域。请看下面的
例子:
PIRP MyAllocateIrp(CCHAR NumberOfStackLocations) {
USHORT IrpSize = IoSizeOfIrp(NumberOfStackLocations);
Irp = ExAllocatePool(NonPagedPool, IrpSize);
if (!Irp) {
return 0; // failureContent provided by OSR Open Systems Resources, Inc.
}
IoInitializeIrp(Irp, IrpSize, NumberOfStackLocations);
return Irp;
}
一般地,驱动依赖于I/O管理器分配和管理IRPs。然而这些实例使你的驱动能更有效的分配和管理自
己的IRPs。
当NT启动的时候,I/O管理器建立两个旁视列表:一个对应于一个I/O栈单元的IPRs,另一个对应于四
个I/O栈单元的IPRs。当你分配大栈单元的IRPs或旁视列表为空时,I/O管理器在非分页内存中分配新的
IRPs。如果你知道你将使用的IRPs多于四个栈单元,你能通过在你自己的自由列表中保持IRPs提高一些效
率。换句话说,你的驱动在第一次运行时能创建一个IRPs池并且保存它在一个私有列表中。当你的驱动需
要IRP时能在这个列表中分配,当I/O操作完成时能返回到这个列表中,从而消除了间接的分配和释放内存
池。我们已经在OSR网站上的"roll.c"展示了这方面的例子代码。
你怎么确信I/O管理器将IRP返回到你的驱动让它能返回到你的旁视列表中?简单的方法是使用I/O完
成例程。下面展示了I/O完成例程如何实际的工作即使你驱动创建的IRP没有栈单元也能注册一个完成例程
。
这是为什么?如果你考虑一下完成例程如何使用,你就会认识到底层驱动被调用不需要完成例程(毕
竟,这将是驱动完成I/O请求).所以,底层的I/O栈单元可以用来存储下层驱动的完成例程。继续这个处理
直到驱动的顶层将给我们一个能把握的额外的完成例程,这个例程对原始的IRP创建者是有效的。
对于IRP的创建者,设置I/O完成例程等同于中间层驱动的完成例程(简单的调用
IoSetCompletionRoution(...)).我们将在这篇文章在后面描述如何构建完成例程。
Building
只要你分配了自己的IRP,不管是使用IoAllocateIrp还是从自己的旁视列表中,你必须初始化I/O请
求指示你需要低层驱动的什么服务。当你调用下层驱动执行的时候你的驱动必须做同样的工作。简单的设
置下层驱动的参数块。你的驱动要做的额外工作是初始化自己分配的IPR的其他域。
MdlAddress
this field will point to the MDL containing the data (if any)
Flags
any appropriate flags (c.f., ntddk.h for the IRP_ flags)
AssociatedIrp.SystemBuffer
any data buffer for this I/O request
RequestorMode
UserMode or KernelMode. Typically, this is UserMode if the arguments being passed should be
validated, KernelMode otherwise.
UserBuffer
any data buffer for this I/O request
Tail.Overlay.Thread the PETHREAD for the original requestor
当然,其中的某些域对于你的I/O操作可是不是必须的(如:MdlAddress,AoosciatedIrp.SystemBuffer,
和UserBuffer参数,仅仅是它们中的一个可能被你的驱动使用).当然,你将用到的域用因I/O操作不同而
变化。
Tail.Overlay.Thread数据结构仅仅用在明确的设备中,如可移动设备,所以系统知道如何操作"错误
的弹出"如当媒体设备没有在驱动中加载的时候abort/retry/cancel对话框的出现。
IRP有很多不同的标志,告诉下层驱动如何解释这个I/O请求的内容。
-IRP_NOCA CHE – data for this I/O request should be read from the actual backing
media and not from cache.
-IRP_PAGING_IO – the I/O operation in question is performing paging I/O. This bit
is used by the Memory Manager.
-IRP_MOUNT_COMPLETION – the I/O operation in question is performing
a mount
operation.
-IRP_SYNCHRONOUS_API – the API in question expects synchronous
behavior. While synchronous behavior is advised when this bit is set, it is not
required.
-IRP_ASSOCIATED_IRP – the IRP in question is associated wit h some larger I/O
operation.
-IRP_BUFFERED_IO – the AssociatedIrp.SystemBuffer field is valid
-IRP_DEALLOCATE_BUFFER – the system buffer was allocated from pool and
should be deallocated by the I/O Manager.
-IRP_INPUT_OPERATION – the I/O operation is fo r input. This is used by
the Memory
Manager to indicate a page in operation.
-IRP_SYNCHRONOUS_PAGING_IO – the paging operation should complete synchronously.
This bit is used by the Memory Manager.
-IRP_CREATE_OPERATION – the IRP represents a file system create operation.
-IRP_READ_OPERATION – the IRP represents a read operation.
-IRP_WRITE_OPERATION – the IRP represents a write operation.
-IRP_CLOSE_OPERATION – the IRP represents a close operation.
-IRP_DEFER_IO_COMPLETION – the IRP should be process ed asynchronously.
While asynchronous behavior is advised when this bit is se, it is not required.
小心的使用这些标志设置IPR,因为这些标志将对下层驱动处理IRP请求产生一个根本的影响。
如上所述,你的驱动还应该设置下一个I/O栈单元。它仅仅发生的处理第一个栈单元的时候。第一个
栈单元的指针通过IoGetNextIrpStackLocation(...)得到。这个调用返回下一个将要调用的驱动的栈单元
你的驱动要负责初始化这个栈单元的下面这个域:
MajorFunction
the function code for the I/O to be performed
MinorFunction
a minor function code for the I/O. This field should be zero if there is no minor
function code.
Flags
any flags needed to modify the behavior of the I/O operation
(c.f., ntddk.h for the SL_* flags.)
DeviceObject
the device to which your driver will pass the IRP.
标志域用来修改低层驱动的行为当处理I/O请求的时候。可能的标志如下面所示:表3未画
最后,你的驱动必须初始化I/O操作指定的参数。如Read Write请求,需要初始化偏移,长度等。
Completion
有时候,当你构建自己的IRPs时你需要提供一个完成例程。完成例程的特定规则是不明确的。但是如
果你错误的使用它将导致系统崩溃。
提供完成例程最重要的原因是你可以重新使用I/O操作。在DDK文档中提到的不太重要的原因是你要释
放它。这排除了I/O管理器需要执行I/O完成例程,你通过在你的完成例程中返回
STATUS_MORE_PROCESSING_REQUIRED告诉I/O管理器停止继续向上返回。
什么时候你不需要使用一个完成例程?当你不需要考虑I/O操作的完成状态,或者你不能在你的完成
例程中释放IRP。后面的情况没有在DDK文档中指出但是非常重要。典型的,I/O管理器为一个线程创建一
个I/O操作,这个IRP存储在线程的链表中(ThreadListEntry域).当线程退出的时候允许NT清除IRP如果你
的驱动有一个完成例程并返回STATUS_MORE_PROCESSING_REQUIRED,这个IRP可能仍然存放于线程的I/O列
表中,这可能产生严重的问题。已经证明I/O管理器的某些函数将创建的IRP添加到线程链表中,而其它的
函数不添加。那么,当构建你的完成例程时,这将是一个好机会检查你的IRP不在线程列表中!
只要你的完成例程返回STATUS_MORE_PROCESSING_REQUIRED,I/O管理器将停下来等待更多的处理。那
么,你就可以做你想做的任何事情,此时你在I/O操作开始时的线程上下文中,文件句柄和用户地址都是
不必须的。第二点,你不用假设你的完成例程在PASSIVE_LEVEL被调用,它可能在DISPATCH_LEVEL级被调
用,原因是你完成例程的驱动可能是一个DPC例程。将这些记在脑子里,当你设计你自己的完成例程时,
如果你需要在IRP完成后有更多的处理你可能需要设置一个工作例程确保它安全。
Reuse
我们描述了你的驱动如何在旁视列表中保持IRPs。当你不在需要IRP时你的驱动可以在完成例程中设
置将它返回到你的旁视列表中。然而,你可能需要做些额外的工作在IRP准备重用的时候。
例如,如果你调用文件系统驱动并指定了一个用户缓冲区(通过设置Irp->Userbuffer),文件系统驱
动可以创建一个MDL描述这个缓冲区。如果是那样,因为你在负责清除IRP,所以你有责任
unmapping,unlocking并且释放与IRP关联的MDL。可以通过MmGetSystemAddressForMdl(...)得到MDL的地
址,通过MmUnmapLockedPages(...)解除映射并通过MmUnlockPages(...)解除锁定页。
Short-cuts
现在我们已经描述了如何构建自己的IRPs,我们将提到I/O管理器提供的三个短小的调用来使你容易
的完成这些。这些I/O管理器提供的函数不如你自己构建IPRs的通用性好,但它们能快速的构建一个IPR并
且你能在你的驱动中完成初始化工作,这些例程是:
- IoBuildAsynchronousFsdRequest(...)
- IoBuildSynchronousFsdRequest(...)
- IoBuildDeviceIoControlRequest(...)
这三个调用都没有初始化FileObject参数,因此如果你调用文件系统驱动,你的驱动需要设置这个域。既
然你知道如何在你的驱动中构建IRP,你能扩充I/O管理器创建的IPRs来适用于你自己使用。
前面提到的,在I/O管理器的帮助函数中使用完成例程不是那么容易。在
IoBuildSynchronousFsdRequset(..)和IoBuildDeyiceIoControlRequest(...)中你不能在你的完成例程中
释放IRPs,但是在IoBuildAsynchronousFsdRequest(...)中你可以这样做。这是因为前面的两个例程把
IRP增加到线程的IRP链表中。因为I/O管理器不会在线程中移除IRP,所以唯一的选择是允许完成请求。
用上面三个函数中的任何一个都可以简单的创建一个IPPs,但是对于你的驱动的请求所需要的帮助例
程仍有限制。IoBuildDeviceIoControlRequest仅用来构建IRP_MJ_DEVICE_CONTROL请求,
IoBuildSynchronousFsdRequset(...)仅用来支持IRP_MJ_INTERNAL_DEVICE_CONTROL,
IoBuildAsynchronousFsdRequest(...)仅对IRP_MJ_READ,IRP_MJ_WRITE,IRP_MJ_FLUSH_BUFFERS和
IRP_MJ_SHUTDOWN有效。
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课