有效使用内存的 6 条技巧

Updated: March 17, 2004
On This Page

有效布局数据结构并尽可能重用它们。 有效布局数据结构并尽可能重用它们。

在启动时将未分页的池内存用于长期用途。 在启动时将未分页的池内存用于长期用途。

经济有效地使用内存 经济有效地使用内存

使用后备列表 使用后备列表

避免频繁地建立和取消虚拟地址空间的映射 避免频繁地建立和取消虚拟地址空间的映射

测试与验证 测试与验证

资源 资源

本文提供在 Microsoft Windows 家族操作系统内核模式驱动程序中有效使用内存的技巧。有效使用内存有助于提高驱动程序性能。以下是有效使用内存的 6 条技巧。

有效布局数据结构并尽可能重用它们。

设计驱动程序时,根据内存类型、大小和生命周期来计划内存分配。合并类似生存期的内存分配,这样就可以在不使用内存时尽快将其释放。不要将大小差距很大的数据结构混合到同一个内存分配中,除非您确定它们能够恰当地保持一致。

重用数据结构,而不是释放它们并在以后为其他用途重新分配内存。数据结构的重用可以避免额外的重新分配操作,有助于预防内存池的碎片化。

处理 I/O 请求时,驱动程序往往需要额外的内存。驱动程序可能为特定的 I/O 请求分配内存描述符表 (MDL) 或者内部缓冲区,也可能需要分配一个 IRP 并发送到底层驱动程序。这些数据结构的大小根据请求的不同而异。例如,MDL 的大小依赖于它所描述的 MDL 大小。

如果驱动程序拥有一种限制 I/O 大小或分割大的 I/O 请求的技术,那么您可以固定缓冲区的大小,从而固定 MDL 的大小并使缓冲区可重用。

请记住,所有性能问题都涉及到调优和平衡。作为一条通用规则,您应该优化最常用的操作,而不是不常用的大型请求或很少发生的小型请求。

在启动时将未分页的池内存用于长期用途。

驱动程序通常将未分页的池内存用于长期的 I/O 缓冲区。因为随着系统的不断运行,未分页的池会碎片化,所以驱动程序应该预先分配长期结构所需的内存,并在设备移除时释放这些内存。例如,总是执行 DMA 操作、创建若干事件并且使用后备列表的启动程序应该在启动时、在 DriverEntry 或者 AddDevice 例程中为这些对象分配内存,并在在处理设备移除请求时释放这些内存。

但是,驱动程序不应该预分配非常大的内存块(例如几兆字节)并试图在该内存块中管理自己的分配。

合适的内存分配例程包括 ExAllocatePoolWithTagExAllocatePoolWithQuotaTagExAllocatePoolWithTagPriorityAllocateCommonBuffer (如果驱动程序的设备使用总线主控 DMA 或者系统 DMA 控制器的自动初始化模式)。

驱动程序应该使用池分配例程的有标记版本,而不应使用陈旧的无标记版本。WinDbg 和许多测试工具使用标记来跟踪内存分配。对池分配进行标记有助于更轻松地找到与内存相关的 bug。

经济有效地使用内存

未分页的池内存是一种有限的系统资源。驱动程序应该尽可能经济有效地分配内存。一般而言,应避免重复调用内存分配支持例程来请求小于 PAGE_SIZE 的内存分配。如果驱动程序通常同时使用几个相关的数据结构,那么考虑将这些数据结构捆绑到单个内存分配中。例如,SCSI 端口驱动程序将 IRP、SCSI 请求块 (SRB) 和 MDL 绑定到单个内存分配中。

使用 DMA 的驱动程序例外。如果执行 DMA 的驱动程序需要若干个单页缓冲区,但是这些缓冲区不需要彼此相邻,那么它应该分别为每个缓冲区调用 AllocateCommonBuffer。这种方法保持连续的地址空间并改进内存分配将要进行的更改。

