MDL 中实际包含的内容是什么?
内存描述符列表 (MDL) 是一个系统定义的结构,通过一系列物理地址描述缓冲区。执行直接 I/O 的驱动程序从 I/O 管理器接收一个 MDL 的指针,并通过 MDL 读写数据。一些驱动程序在执行直接 I/O 来满足设备 I/O 控制请求时也使用 MDL。
驱动程序编写人员不应该假设 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 进行第二次读取更有效。因此,内存管理器发出一个包含所有 4 个页(A、Y、Z 和 B)的单个从备份存储区读取的请求。这种读取请求包含对于读取有意义的任意多页(依赖于可用内存的量、当前系统使用情况等)。
当内存管理器构建描述请求的内存描述符列表 (MDL) 时,它提供页 A 和 B 的有效指针。但是,页 Y 和 Z 的条目指向单个系统范围的虚拟页 X。内存管理器可能会使用来自备份存储区的潜在的过时数据填充虚拟页 X(因为它使得 X 不可见)。但是,如果组件访问 MDL 中的 Y 和 Z 偏移,那么它看到的是虚拟页 X 而不是 Y 和 Z。
内存管理器可以将任何数目的丢弃页表示为单个页,该页可以在同一个 MDL 中或者甚至多个并发的 MDL(用于不同的驱动程序)中嵌入多次。因此,表示丢弃页的位置的内容可以随时都可能改变。
基于 MDL 映射的页上的数据值来执行解密或计算校验和的驱动程序不得从系统提供的 MDL 撤销指针来访问数据。为了确保正确的操作,这样的驱动程序应该根据驱动程序从 I/O 管理器接收到的系统提供的 MDL 来创建一个临时 MDL。要创建临时 MDL:
-
调用 MmGetMdlVirtualAddress 和 MmGetMdlByteCount 来获取系统提供的 MDL 的虚拟基址和长度。
-
使用 PoolType=NonPagedPool 调用 ExAllocatePoolWithTag 来从未分页内存池分配缓冲区。指定等于系统提供的 MDL 长度的缓冲区大小,向上扩大到页边界。
-
调用 IoAllocateMdl 来分配一个 MDL(使用步骤 2 中创建的池缓冲区的虚拟基址和长度)。
-
调用 MmBuildMdlForNonpagedPool 来更新临时 MDL,使其描述步骤 2 中池缓冲区的底层物理页)。
驱动程序应该将这个临时 MDL 传递给从其硬件读取数据的调用,然后在任何需要的操作中使用临时 MDL 描述的数据值。通过调用 MmBuildMdlForNonPagedPool 来更新临时 MDL,驱动程序可以确保临时 MDL 不包含任何临时页,这样可以使其不会发生对页内容的任何改变。通过这种方式,即使系统 MDL 包含将被丢弃的(可能重复的)页,驱动程序仍然可以避免检查不稳定的内容。当驱动程序完成其操作时,它应该通过在 try/except 或 try/finally 块中使用 RtlCopyMemory 将更改的数据从临时 MDL 复制回系统提供的 MDL。
使用 MDL 作为典型 I/O 操作的一部分的驱动程序(不访问底层页上的数据)不需要创建临时 MDL。在内部实现上,内存管理器跟踪驻留的所有页以及每个页如何被映射。当驱动程序将 MDL 传递给系统服务例程来执行 I/O 时,内存管理器确保使用正确的数据。
您应该做什么?
-
不要假设 MDL 指向的任何内存位置的内容在任何给定的时间都有效。
-
如果您的驱动程序依赖于数据的值,那么始终应该在系统提供的 MDL 中双重缓存数据。