Windows 内存管理 - 第二部分





4.00/5 (1投票)
对 Windows 内存管理文章的扩展。
引言
最近,我写了一篇探讨 Windows 内存管理基础的文章。我写这篇文档是为了回应一些反馈,以便对这篇初步的文章进行扩展。很多时候,应用程序开发人员、系统管理员或设备驱动程序编写者会发现他们的操作系统开始出现内存泄漏。理解 Windows 内存管理器基础知识、用于代码检测的各种进程内存计数器以及这些计数器之间的差异,可以带来更稳定的操作系统,增强开发过程,并防止数据损坏的可能性。例如,如果一个应用程序由于某个进程访问过的资源但从未关闭而出现 bug,这个 bug 甚至可能覆盖原始源代码。及时发现这类问题可以帮助开发人员尝试在调试器中获取进程地址空间的内存快照,并可能进行紧急处理。
Windows 内存管理器的组件是 Windows Executive 的一部分,因此存在于文件 Ntsokrnl.exe 中。此内存管理器没有任何组件存在于硬件抽象层。因此,内存管理器组件能够在应用程序启动到运行时立即创建可执行映像已完全加载的假象。实际上,只有可执行文件的部分在需要时,并且由用户交互决定,才会被加载。
Windows IT 人员称之为“懒惰分配器”。换句话说,如果您启动像记事本这样的应用程序,Windows 并不会将整个应用程序和必要的 DLL 加载到物理内存中。它会按需进行:当记事本访问代码页时,当记事本访问数据页时,正是这时内存管理器将虚拟(逻辑)内存与物理(绝对)内存建立联系,并在需要时从磁盘读取内容。实际上,我们将看到 Windows 如何利用这些内存需求和分页速率来确定在系统启动时应为进程分配多少物理内存(工作集)。它已经监控了这些需求和速率,甚至将它们记录在预取文件中(逻辑预取)。自 Windows XP 以来,逻辑预取通过记录这些行为来增强应用程序的启动速度。
进程句柄泄漏
总而言之,句柄是对对象的引用,是对进程可用的资源的引用。句柄泄漏通常不会直接影响已提交的虚拟内存。它可能会影响系统内存,例如系统内存内核堆之一,或者可能会影响系统虚拟内存,但不是由进程显式分配的私有虚拟内存。句柄,再次强调,是对已打开的操作系统资源的引用(对象的一个已打开实例),例如文件、注册表项、TCP/IP 端口等。
打开这些句柄的进程会获得分配给它们的资源。如果进程不关闭句柄,它可能会耗尽系统内存。也就是说,如果它们不关闭资源,最终将耗尽系统内存,因为系统正在分配内存来存储与这些对象相关的数据,或者仅仅是存储用于跟踪进程句柄表的内存。这些资源本可以由其他进程使用,但却一直打开着,消耗系统内核内存。系统范围内对打开的句柄数量没有内置限制。但是,单个进程可以创建的句柄数量是有限制的。每个进程的固定限制约为 1600 万个句柄。或者一个进程可以创建 1600 万个句柄。由于 Windows 用来跟踪已打开句柄的表来自一个名为分页池的系统范围内存资源,因此,进程句柄泄漏(通常是用户应用程序中的 bug)最终可能导致系统内存耗尽。
Testlimit.exe 上的“-h”开关会导致此测试程序创建单个对象并反复打开对同一对象的句柄。这是一个进程句柄表增长导致内核内存消耗的例子。例如,运行 C:\Windows\System32> testlimit.exe -h,将开始为单个创建的对象创建句柄。打开系统信息框后,分页内核内存会急剧增加。非分页内存变化不大。当进程创建句柄时,操作系统正在扩展该进程的句柄表,而这个扩展来自内核内存分页池。如果我们继续使用“-h”开关启动几个 Testlimit.exe 实例,最终将耗尽内核内存并导致系统瘫痪。这意味着用户应用程序中的 bug 最终可能导致数据损坏。请注意,系统信息框中的句柄数以百万计地增加。这就是如何确定您是否遇到句柄泄漏的方法。当您需要查找出现句柄泄漏的进程时,您可以使用 Process Explorer 的进程视图。选择工具栏上的“视图”选项中的“选定列”。选择“进程性能”,然后选择“句柄计数”。当这个在进程视图中显示出来时,请注意这个数字是固定的,还是在增加。如果数字在增加,那么您必须终止该进程。
任何泄漏者(无论是在内核模式还是用户模式)的常见属性之一是,当前分配接近或等于峰值,并且它将始终在增长。如果我们查看各种进程内存计数器之间的差异——私有字节、虚拟内存字节和工作集大小——那么最有效的计数器是私有字节计数器。请注意,计数器是每秒(或其他时间间隔)计算某些东西(例如字节)。这意味着计数器可以计算或指定实际数量或每秒的实际速率。Process Explorer 中使用的私有字节计数器列表示私有于进程的字节的实际数量。例如,您在记事本中键入的文本是私有数据——没有其他进程对此数据感兴趣。这是私有虚拟内存或私有字节的一部分。进程共享内存,多个进程可以访问同一个 DLL。一个 DLL 只加载一次。下面是系统信息框的图像,它是 Process Explorer 工具栏窗格上的一个图标。请注意,在左下方,您会看到一个“提交”部分
如提交电荷列所示,当前分配为 873,968 KB。请注意,提交图显示为 851.2 MB,相对而言是相同的。请注意,有一个限制和峰值。为了演示 Mark Russinovitch 编写的命令行工具 Testlimit.exe,我们首先注意到当前分配。我们运行 Testlimit.exe(如果您的系统是 64 位系统,则运行 Testlimit64.exe),并注意到当前分配急剧上升;进程正在持续分配虚拟内存而不释放它。相反,它接近分配然后继续增长。Testlimit.exe 有一个“m”开关可以泄漏内存。要运行命令行程序,我们键入 C:\Windows\System32>testlimit.exe -m 10。此命令导致进程每半秒泄漏 10 MB,即每秒泄漏 20 MB。注意提交图的变化
当前分配将超过提交限制,并可能超过提交峰值。当打开 Process Explorer,并将私有字节、私有字节增量和私有字节历史计数器列拖放到相邻位置时,您会看到 Testlimit.exe 进程的增长,它将从一条平坦的黄色线变为一条粗黄线
私有字节历史列显示,此测试程序的黄色线从平坦线变为粗线。请注意,此泄漏甚至导致 Explorer.exe 释放少量内存。私有字节列指定该进程的大量私有字节,这些内存不能与其他进程共享。此信息不会在任务管理器的 memusage 列中显示。
那些私有字节是不共享的
Windows 会自动共享任何可共享的内存(非进程私有)。这意味着代码:任何可执行文件或 DLL 的部分。一次内存中只有一个可执行文件或 DLL 的副本。为了让多个进程引用一个已加载的 DLL,Windows 使用内存映射文件,这些文件在内部称为节对象。页面文件是物理内存的一个页面。“小”页面是 4096 字节,或 4 KB,而大页面是 16 KB。一次允许在内存中的可执行映像副本不仅适用于独立桌面。这还包括终端服务器等服务器。
如果有多个用户登录并使用 Outlook,Outlook 的一个副本将从磁盘读取(或其所需的部分)以驻留在内存中。如果用户开始使用 Microsoft Outlook 的其他功能,它们将在需要时从磁盘读取。这间接定义了一个进程的工作集。Windows 分配给进程的物理内存量称为其工作集。每个进程都从一个空的大小为零的工作集开始。当进程内的线程开始访问虚拟地址时,工作集开始增长。当操作系统开始启动时,它必须决定为每个进程分配多少物理内存,以及它需要为自身保留多少物理内存(RAM)以存储缓存数据并保持空闲。Windows 监控进程的行为,以便 Windows 内存管理器最终根据进程的内存需求和分页速率确定该数量。
内存映射文件的好处简要概述
大多数程序都需要某种形式的动态内存管理。当需要创建其大小在程序构建时无法静态确定的数据结构时,就会产生这种需求。搜索树,类似于二叉树、符号表和链表,是数据结构的常见示例。请注意,符号代表源代码中编写的变量和方法(无论是来自 C 中使用的预定义头文件,还是 C++ 中使用的类库)。这些有时会被预处理,而不是包含在编译器本身的输入流中。这些符号还可以代表它们在源代码文本文件中的行号。关键在于,这些数据结构的大小可能会改变,因此在为这些数据结构提供灵活机制的同时,重要的是要注意,程序通常会创建一个临时文件(该文件必须在程序终止前删除)。Windows 内存管理器提供内存映射文件,将进程的地址直接与文件关联,从而使操作系统能够管理文件和内存之间的所有数据移动,避免程序员依赖某些 I/O 函数。那么这意味着什么呢?堆中动态分配的内存必须物理上存在于页面文件中。Windows 内存管理控制物理内存和页面文件之间的页面移动,还将进程的虚拟地址映射到页面文件。当进程终止时,文件中的物理空间将被释放。
如何确定页面文件的大小?
许多技术帮助文档断言,页面文件的大小应为 RAM 的倍数,例如 RAM 的 1.5 倍。您最好使用 Process Explorer 并将系统范围内所有进程的私有虚拟内存字节相加,然后乘以 1.5 或 1.2 左右,因为为了正确确定页面文件的大小,您需要了解什么进入页面文件。如果您同时运行多个应用程序,那么您需要一个更大的页面文件,因为并发执行将导致将更多已修改的私有数据转储到页面文件中。如果您顺序运行这些应用程序,那么对大型页面文件的需求就会减少。但您不应根据 RAM 的大小来确定页面文件的大小。同样,如果您收到一条消息框提示您虚拟内存不足,也不应增加页面文件的大小。这只会延迟在重新启动后收到相同的消息框。
进入页面文件的唯一数据是修改过的私有虚拟数据,例如您在记事本中键入的文本。存储该文本的内存必须位于磁盘上的某个可保存位置,以便之后可以从中重新获取,而该区域就是页面文件。如果记事本的代码被从记事本的工作集中移除,Windows 实际上可以重用该物理内存,而无需保存该内存,因为该内存已经保存在磁盘上:它在磁盘上的记事本映像中。因此,代码以及来自页面文件以外的文件映射的数据,无需先进入页面文件即可从磁盘重新读取。那么什么会被分页出去?修改过的私有数据,而不是代码,会被分页出去。这些数据仅在必要时才发送到页面文件。仅当它们已被从进程的工作集中移除,并且 Windows 需要将该内存用于其他用途时。
当发送到页面文件的已修改页面的数量达到一个阈值(略大于 2 MB)时,修改页面写入器将启动,开始将这些内容发送到页面文件,因为它之后可以将这些页面移至待定列表,从而贡献于可立即使用的内存。修改页面写入器是运行在六个不同的内核模式系统线程中的几个关键组件之一。在 0-31 的优先级范围内,修改页面写入器(优先级 17)将已修改列表中的脏页面写回相应的页面文件。当需要减小已修改列表大小时,会唤醒此线程。
更重要的是,当已修改页面列表过大,或者零页和待定列表的大小低于最小阈值时,会唤醒两个系统线程中的一个将页面写回磁盘并将页面移至待定列表。一个系统线程将已修改的页面写出到页面文件,第二个系统线程将已修改的页面写出到映射文件。
要从页面文件中读取页面,它必须长时间未被引用。也就是说,进程尚未引用它,但它尚未表明可以完全放弃它。当进程必须从其工作集中放弃一个页面时(因为它引用了一个新页面,并且其工作集已满,或者内存管理器修剪了其工作集(它认为进程变得太大;这就是为什么工作集计数器在跟踪泄漏者时不是一个好的指标)),如果该页面是干净的(未修改 - 被该进程写入以存储数据),它将转到待定列表;如果页面在驻留期间被修改,它将转到已修改页面列表。
因此,再次强调,进程工作集中的私有内存会在一段时间内未被引用后被移除并放入已修改列表。如果它仍然未被引用,它将被发送到页面文件。然而,在那时,该页面仍然保留在内存的待定列表中。如果进程继续足够长的时间不引用该页面,直到该页面遍历待定列表中的所有页面,Windows 就会获取该页面,用于其他目的,然后,最终,进程调用该页面;只有到那时,Windows 才会从页面文件中重新读取该页面。Process Monitor 可以显示页面文件读取。如果您看到页面是或一直是页面文件的常态读取,那么您将需要更多内存。页面文件的存在是为了允许 Windows 转储未被删除或保存到其他磁盘文件的已修改私有内存到磁盘上,以便 Windows 可以将该内存用于其他目的。
让我们回到关于页面文件大小的问题。确定页面文件大小的最坏情况场景是,您将所有正在运行的进程在任何给定时间点的所有私有虚拟内存使用量相加,然后所有这些都被挤出物理 RAM 以供其他用途。这将是存储可能被这些进程读回的私有数据的页面文件的大小。这个总和与您的系统有多少 RAM 无关。它取决于操作系统和用户的行为。
Windows 保留着一个数字,即可以分页出去的已提交私有虚拟内存的总量(如潜在的页面文件使用量)。它被称为系统“提交电荷”。它是私有已提交虚拟内存的总量。它是每个进程的私有字节之和加上内核内存私有字节之和,或内核内存分页池。总私有虚拟内存使用量是提交电荷峰值;足以容纳您在任何给定时间点正在运行的所有同时分配的私有虚拟内存。将其乘以 1.5 或 1.2 左右。也就是说,提交电荷峰值加上一些量或一些乘数,但不要仅根据 RAM 大小来确定页面文件的大小。
页面文件应该是连续的。当 Windows 写入页面文件时,它会以块的形式写入,因此如果在一个写入过程中页面文件之间有中断,Windows 将不得不进行一次写入而不是多次写入。TechNet Sysinternals 工具 Pagedfrg.exe 在系统启动时对页面文件进行碎片整理。此工具可以对页面文件进行碎片整理,使其尽可能集中。
本节介绍了 Windows 内存管理器实现虚拟内存管理的基本知识。给出的一些故障排除技巧表明,当一个进程开始反复分配虚拟内存或创建进程句柄时,某些系统限制或内核分页池阈值就会被超过,并且会持续增长。每个进程都可以访问私有的虚拟地址空间,保护一个进程的内存不受另一个进程的影响,但允许进程高效地共享内存。一个非常基本但有效的测试是下载 Microsoft 的“tswebset.exe”。这是一个终端服务器 Web 浏览器。如果您运行 XP 机器并配置了远程桌面访问,则将 tswebsetup.exe 安装在您的 C:\Inetpub\wwwroot 目录下。输入 default.htm 页面。输入 IP 和计算机名称(以及登录信息),以便远程分析终端。然后筛选 TechNet Sysinternals 的 PSTools Suite,并尝试远程安装将从系统中提取数据的工具。当然,这需要您记录由于您自己的存在而对远程系统进行的任何更改。也许是 Windows 目录中的预取 .pf 文件?
参考文献
- Mark Russinovich 和 David Solomon 著《Windows Internals, 4th Edition》。
- Mark Russinovich 和 David Solomon 著《Sysinternals Video Library》。
- Johnson M. Hart 著《Windows Systems Programming》。