另外,请考虑您计划用于分配请求的内存分配例程是否会达到下一个页面边界。

如果驱动程序请求小于 PAGE_SIZE 字节,那么 ExAllocatePoolWithTag 将根据请求的字节量分配内存。如果驱动程序请求等于或大于 PAGE_SIZE,那么 ExAllocatePoolWithTag 将分配一个页对齐 (page-aligned) 的缓冲区,这个缓冲区的大小是 PAGE_SIZE 的整数倍。小于 PAGE_SIZE 的内存分配不会跨越页边界,不必是页对齐的;但是它们需要与一个 8 字节边界对齐。

AllocateCommonBuffer 总是分配至少一页的内存。如果驱动程序请求小于 PAGE_SIZE 字节的整数倍,那么驱动程序将无法访问最后一页的剩余字节。

使用后备列表

后备列表提供大小固定、可重用的缓冲区。设计它们用于驱动程序需要动态分配但无法预料分配数量的结构。

后备列表既可以从分页的内存池分配,也可以从未分页的内存池分配。驱动程序定义该列表中条目的布局和内容,系统根据需要维护列表状态并调整可用条目的数量。

驱动程序调用 ExInitialize[N]PagedLookasideList 创建后备列表,调用 ExAllocateFrom[N]PagedLookasideList 分配列表中的条目,调用ExFreeTo[N]PagedLookasideList 释放列表中的条目。列表头部必须从未分页内存中分配,即使该列表条目本身处于分页内存之中。

避免频繁地建立和取消虚拟地址空间的映射

频繁地建立和取消虚拟地址空间的映射会降低系统整体性能,因为这会导致 Translation Lookaside Buffer (TLB) 频繁刷新。TLB 是每个处理器的虚拟地址到物理地址转换的缓存。TLB 中的每个条目都包含一个页表条目 (PTE)。

系统转换引用新页的虚拟地址时,它就会将一个条目添加到 TLB。填满 TLB 之后,每次添加一个新增条目时,系统必须丢弃一个现有条目。结果,每当调用方重新映射或取消映射地址空间时都会更改一个 PTE,系统必须中断所有 CPU 才能更新包含该 PTE 的所有 TLB 条目。

就内部而言,I/O 管理器能够避免 Irp->MdlAddress 中的 MDL 发生这种问题。内核模式组件首次调用 MmGetSystemAddressForMdlSafe 时,I/O 管理器将系统地址与相应的物理地址一同存储在 MDL 中。IRP 在完成之后返回到 I/O 管理器时,I/O 管理器取消 MDL 的映射。这样,I/O 管理器仅需要将单个映射(和单个虚拟地址到物理地址的转换)用于每个 I/O 请求。

测试与验证

使用 Driver Verifier (verifier.exe)、GFlags (gflags.exe) 和 PoolMon (poolmon.exe) 来跟踪、测试和验证内存分配问题。

驱动程序验证工具能够从特殊内存池分配内存并监视驱动程序对分配的内存访问。它能够检测出对分配范围之外的内存或已经释放的内存的访问企图。另外,它可以检测出内存泄漏。内存泄漏是指已经分配,但不再使用并且未释放的内存块。验证工具还收集统计数据,统计从特殊内存池请求的内存分配量以及分配是否成功。

结合使用 GFlags 与 Driver Verifier。可以使用 GFlags 配置 Driver Verifier 的特殊内存池选项,或者指定要在单独的内存分配中使用的特殊内存池。

PoolMon 收集和显示内存分配的多种数据,这些数据按照在分配阶段赋予的池标记进行排序。

资源

关于内存分配的更多信息,请参阅 Microsoft Windows Driver Development Kit (DDK):
http://www.microsoft.com/whdc/devtools/ddk/default.mspx

内核模式驱动程序体系结构

设计指南("内存管理")

驱动程序开发工具

驱动程序测试工具 ("Driver Verifier"、"GFlags with Page Heap Verification" 和 "PoolMon")



此信息有用吗?