驱动程序访问用户模式数据缓冲区的三种方式

来源:互联网 发布:华为定时开关机软件 编辑:程序博客网 时间:2024/06/15 04:16

当应用程序发起一个读或写操作时,通过给出一个用户模式虚拟地址和长度,应用程序向I/O管理器提供了一个数据缓冲区。正如我在第三章中提到的,内核模式驱动程序几乎从不使用用户模式虚拟地址访问内存,因为你不能把线程上下文确定下来。Windows 2000为驱动程序访问用户模式数据缓冲区提供了三种方法:

  • buffered方式中,I/O管理器先创建一个与用户模式数据缓冲区大小相等的系统缓冲区。而你的驱动程序将使用这个系统缓冲区工作。I/O管理器负责在系统缓冲区和用户模式缓冲区之间复制数据。
  • direct方式中,I/O管理器锁定了包含用户模式缓冲区的物理内存页,并创建一个称为MDL(内存描述符表)的辅助数据结构来描述锁定页。因此你的驱动程序将使用MDL工作。
  • neither方式中,I/O管理器仅简单地把用户模式的虚拟地址传递给你。而使用用户模式地址的驱动程序应十分小心。

d

指定缓冲方式

为了指定设备读写的缓冲方式,你应该在AddDevice函数中,在创建设备对象后,立即设置其中的标志位:

NTSTATUS AddDevice(...){  PDEVICE_OBJECT fdo;  IoCreateDevice(..., &fdo);  fdo->Flags |= DO_BUFFERED_IO;           <or>  fdo->Flags |= DO_DIRECT_IO;           <or>  fdo->Flags |= 0; // i.e., neither direct nor buffered}

这之后你不能该变缓冲方式的设置,因为过滤器驱动程序将复制这个标志设置,并且,如果你改变了设置,过滤器驱动程序没有办法知道这个改变。

Buffered方式

当I/O管理器创建IRP_MJ_READ或IRP_MJ_WRITE请求时,它探测设备的缓冲标志以决定如何描述新IRP中的数据缓冲区。如果DO_BUFFERED_IO标志设置,I/O管理器将分配与用户缓冲区大小相同的非分页内存。它把缓冲区的地址和长度保存到两个十分不同的地方,见下面代码片段中用粗体字表示的语句。你可以假定I/O管理器执行下面代码(注意这并不是Windows NT的源代码):

PVOID uva;             //  user-mode virtual buffer addressULONG length;          //  length of user-mode bufferPVOID sva = ExAllocatePoolWithQuota(NonPagedPoolCacheAligned, length);if (writing)  RtlCopyMemory(sva, uva, length);Irp->AssociatedIrp.SystemBuffer = sva; PIO_STACK_LOCATION stack = IoGetNextIrpStackLocation(Irp);if (reading)  stack->Parameters.Read.Length = length; else  stack->Parameters.Write.Length = length; <code to send and await IRP>if (reading)  RtlCopyMemory(uva, sva, length);ExFreePool(sva);

可以看出,系统缓冲区地址被放在IRP的AssociatedIrp.SystemBuffer域中,而数据的长度被放到stack->Parameters联合中。在这个过程中还包含作为驱动程序开发者不必了解的额外细节。例如,读操作之后的数据复制工作实际发生一个APC期间,在原始线程的上下文中,由一个与构造该IRP完全不同的子例程执行。I/O管理器把用户模式虚拟地址(uva变量)保存到IRP的UserBuffer域中,这样一来复制操作就可以找到这个地址。但你不要使代码依赖这些事实,因为它们有可能会改变。IRP最终完成后,I/O管理器将释放系统缓冲区所占用的内存。

Direct方式

如果你在设备对象中指定DO_DIRECT_IO方式,I/O管理器将创建一个MDL用来描述包含该用户模式数据缓冲区的锁定内存页。MDL结构的声明如下:

typedef struct _MDL {  struct _MDL *Next;  CSHORT Size;  CSHORT MdlFlags;  struct _EPROCESS *Process;  PVOID MappedSystemVa;  PVOID StartVa;  ULONG ByteCount;  ULONG ByteOffset;} MDL, *PMDL;

图7-3显示了MDL扮演的角色。StartVa成员给出了用户缓冲区的虚拟地址,这个地址仅在拥有数据缓冲区的用户模式进程上下文中才有效。ByteOffset是缓冲区起始位置在一个页帧中的偏移值,ByteCount是缓冲区的字节长度。Pages数组没有被正式地声明为MDL结构的一部分,在内存中它跟在MDL的后面,包含用户模式虚拟地址映射为物理页帧的个数。

 d

图7-3. 内存描述符表(MDL)结构

顺便说一下,我们不可以直接访问MDL的任何成员。应该使用宏或访问函数,见表7-2。

表7-2. 用于访问MDL的宏和访问函数

宏或函数描述IoAllocateMdl创建MDLIoBuildPartialMdl创建一个已存在MDL的子MDLIoFreeMdl销毁MDLMmBuildMdlForNonPagedPool修改MDL以描述内核模式中一个非分页内存区域MmGetMdlByteCount取缓冲区字节大小MmGetMdlByteOffset取缓冲区在第一个内存页中的偏移MmGetMdlVirtualAddress取虚拟地址MmGetSystemAddressForMdl创建映射到同一内存位置的内核模式虚拟地址MmGetSystemAddressForMdlSafe与MmGetSystemAddressForMdl相同,但Windows 2000首选MmInitializeMdl(再)初始化MDL以描述一个给定的虚拟缓冲区MmPrepareMdlForReuse再初始化MDLMmProbeAndLockPages地址有效性校验后锁定内存页MmSizeOfMdl取为描述一个给定的虚拟缓冲区的MDL所占用的内存大小MmUnlockPages为该MDL解锁内存页

对于I/O管理器执行的Direct方式的读写操作,其过程可以想象为下面代码:

KPROCESSOR_MODE mode;   //  either KernelMode or UserModePMDL mdl = IoAllocateMdl(uva, length, FALSE, TRUE, Irp);MmProbeAndLockPages(mdl, mode, reading ? IoWriteAccess : IoReadAccess); <code to send and await IRP>MmUnlockPages(mdl);ExFreePool(mdl);

I/O管理器首先创建一个描述用户缓冲区的MDL。IoAllocateMdl的第三个参数(FALSE)指出这是一个主数据缓冲区。第四个参数(TRUE)指出内存管理器应把该内存充入进程配额。最后一个参数(Irp)指定该MDL应附着的IRP。在内部,IoAllocateMdl把Irp->MdlAddress设置为新创建MDL的地址,以后你将用到这个成员,并且I/O管理器最后也使用该成员来清除MDL。

这段代码的关键地方是调用MmProbeAndLockPages(以粗体字显示)。该函数校验那个数据缓冲区是否有效,是否可以按适当模式访问。如果我们向设备写数据,我们必须能读缓冲区。如果我们从设备读数据,我们必须能写缓冲区。另外,该函数锁定了包含数据缓冲区的物理内存页,并在MDL的后面填写了页号数组。在效果上,一个锁定的内存页将成为非分页内存池的一部分,直到所有对该页内存加锁的调用者都对其解了锁。

Direct方式的读写操作中,对MDL你最可能做的事是把它作为参数传递给其它函数。例如,DMA传输的MapTransfer步骤需要一个MDL。另外,在内部,USB读写操作总使用MDL。所以你应该把读写操作设置为DO_DIRECT_IO方式,并把结果MDL传递给USB总线驱动程序。

顺便提一下,I/O管理器确实在stack->Parameters联合中保存了读写请求的长度,但驱动程序应该直接从MDL中获得请求数据的长度。

ULONG length = MmGetMdlByteCount(mdl);

Neither方式

如果你在设备对象中同时忽略了DO_DIRECT_IO和DO_BUFFERED_IO标志设置,你将得到默认的neither方式。对于这种方式,I/O管理器将简单地把用户模式虚拟地址和字节计数(以粗体显示的代码)交给你,其余的工作由你去做。

Irp->UserBuffer = uva; PIO_STACK_LOCATION stack = IoGetNextIrpStackLocation(Irp);if (reading)  stack->Parameters.Read.Length = length; else  stack->Parameters.Write.Length = length; <code to send and await IRP>

原创粉丝点击