https://msdn.microsoft.com/zh-cn/library/windows/hardware/Dn614012(v=vs.85).aspx
- 一个I/O 跨越一段虚拟地址范围的I/O 缓冲区可以被扩展到几个物理页面,这些页面可以是不连续的。操作系统使用内存描述链表(MDL)来描述一段虚拟地址对应的物理页面的布局。
MDL 包含一个 MDL 结构,后面跟着一个数组,描述了I/O 缓冲区所在的物理内存。MDL 的大小是可变的,取决于其所描述的I/O 缓冲区的大小。可以通过调用系统函数来计算所需要的MDL 的大小并申请和释放MDL。
MDL 结构是半透明的,驱动只可以直接使用它的Next 和 MdlFlags 成员,后面有使用的示例代码。
剩下的成员是透明的,不要直接访问透明的成员。相反,我们应该使用如下函数来执行MDL 的基础操作。
MmGetMdlVirtualAddress 返回MDL 所描述的I/O 缓冲区的虚拟地址
- MmGetMdlByteCount 返回I/O 缓冲区的大小(字节单位)
MmGetMdlByteOffset I/O 缓冲区的开始的位置在其对应的物理页面的偏移。
调用IoAllocateMdl 申请MDL,IoFreeMdl 释放MDL。或者,我们可以申请一块非分页内存并通过调用MmInitializeMdl 将其格式化为一个MDL。
无论是IoAllocateMdl 和 MmInitializeMdl 都初始化在MDL 结构后紧跟着的数据数组。对于驻留在由驱动申请的非分页内存中的MDL 结构,使用MmBuildMdlForNonPagedPool 来初始化这个数组以描述I/O 缓冲区所驻留的物理内存。
对于可分页内存来说,虚拟内存和物理内存之间的关系是临时的,因此,直到在确定的环境下,MDL 后面的数据数组才是合法的。调用MmProbeAndLockPages 来为当前的布局锁定可分页内存,并除初始化MDL 结构后的数据数组。直到调用MmUnlockPages ,内存将不会被置换出内存,一旦调用这个函数,MDL 后的数据数组不再有效。
如果MDL描述的物理页面 尚未被映射的话, MmGetSystemAddressForMdlSafe 函数将映射它们 到系统地址空间的虚拟地址,这个虚拟地址对于驱动是有用的,当驱动可能需要查看将要执行I/O 操作的页面,因为原来的虚拟地址可能是用户地址,它只能用在原来的环境中而且随时可能被删除。
当我们调用IoBuildPartialMdl 建立一个部分的MDL 时,调用者使用MmGetMdlVirtualAddress 而不是MmGetSystemAddressForMdlSafe 函数 当决定将要传入的虚拟地址的时候。IoBuildPartialMdl 使用MmGetMdlVirtualAddress 函数从源MDL 返回的地址来判断目标MDL 的偏移。如果地址不同,传入MmGetSystemAddressForMdlSafe 返回的地址可能导致数据损坏或bug check。???不是很懂啊
当驱动调用IoAllocateMdl,可以通过将一个IRP 的指针作为IoAllocateMdl 的Irp 参数来将一个IRP 与新申请的MDL 绑定。一个IRP 可以有一个或者多个MDL 绑定。如果IRP 仅有一个MDL 与其绑定,IRP的MdlAddress 成员指针指向这个绑定的MDL。如果IRP 有多个MDL 绑定,MdlAddress 指向对应的一个MDL 链表中的第一个MDL结构,这些MDL 是通过其Next 指针链接在一起的,链表中最后一个MDL 的Next 指针为NULL。
当调用IoAllocateMdl 时,如果SecondaryBuffer 传FALSE,IRP 的MdlAddress 成员被设置为指向新建的MDL。如果传TRUE,函数将新生成的MDL 插入到MDL 链表的末尾。
当IRP 完成,系统解锁并释放绑定的MDL,系统在MDL 排队等候I/O 完成函数之前解锁MDL,在I/O 完成函数执行之前释放它们。
驱动可以通过使用MDL 结构中的Next 成员,以访问链表中的下一个MDL最终完成转换MDL 链表。驱动可以通过更新Next 成员来手动插入MDL。
MDL 链表通过用来管理与单个I/O 请求绑定的一个缓冲区数组。(例如,在一个网络操作中,一个网络驱动可以使用一个Ip 包 对应一个缓冲区。)数组中的每一个缓冲区都对应一个MDL 链中的一个MDL。驱动完成请求之后,合并缓冲区到一个单独的大的缓冲中。系统之后自动清理所有为这个请求申请的MDL。
I/O 管理器是I/O 请求的常客。当I/O 管理器完成一个I/O 请求,I/O 管理器释放IRP 并释放绑定的MDL。这些MDL 中的某些可能已经被设备栈中比I/O 管理器位置更低的驱动附载到一个IRP。类似的,如果你的驱动是一个I/O 请求的源,你的驱动必须在I/O 请求完成的时候清理IRP 和 被附载到IRP 上的MDL。
示例代码
下面的示例代码为一个驱动实现函数,从高一个IRP释放一个MDL 链表。
VOID MyFreeMdl(PMDL Mdl)
{
PMDL CurrentMdl,NextMdl;
for(CUrrentMdl = Mdl;CurrentMdl != NULL; CurrentMdl = NextMdl)
{
NextMdl = CurrentMdl->Next;
if(CurrentMdl->MdlFlags & MDL_PAGES_LOCKED)
{
MmUnlockPages(CurrentMdl);
}
IoFreeMdl(CurrentMdl);
}
}
}
- 如果被描述的物理页面是锁定的,在调用IoFreeMdl 释放MDL前调用MmUnlockPages 解锁页面。尽管如此,示例函数不必在调用IoFreeMdl 前显式取消映射页面。取而代之,IoFreeMdl 在shifangMDL 的时候自动取消映射。
MDL 是系统定义的结构,其通过一个物理地址的集合来描述一个缓冲区。执行直接I/O 的驱动从I/O 管理器接收到了一个指向MDL 的指针,并通过MDL 来进行读写内存。一些驱动使用MDL 来执行直接I/O 以满足设备I/O 控制请求。
驱动程序开发者不应该假设MDL 所描述的页面的顺序或者内容。驱动不应该依赖被MDL 引用的任何位置的数据的值,并且不能直接通过解引用一个内存位置来得到数据。如果MDL 描述了一个直接I/O 操作对应的缓冲区,发起I/O 请求的应用程序可能同时映射了相同的物理地址到它的地址空间中,如果是这样的话,应用和驱动可能同时修改数据,从而导致错误产生。
更进一步,某些情况下,某些情况下,MDL 的位置不引用内存管理器保留的同样的物理地址。当Microsoft windows 内存管理器为了设备读而构建一个MDL 的时候,它为了传输的目的锁定物理页面。尽管如此,决定哪个页面去锁定以及哪个页面被丢弃仅仅是内存管理器的责任。为了内存管理器将数据读取到这些页面然后又丢弃它们。因为在更大的集群中执行I/O 操作有更好的性能。
例如,下图所示,文件偏移和虚拟地址与页面A,Y,Z 绑定,B 页面是逻辑邻居,但是物理页面自己并不一定是邻居。页面A 和 B 当前没有在内存中,因此内存管理器必须读取他们。页面Y 和 Z 已经在内存中了,没有必要读取它们。实际上,他们可能已经被修改了,自从它们最后从它们的后备存储中被读取进来到内存中,这种情况下,重写它们的内容可能导致一系列的问题。尽管如此,在一个单独的操作中读取A 和 B 页面相比 读取A 然后 读取B 是更有效率的。因此,内存管理器从后备存储中产生一个单一的读取请求包含了所有的页面(AYZB)。这样的读取请求包含尽量多的对于读请求有意义的页面,基于可获得的内存的数量,当前系统使用的内存的状况等等。
当内存管理器为了一个请求建立MDL 的时候,它提供了A 和 B 的合法指针。尽管如此,页面Y 和 Z 的项指向一个单一的system-wide 虚拟页面X。内存管理器可以使用后备存储的潜在数据来填充虚拟页面X 因为它并不使X 可视。尽管如此,如果一个组件访问MDL 中的Y 和 Z 的偏移,它看见假的页面X 而不是 Y 和 Z。
内存管理器可以代表任意数量的被释放的页面为一个单一的页面,这个页面可以被多次植入到同一个MDL 甚至多个同时存在的不同驱动使用的MDL。总的来说,被抛弃的页面的位置的内容可以随时改变。
执行解码或者计算验证和的驱动,它们的操作是基于MDL 映射的页面的数据的值的,它们不可以从系统提供的MDL 解引用指针以访问数据(可能被释放掉之类的操作)。取而代之,为了确保正确的操作,这样的驱动应该创建一个临时的MDL,这个MDL 基于驱动从I/O 管理器接收到的系统提供的MDL(锁定它的内容,确保其内容的同时确保其不会被置换到后备文件中)。创建临时的MDL 可按照如下方法:
- 调用MmGetMdlVirtualAddress 和 MmGetMdlByteCount 得到虚拟地址基地址和系统提供的的MDL 的长度。
- 调用ExAllocatePoolWithTag,PoolType = NonPagedPool,从非分页内存中申请一块内存。指定缓冲区大小为系统提供的MDL 的长度,向上页面对齐。
- 调用IoAllocateMdl 申请一个MDL 传入步骤2中 申请得到的虚拟地址的起始地址以及缓冲区的长度。
- 调用MmBuildMdlForNonPagedPool 更新临时MDL,之后它就描述了步骤2 中申请的缓冲区底层的物理页面。
在从硬件读取数据的时候,驱动应该传入这个临时MDL ,然后在任何需要操作的地方,使用这个临时MDL 所描述的数据的值。通过调用MmBuildMdlForNonPagedPool 来更新这个临时MDL,驱动确保这个临时MDL不可能有任何的临时页面,因此避免了页面内容的任何可能的修改。通过这种方式,甚至系统的MDL 包含可能被置换的页面,驱动避免了检查不稳定的内容。驱动完成操作之后,应该通过tyr/except 或 try/finally 块内调用RtlCopyMemory 从临时MDL 块拷贝被修改的数据到系统提供的MDL。
在典型的I/O 操作中使用MDL 但是没有底层页面的数据的驱动没有必要创建临时MDL。在内部,内存管理器一直跟踪被保留的所有的页面,并监控它们如何被映射。当一个驱动将一个MDL 传给系统函数以执行I/O 操作,内存管理器其使用的是正确的数据。
开发人员应该做什么?
- 任何时间都不要假设MDL 所指定的任何内存位置的内容是合法的。
如果你的驱动取决于数据的内容的话,为系统提供的MDL 建立一个双层的缓冲区数据。
内核驱动调用地址映射与MDL 管理函数来管理地址映射和MDL。
PMDL
IoAllocateMdl(
IN PVOID VirtualAddress,
IN ULONG Length,
IN BOOLEAN SecondaryBuffer,
IN BOOLEAN ChargeQuota,
IN OUT PIRP Irp OPTIONAL
);
申请一个足够大的内存来映射调用者指定的起始的地址和长度。绑定可选的一个IRP结构。
VOID
IoBuildPartialMdl(
IN PMDL SourceMdl,
IN OUT PMDL TargetMdl,
IN PVOID VirtualAddress,
IN ULONG Length
);
从一个给定的MDL中建立一个指定起始地址和长度的MDL。驱动可以通过调用这个函数来将一个大的地址范围转换为一系列的小的传输包。
VOID
IoFreeMdl(
IN PMDL Mdl
);
释放一个调用者给定的MDL。
MmAllocatePagesForMdlEx (
__in PHYSICAL_ADDRESS LowAddress,
__in PHYSICAL_ADDRESS HighAddress,
__in PHYSICAL_ADDRESS SkipBytes,
__in SIZE_T TotalBytes,
__in MEMORY_CACHING_TYPE CacheType,
__in ULONG Flags
);
为MDL申请非分页的,物理内存页。
VOID
MmBuildMdlForNonPagedPool (
__inout PMDL MemoryDescriptorList
);
传入的MDL 必须已经有了非分页池中的一个地址范围。该函数填充该MDL 的对应的物理地址。
PMDL
MmCreateMdl (
__in_opt PMDL MemoryDescriptorList,
__in_bcount_opt(Length) PVOID Base,
__in SIZE_T Length
);
申请并初始化一个MDL 描述调用者给定的虚拟地址和长度,返回一个MDL 的指针。
#define MmGetMdlByteCount(Mdl) ((Mdl)->ByteCount)
#define MmGetMdlByteOffset(Mdl) ((Mdl)->ByteOffset)
#define MmGetMdlVirtualAddress(Mdl) \
((PVOID) ((PCHAR) ((Mdl)->StartVa) + (Mdl)->ByteOffset))
PHYSICAL_ADDRESS
MmGetPhysicalAddress (
__in PVOID BaseAddress
);
返回给定的合法的虚拟地址对应的物理地址。
#define MmGetSystemAddressForMdlSafe(MDL, PRIORITY) \
(((MDL)->MdlFlags & (MDL_MAPPED_TO_SYSTEM_VA | \
MDL_SOURCE_IS_NONPAGED_POOL)) ? \
((MDL)->MappedSystemVa) : \
(MmMapLockedPagesSpecifyCache((MDL), \
KernelMode, \
MmCached, \
NULL, \
FALSE, \
(PRIORITY))))
为设备必须使用编程的I/O 的程序,返回映射了给定的MDL 所描述的物理页面的一个系统空间的虚拟地址,如果没有虚拟地址存在,就分配指定一个。
返回值就是对应的系统地址空间地址。
#define MmInitializeMdl(MemoryDescriptorList, BaseVa, Length)
初始化一个调用者创建的MDL,以描述给定的虚拟地址和长度。
BOOLEAN
MmIsAddressValid (
__in PVOID VirtualAddress
);
返回当在传入的地址进行一个读写操作时是否会产生异常。
NTKERNELAPI
__out_bcount(NumberOfBytes) PVOID
MmMapIoSpace (
__in PHYSICAL_ADDRESS PhysicalAddress,
__in SIZE_T NumberOfBytes,
__in MEMORY_CACHING_TYPE CacheType
);
映射一个物理地址范围到一个缓存的或者非缓存的虚拟地址范围到一个非分页的地址空间。
PVOID
MmMapLockedPages (
__in PMDL MemoryDescriptorList,
__in KPROCESSOR_MODE AccessMode
);
映射MDL 描述的物理页面到一个系统虚拟地址空间,或者虚拟地址空间的用户部分。
MemoryDescriptorList 提供一个合法的MDL ,该MDL 应该已经被MmProbeAndLockPages 更新
AccessMode - 提示映射该页面到哪里。KernelMode 映射到系统空间, UserMode 映射到用户地址空间
```
PVOID
MmMapLockedPagesWithReservedMapping (
__in PVOID MappingAddress,
__in ULONG PoolTag,
__in PMDL MemoryDescriptorList,
__in MEMORY_CACHING_TYPE CacheType
)
“`
MappingAddress - 由MmAllocateMappingAddress 申请得到的合法的映射地址
__bcount(NumberOfBytes) PVOID
MmAllocateMappingAddress (
__in SIZE_T NumberOfBytes,
__in ULONG PoolTag
);
申请一个给定长度的系统PTE 映射,可以在之后用于任何地址的映射。
NumerOfBytes 可以跨越的最大的长度
PoolTag 不为空,调用者必须指定自己的身份。
#define MmPrepareMdlForReuse(MDL) \
if (((MDL)->MdlFlags & MDL_PARTIAL_HAS_BEEN_MAPPED) != 0) { \
ASSERT(((MDL)->MdlFlags & MDL_PARTIAL) != 0); \
MmUnmapLockedPages( (MDL)->MappedSystemVa, (MDL) ); \
} else if (((MDL)->MdlFlags & MDL_PARTIAL) == 0) { \
ASSERT(((MDL)->MdlFlags & MDL_MAPPED_TO_SYSTEM_VA) == 0); \
}
重新初始化一个调用者创建的MDL 以重新利用。做必要的动作以满足重新使用MDL的目的。
MmProbeAndLockPages (
__inout PMDL MemoryDescriptorList,
__in KPROCESSOR_MODE AccessMode,
__in LOCK_OPERATION Operation
);
预读给定的页面,使页面驻留内存锁定虚拟页面对应的物理页面到内存中。
MDL 被更新以描述这些物理页面。
NTKERNELAPI
NTSTATUS
MmProtectMdlSystemAddress (
__in PMDL MemoryDescriptorList,
__in ULONG NewProtect
);
修改内存地址范围的保护属性。
HANDLE
MmSecureVirtualMemory (
__in_bcount(Size) PVOID Address,
__in SIZE_T Size,
__in ULONG ProbeMode
);
保护一个内存地址范围,使它不能被释放,它的保护属性不能被设置更严格。
SIZE_T
MmSizeOfMdl (
__in_bcount_opt(Length) PVOID Base,
__in SIZE_T Length
)
{
return( sizeof( MDL ) +
(ADDRESS_AND_SIZE_TO_SPAN_PAGES( Base, Length ) *
sizeof( PFN_NUMBER ))
);
}
VOID
MmUnlockPages (
__inout PMDL MemoryDescriptorList
);
解锁MDL 描述的物理页面
VOID
MmUnmapIoSpace (
__in_bcount(NumberOfBytes) PVOID BaseAddress,
__in SIZE_T NumberOfBytes
);
解映射之前通过MmMapIoSpace 函数映射的物理地址的范围。
VOID
MmUnmapLockedPages (
__in PVOID BaseAddress,
__in PMDL MemoryDescriptorList
);
解映射之前通过MmMapLockedPages 映射的被锁定的物理页面。
VOID
MmUnmapReservedMapping (
IN PVOID BaseAddress,
IN ULONG PoolTag,
IN PMDL MemoryDescriptorList
);
解除之前通过MmMapLockedPagesWithReservedMapping 函数映射的被锁定的页面。
VOID
MmUnsecureVirtualMemory (
__in HANDLE SecureHandle
);
MmSecureVirtualMemory 逆操作。