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

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

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

指定缓冲方式

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

NTSTATUS AddDevice(...)
{
  PDEVICE_OBJECT fdo;
  IoCreateDevice(..., &fdo);
  fdo->Flags |= DO_BUFFERED_IO;
           
  fdo->Flags |= DO_DIRECT_IO;
           
  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 address
ULONG length;          //  length of user-mode buffer

PVOID 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; 



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的后面,包含用户模式虚拟地址映射为物理页帧的个数。

 

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

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

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

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

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

KPROCESSOR_MODE mode;   //  either KernelMode or UserMode
PMDL mdl = IoAllocateMdl(uva, length, FALSE, TRUE, Irp);
MmProbeAndLockPages(mdl, mode, reading ? IoWriteAccess : IoReadAccess); 



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; 

你可能感兴趣的:(杂文,嵌入式编程)