突破 Windows 的极限
Pushing the Limits of Windows: Physical Memory - Microsoft Community Hub
首次发布于 2008 年 7 月 21 日在 TechNet 上
这是我将在接下来的几个月中撰写的系列博客文章中的第一篇,名为“突破 Windows 的限制”,该系列文章描述了 Windows 和应用程序如何使用特定资源、资源的许可和实现衍生的限制、如何衡量资源的使用情况。用途以及如何诊断泄漏。为了能够有效地管理 Windows 系统,您需要了解 Windows 如何管理物理资源(例如 CPU 和内存)以及逻辑资源(例如虚拟内存、句柄和窗口管理器对象)。了解这些资源的限制以及如何跟踪其使用情况,使您能够将资源使用情况归因于使用它们的应用程序,针对特定工作负载有效调整系统大小,并识别泄漏资源的应用程序。
这是整个“挑战极限”系列的索引。虽然它们可以独立存在,但它们假设您按顺序阅读它们。
物理内存
计算机上最基本的资源之一是物理内存。 Windows 的内存管理器负责用活动进程、设备驱动程序和操作系统本身的代码和数据填充内存。由于大多数系统在运行时访问的代码和数据超出了物理内存的容量,因此物理内存本质上是了解随着时间的推移使用的代码和数据的窗口。因此,内存量会影响性能,因为当进程或操作系统所需的数据或代码不存在时,内存管理器必须从磁盘将其引入。
除了影响性能之外,物理内存量还会影响其他资源限制。例如,非分页池、物理内存支持的操作系统缓冲区的数量显然受到物理内存的限制。物理内存也会影响系统虚拟内存限制,该限制大约是物理内存大小加上任何页面文件的最大配置大小的总和。物理内存还可以间接限制最大进程数,我将在以后有关进程和线程限制的文章中讨论这一点。
Windows 服务器内存限制
Windows 对物理内存的支持取决于硬件限制、许可、操作系统数据结构和驱动程序兼容性。 MSDN 中 的 Windows 版本内存限制 页面 按 SKU 记录了不同 Windows 版本以及版本内的限制。
您可以看到所有 Windows 版本的服务器 SKU 之间的物理内存支持许可差异。例如,32位版本的Windows Server 2008 Standard仅支持4GB,而32位Windows Server 2008 Datacenter支持64GB。同样,64位Windows Server 2008 Standard支持32GB,64位Windows Server 2008 Datacenter可以处理高达2TB的容量。市面上的 2TB 系统并不多,但 Windows Server 性能团队知道有几个,其中包括他们实验室中曾经拥有的一个。这是该系统上运行的任务管理器的屏幕截图:
Windows Server 2003 Datacenter Edition 支持的最大 32 位限制为 128GB,这是因为内存管理器用于跟踪物理内存的结构会在较大的系统上消耗过多的系统虚拟地址空间。内存管理器跟踪称为 PFN 数据库的数组中的每个内存页,并且为了提高性能,它将整个 PFN 数据库映射到虚拟内存中。由于它用 28 字节的数据结构来表示内存的每个页面,因此 128GB 系统上的 PFN 数据库大约需要 980MB。 32位Windows有一个由硬件定义的4GB虚拟地址空间,默认情况下它在当前执行的用户模式进程(例如记事本)和系统之间划分。因此,980MB 几乎消耗了可用的 2GB 系统虚拟地址空间的一半,只留下 1GB 用于映射内核、设备驱动程序、系统缓存和其他系统数据结构,这是一个合理的划分:
这也是为什么内存限制表列出了使用 4GB 调整启动时相同 SKU 的下限(称为 4GT,并通过 Boot.ini 的 /3GB 或 /USERVA 以及 Bcdedit 的 /Set IncreaseUserVa 启动选项启用),因为 4GT 将分割移动到给用户模式3GB,只给系统留下1GB。为了提高性能,Windows Server 2008 通过将其最大 32 位物理内存支持降低到 64GB 来保留更多的系统地址空间。
内存管理器可以通过根据需要将 PFN 数据库的各个部分映射到系统地址来容纳更多内存,但这会增加复杂性,并可能因映射和取消映射操作的额外开销而降低性能。直到最近,系统才变得足够大,可以考虑这一点,但由于系统地址空间不是在 64 位 Windows 上映射整个 PFN 数据库的约束,因此对更多内存的支持留给了 64 位 Windows。
64 位 Windows Server 2008 Datacenter 的最大 2TB 限制并非来自任何实施或硬件限制,但 Microsoft 将仅支持他们可以测试的配置。截至 Windows Server 2008 发布时,任何地方可用的最大系统为 2TB,因此 Windows 限制了其物理内存的使用。
Windows 客户端内存限制
64 位 Windows 客户端 SKU 支持不同数量的内存作为 SKU 差异化功能,低端 Windows XP Starter 为 512MB,Vista Ultimate 为 128GB,Windows 7 Ultimate 为 192GB。然而,所有 32 位 Windows 客户端 SKU(包括 Windows Vista、Windows XP 和 Windows 2000 Professional)均支持最大 4GB 物理内存。 4GB是标准x86内存管理模式下可访问的最高物理地址。最初,甚至不需要考虑在客户端上支持超过 4GB 的内存,因为这种内存量很少见,即使在服务器上也是如此。
然而,当Windows XP SP2正在开发时,可以预见超过4GB的客户端系统,因此Windows团队开始在超过4GB内存的系统上广泛测试Windows XP。 Windows XP SP2 还在实现不执行内存的硬件上默认启用物理地址扩展 (PAE) 支持,因为它是数据执行保护 (DEP) 所必需的,但这也支持超过 4GB 的内存。
他们发现,许多系统会崩溃、挂起或无法启动,因为某些设备驱动程序(通常是在客户端而非服务器上找到的视频和音频设备驱动程序)未编程为期望物理地址大于 4GB。结果,驱动程序截断了这些地址,导致内存损坏和损坏副作用。服务器系统通常具有更通用的设备以及更简单和更稳定的驱动程序,因此通常不会出现这些问题。有问题的客户端驱动程序生态系统导致客户端 SKU 决定忽略驻留在 4GB 以上的物理内存,尽管理论上它们可以解决该问题。
32 位客户端有效内存限制
虽然 4GB 是 32 位客户端 SKU 的许可限制,但有效限制实际上较低,并且取决于系统的芯片组和连接的设备。原因是物理地址映射不仅包括 RAM,还包括设备内存,x86 和 x64 系统映射 4GB 地址边界以下的所有设备内存,以保持与不知道如何处理的 32 位操作系统兼容地址大于 4GB。如果系统具有 4GB RAM 和设备(例如视频、音频和网络适配器),这些设备在其设备内存中实现了 Windows,总容量为 500MB,则 4GB RAM 中的 500MB 将驻留在 4GB 地址边界之上,如下所示:
结果是,如果您的系统具有 3GB 或更多内存,并且运行的是 32 位 Windows 客户端,则您可能无法充分利用所有 RAM。在 Windows 2000、Windows XP 和 Windows Vista RTM 上,您可以在“系统属性”对话框、任务管理器的“性能”页面中查看 Windows 可访问的 RAM 量,在 Windows XP 和 Windows Vista(包括 SP1)上,可以在 Msinfo32 和温弗公用事业。在 Window Vista SP1 上,其中一些位置已更改为显示已安装的 RAM,而不是可用的 RAM,如本 知识库文章 中所述 。
在我的 4GB 笔记本电脑上,当使用 32 位 Vista 启动时,可用物理内存量为 3.5GB,如 Msinfo32 实用程序中所示:
您可以使用Alex Ionescu 的 Meminfo 工具 查看物理内存布局 (他 为我与 David Solomon 共同创作的 《Windows 内部结构 》第 5 版做出了贡献 )。这是当我使用 -r 开关在该系统上运行 Meminfo 来转储物理内存范围时的输出:
请注意内存地址范围中的间隙是从页 9F0000 到页 100000,另一个间隙是从 DFE6D000 到 FFFFFFFF (4GB)。然而,当我使用 64 位 Vista 启动该系统时,所有 4GB 都显示为可用,您可以看到 Windows 如何使用 4GB 边界之上剩余的 500MB RAM:
4GB以下的空间被什么占据了?设备管理器可以回答这个问题。要进行检查,请启动“devmgmt.msc”,在“视图”菜单中选择“按连接划分的资源”,然后展开“内存”节点。在我的笔记本电脑上,毫不奇怪,映射设备内存的主要消耗者是显卡,它消耗 256MB 的内存,范围为 E0000000-EFFFFFFF:
其他杂项设备占其余大部分,并且 PCI 总线为设备保留了额外的范围,作为固件在引导期间使用的保守估计的一部分。
在配备大显卡的高端游戏系统上,低于 4GB 的内存地址消耗可能会非常严重。例如,我从一家精品游戏设备公司购买了一台配备 4GB RAM 和两个 1GB 显卡的设备。我没有指定操作系统版本,并假设他们会安装 64 位 Vista,但它附带了 32 位版本,因此 Windows 只能访问 2.2GB 内存。在我安装 64 位 Windows 后,您可以在系统的 Meminfo 输出中看到从 8FEF0000 到 FFFFFFFF 的巨大内存漏洞:
设备管理器显示超过 2GB 的空洞中有 512MB 用于显卡(每个 256MB),而且固件似乎为动态映射保留了更多空间,或者因为它的估计比较保守:
即使只有 2GB 的系统也可能无法在 32 位 Windows 下使用所有内存,因为芯片组会积极为设备保留内存区域。我们的共享家庭计算机是我们几个月前从一家主要 OEM 购买的,报告显示已安装的 2GB 内存中只有 1.97GB 可用:
从 7E700000 到 FFFFFFFF 的物理地址范围由 PCI 总线和设备保留,这留下了理论上最大的 7E700000 字节(1.976GB)的物理地址空间,但甚至其中一些是为设备内存保留的,这解释了 Windows 报告的原因1.97GB。
由于设备供应商现在必须向 Microsoft 的 Windows 硬件质量实验室 (WHQL) 提交 32 位和 64 位驱动程序以获得驱动程序签名证书,因此当今大多数设备驱动程序可能可以处理 4GB 以上的物理地址。然而,32 位 Windows 将继续忽略其上方的内存,因为仍然存在一些难以衡量的风险,并且 OEM 正在(或至少应该)转向 64 位 Windows,这不是问题。
最重要的是,无论数量多少,您都可以通过 64 位 Windows 充分利用系统内存(提高 SKU 的限制),如果您购买的是高端游戏系统,您绝对应该要求 OEM 将 64 位设置为 64 位。工厂里就装有窗户。
你有足够的内存吗?
无论您的系统有多少内存,问题是,它足够吗?不幸的是,没有硬性规则可以让您确定地知道。您可以使用一个通用准则,该准则基于随着时间的推移监控系统的“可用”内存,特别是当您运行内存密集型工作负载时。 Windows 将可用内存定义为未分配给进程、内核或设备驱动程序的物理内存。顾名思义,如果需要,可用内存可分配给进程或系统。当然,内存管理器试图通过将其用作文件缓存(备用列表)以及归零内存(零页列表)来充分利用该内存,并且 Vista 的 Superfetch 功能将数据和代码预取到备用内存中列出并优先考虑可能在不久的将来使用的数据和代码。
如果可用内存变得稀缺,则意味着进程或系统正在积极使用物理内存,如果它在很长一段时间内保持接近于零,您可能可以通过添加更多内存来受益。有多种方法可以跟踪可用内存。在 Windows Vista 上,您可以通过查看任务管理器中的物理内存使用历史记录来间接跟踪可用内存,并使其随着时间的推移保持接近 100%。这是我的 8GB 桌面系统上任务管理器的屏幕截图(嗯,我想我的内存可能太多了!):
在所有版本的 Windows 上,您可以通过在内存性能计数器组中添加可用字节计数器,使用性能监视器绘制可用内存图表:
您可以在Process Explorer 的 “系统信息”对话框 中查看瞬时值 ,或者在 Vista 之前的 Windows 版本上,在“任务管理器”的“性能”页面上查看瞬时值。
突破 Windows 的极限
在 CPU、内存和磁盘中,内存对于整体系统性能通常是最重要的。越多越好。 64 位 Windows 是确保您充分利用所有优势的最佳选择,并且 64 位 Windows 还具有其他性能优势,我将在以后的突破极限博客文章中讨论这些优势关于虚拟内存限制。
突破 Windows 的极限:虚拟内存
在我的第一篇突破 Windows 限制的文章中,我讨论了物理内存限制,包括许可、实现和驱动程序兼容性所施加的限制。这是整个“挑战极限”系列的索引。虽然它们可以独立存在,但它们假设您按顺序阅读它们。
这次我将注意力转向另一个基本资源,虚拟内存。虚拟内存将程序的内存视图与系统的物理内存分开,因此操作系统决定何时以及是否将程序的代码和数据存储在物理内存中,以及何时将其存储在文件中。虚拟内存的主要优点是它允许比物理内存中容纳的进程同时执行更多的进程。
虽然虚拟内存具有与物理内存限制相关的限制,但虚拟内存具有源自不同来源且因消费者而异的限制。例如,虚拟内存限制适用于运行应用程序的各个进程、操作系统以及整个系统。当您阅读本文时,请务必记住,虚拟内存,顾名思义,与物理内存没有直接联系。 Windows为文件缓存分配一定数量的虚拟内存并不决定它在物理内存中实际缓存多少文件数据;它可以是任何数量,从无到大于可通过虚拟内存寻址的数量。
进程地址空间
每个进程都有自己的虚拟内存,称为地址空间,它将其执行的代码以及代码引用和操作的数据映射到其中。 32 位进程使用 32 位虚拟内存地址指针,这为 32 位进程可以寻址的虚拟内存量创建了 4GB (2^32) 的绝对上限。然而,为了使操作系统能够在不改变地址空间的情况下引用自己的代码和数据以及当前正在执行的进程的代码和数据,操作系统使其虚拟内存在每个进程的地址空间中可见。默认情况下,32 位版本的 Windows 在系统和活动进程之间均匀划分进程地址空间,为每个进程创建 2GB 的限制:
应用程序可能使用堆 API、.NET 垃圾收集器或 C 运行时 malloc 库来分配虚拟内存,但在幕后所有这些都依赖于VirtualAlloc API。当应用程序用完地址空间时,VirtualAlloc 以及位于其之上的内存管理器将返回错误(由 NULL 地址表示)。 Testlimit 实用程序是我为Windows Internals 第 4 版编写的,用于演示各种 Windows 限制,它会重复调用 VirtualAlloc,直到在您指定 –r 开关时出现错误。因此,当您在 32 位 Windows 上运行 32 位版本的 Testlimit 时,它将消耗整个 2GB 的地址空间:
2010 MB 并不完全是 2GB,但 Testlimit 的其他代码和数据(包括其可执行文件和系统 DLL)解释了这一差异。您可以通过在Process Explorer中查看其虚拟大小来了解其消耗的地址空间总量:
某些应用程序(例如 SQL Server 和 Active Directory)管理大型数据结构,并且可以同时加载到其地址空间的数据越多,性能就越好。因此,Windows NT 4 SP3 引入了一个启动选项/3GB,该选项通过将系统地址空间大小减少到 1GB 来为进程提供 4GB 地址空间中的 3GB,而 Windows XP 和 Windows Server 2003 引入了 /userva 选项,该选项将分割 2GB 到 3GB 之间的任意位置:
然而,要利用 2GB 行以上的地址空间,进程必须在其可执行映像中设置“大地址空间感知”标志。对额外虚拟内存的访问是可选的,因为某些应用程序假设它们最多只能获得 2GB 的地址空间。由于引用低于 2GB 的地址的指针的高位始终为零,因此它们会使用指针中的高位作为自己数据的标志,当然在引用数据之前将其清除。如果它们使用 3GB 地址空间运行,它们会无意中截断值大于 2GB 的指针,从而导致程序错误,包括可能的数据损坏。
Windows 中的所有 Microsoft 服务器产品和数据密集型可执行文件都标记有大地址空间感知标志,包括 Chkdsk.exe、Lsass.exe(在域控制器上托管 Active Directory 服务)、Smss.exe(会话管理器)和Esentutl.exe(Active Directory Jet 数据库修复工具)。您可以使用 Visual Studio 附带的 Dumpbin 实用程序查看图像是否具有该标志:
Testlimit 还被标记为大地址感知,因此如果您在使用 3GB 用户地址空间启动时使用 –r 开关运行它,您将看到如下内容:
由于 64 位 Windows 上的地址空间远大于 4GB(我稍后将对此进行描述),因此 Windows 可以为 32 位进程提供最大 4GB 的寻址空间,并将其余部分用于操作系统的虚拟内存。如果您在 64 位 Windows 上运行 Testlimit,您将看到它消耗了整个 32 位可寻址地址空间:
64 位进程使用 64 位指针,因此其理论最大地址空间为 16 艾字节 (2^64)。然而,Windows 并没有在活动进程和系统之间平均划分地址空间,而是在地址空间中为进程和其他各种系统内存资源定义一个区域,例如系统页表条目 (PTE)、文件缓存、分页池和非分页池。
在 IA64 和 x64 版本的 Windows 上,进程地址空间的大小是不同的,其中通过平衡应用程序所需的内存成本和支持所需的开销(页表页和转换后备缓冲区 - TLB - 条目)来选择大小。地址空间。在 x64 上,该大小为 8192GB (8TB),在 IA64 上为 7168GB(7TB - 与 x64 的 1TB 差异来自于 IA64 上的顶级页目录为 Wow64 映射保留插槽)。在 Windows 的 IA64 和 x64 版本上,各种资源地址空间区域的大小均为 128GB(例如,非分页池分配了 128GB 的地址空间),但文件缓存除外,分配了 1TB。因此,64 位进程的地址空间看起来像这样:
该图并未按比例绘制,因为即使是 8TB,更不用说 128GB,也只是一小部分。可以这么说,就像我们的宇宙一样,64 位进程的地址空间中有很多空白。
当您使用 –r 开关在 64 位 Windows 上运行 64 位版本的 Testlimit (Testlimit64) 时,您将看到它消耗 8TB,这是它可以管理的地址空间部分的大小:
承诺内存
Testlimit 的 –r 开关让它保留虚拟内存,但实际上并不提交它。保留的虚拟内存实际上不能存储数据或代码,但应用程序有时会使用保留来创建一大块虚拟内存,然后根据需要提交它,以确保提交的内存在地址空间中是连续的。当进程提交虚拟内存区域时,操作系统保证它可以在物理内存或磁盘上维护该进程存储在内存中的所有数据。这意味着进程可能会遇到另一个限制:提交限制。
正如您从提交保证的描述中所期望的那样,提交限制是物理内存和分页文件大小的总和。实际上,并非所有物理内存都计入提交限制,因为操作系统保留部分物理内存供自己使用。所有活动进程提交的虚拟内存量(称为当前提交费用)不能超过系统提交限制。当达到提交限制时,提交内存的虚拟分配将失败。这意味着,即使是标准 32 位进程,在达到 2GB 地址空间限制之前也可能会出现虚拟内存分配失败的情况。
当前的提交费用和提交限制由 Process Explorer 在“提交费用”部分的“系统信息”窗口以及“提交历史记录”条形图和图表中进行跟踪:
Vista 和 Windows Server 2008 之前的任务管理器以类似方式显示当前提交费用和限制,但在其图表中将当前提交费用称为“PF 使用情况”:
在 Vista 和 Server 2008 上,任务管理器不显示提交费用图表,并使用“页面文件”标记当前提交费用和限制值(尽管事实上,即使您没有页面文件,它们也将是非零值) :
您可以通过使用 -m 开关运行 Testlimit 来强调提交限制,这会指示它分配已提交的内存。 32 位版本的 Testlimit 在达到提交限制之前可能会也可能不会达到其地址空间限制,具体取决于物理内存的大小、分页文件的大小以及运行时的当前提交费用。如果您运行的是 32 位 Windows,并且想要了解系统在达到提交限制时的行为方式,只需运行 Testlimit 的多个实例,直到其中一个实例在耗尽其地址空间之前达到提交限制。
请注意,默认情况下,分页文件配置为增长,这意味着当提交费用接近提交限制时,提交限制将会增长。即使当分页文件达到最大大小时,Windows 也会保留一些内存,其内部调整以及缓存数据的应用程序可能会释放更多内存。 Testlimit 预见到了这一点,当它达到提交限制时,它会休眠几秒钟,然后尝试分配更多内存,无限期地重复此操作,直到您终止它。
如果您运行 64 位版本的 Testlimit,它几乎肯定会在耗尽其地址空间之前达到提交限制,除非物理内存和分页文件总和超过 8TB(如前所述,这是 64 位版本的大小)位应用程序可访问的地址空间。以下是在我的 8GB 系统上运行的 64 位 Testlimit 的部分输出(我指定了 100MB 的分配大小以使其泄漏更快):
这是提交历史记录图,其中包含 Testlimit 暂停以允许分页文件增长时的步骤:
当系统虚拟内存不足时,应用程序可能会失败,并且您在尝试常规操作时可能会收到奇怪的错误消息。不过,在大多数情况下,Windows 将能够向您显示低内存分辨率对话框,就像我运行此测试时所做的那样:
退出 Testlimit 后,当内存管理器截断为适应 Testlimit 的极端提交请求而创建的分页文件的尾部时,提交限制可能会再次下降。此处,Process Explorer 显示当前限制远低于 Testlimit 运行时达到的峰值:
进程提交内存
由于提交限制是一种全局资源,其消耗可能导致性能不佳、应用程序故障甚至系统故障,因此一个自然的问题是“进程贡献了多少提交费用”?要准确回答这个问题,您需要了解应用程序可以分配的不同类型的虚拟内存。
并非进程分配的所有虚拟内存都计入提交限制。正如您所看到的,保留的虚拟内存则不然。表示磁盘上文件的虚拟内存(称为文件映射视图)也不会计入限制,除非应用程序要求写时复制语义,因为 Windows 可以丢弃与物理内存中的视图关联的任何数据,然后从文件中检索它。因此,映射其可执行文件和系统 DLL 映像的 Testlimit 地址空间中的虚拟内存不计入提交限制。有两种类型的进程虚拟内存确实计入提交限制:私有内存和页面文件支持的虚拟内存。
私有虚拟内存是垃圾收集器堆、本机堆和语言分配器的基础。它被称为私有的,因为根据定义它不能在进程之间共享。因此,很容易归因于某个进程,并且 Windows 通过专用字节性能计数器跟踪其使用情况。 Process Explorer 在进程属性对话框的性能页面的虚拟内存部分的专用字节列中显示进程专用字节使用情况,并在进程属性对话框的性能图表页面上以图形形式显示它。这是 Testlimit64 达到提交限制时的样子:
页面文件支持的虚拟内存更难归属,因为它可以在进程之间共享。事实上,没有特定于进程的计数器可以查看进程已分配或正在引用的数量。当您使用 -s 开关运行 Testlimit 时,它会分配页面文件支持的虚拟内存,直到达到提交限制,但即使在消耗了超过 29GB 的提交之后,该进程的虚拟内存统计信息也没有提供任何指示表明它是唯一的虚拟内存。负责任的:
因此,我不久前将 -l 开关添加到了 Handle 中。进程必须打开一个页面文件支持的虚拟内存对象(称为节),以便在其地址空间中创建页面文件支持的虚拟内存的映射。虽然 Windows 会保留现有的虚拟内存,即使应用程序关闭了其所在部分的句柄,但大多数应用程序仍保持句柄打开。 -l 开关打印进程已打开的页面文件支持的部分的分配大小。以下是 Testlimit 在使用 -s 开关运行后打开的句柄的部分输出:
您可以看到 Testlimit 以 1MB 块的形式分配页面文件支持的内存,如果您将其打开的所有部分的大小相加,您会发现它至少是贡献大量提交费用的进程之一。
我应该将分页文件设置为多大?
也许与虚拟内存相关的最常见问题之一是,我应该将分页文件设置为多大?网络上和报亭杂志上有关 Windows 的荒谬建议层出不穷,甚至 Microsoft 也发布了误导性建议。几乎所有建议都是基于将 RAM 大小乘以某个因子,常见值为 1.2、1.5 和 2。 现在您已经了解了分页文件在定义系统提交限制中所扮演的角色以及进程如何贡献提交费用,你已经很清楚这些公式到底有多么无用了。
由于提交限制设置了运行进程可以同时分配多少私有和页面文件支持的虚拟内存的上限,因此合理调整分页文件大小的唯一方法是了解您想要的程序的最大总提交费用同时运行。如果提交限制小于该数字,您的程序将无法分配所需的虚拟内存,并且将无法正常运行。
那么您如何知道您的工作负载需要多少提交费用?您可能已经在屏幕截图中注意到 Windows 跟踪该数字并且 Process Explorer 显示它:Peak Commit Charge。为了优化分页文件的大小,您应该同时启动所有运行的应用程序,加载典型的数据集,然后记下提交费用峰值(或者在您知道达到最大负载的一段时间后查看该值) 。将页面文件最小值设置为该值减去系统中的 RAM 量(如果该值为负数,请选择一个最小大小以允许您配置的故障转储类型)。如果您希望为潜在的大量提交需求留出一些喘息空间,请将最大值设置为该数字的两倍。
有些人认为没有分页文件会带来更好的性能,但一般来说,有分页文件意味着 Windows 可以将修改列表上的页面(代表未被主动访问但尚未保存到磁盘的页面)写入到分页文件,从而使该内存可用于更有用的目的(进程或文件缓存)。因此,虽然某些工作负载在没有分页文件的情况下可能会表现得更好,但一般来说,拥有分页文件意味着系统可以使用更多的可用内存(不用担心,如果没有大的分页文件,Windows 将无法写入内核故障转储)足以容纳它们)。
分页文件配置位于系统属性中,您可以通过在“运行”对话框中键入“sysdm.cpl”,单击“高级”选项卡,单击“性能选项”按钮,单击“高级”选项卡(这确实很高级)来访问该配置,然后单击“更改”按钮:
您会注意到,Windows 的默认配置是自动管理页面文件大小。在 Windows XP 和 Server 2003 上设置该选项后,Windows 将创建一个页面文件,如果 RAM 小于 1GB,则该文件的最小大小为 RAM 的 1.5 倍;如果 RAM 大于 1GB,则该文件的最小大小为 RAM 的 1.5 倍,最大大小为 RAM 的三倍。在 Windows Vista 和 Server 2008 上,最小值应足够大以容纳内核内存故障转储,并且是 RAM 加 300MB 或 1GB(以较大者为准)。最大值为 RAM 大小的三倍或 4GB,以较大者为准。这就解释了为什么我的 8GB 64 位系统上的峰值提交(在其中一张屏幕截图中可见)是 32GB。我想无论谁编写了该代码,都从我提到的一本杂志中得到了指导!
与虚拟内存相关的几个最终限制是 Windows 支持的页面文件的最大大小和数量。 32 位 Windows 的最大页面文件大小为 16TB(如果出于某种原因在非 PAE 模式下运行,则为 4GB),64 位 Windows 的页面文件大小在 x64 上最大为 16TB,在 IA64 上最大为 32TB。 Windows 8 ARM 的最大分页文件大小为 4GB。对于所有版本,Windows 最多支持 16 个分页文件,其中每个分页文件必须位于单独的卷上。
版本 |
不带 PAE 的 x86 限制 |
x86 w/PAE 的限制 |
ARM 的限制 |
x64 限制 |
IA64 的限制 |
Windows 7的 |
4GB |
16TB |
16TB |
||
视窗8 |
16TB |
4GB |
16TB |
||
Windows Server 2008 R2 |
16TB |
32TB |
|||
Windows 服务器 2012 |
16TB |
突破 Windows 的极限:分页池和非分页池
在之前的《突破极限》文章中,我描述了两种最基本的系统资源:物理内存和虚拟内存。这次我将描述两种基本的内核资源:分页池和非分页池,它们基于这些资源,并且直接负责许多其他系统资源限制,包括最大进程数、同步对象和句柄。
这是整个“挑战极限”系列的索引。虽然它们可以独立存在,但它们假设您按顺序阅读它们。
分页池和非分页池充当操作系统和设备驱动程序用来存储其数据结构的内存资源。池管理器在内核模式下运行,使用系统虚拟地址空间的区域(在推动虚拟内存的限制帖子中描述)来分配内存。内核的池管理器的运行方式与在用户模式进程中执行的 C 运行时和 Windows 堆管理器类似。由于最小虚拟内存分配大小是系统页面大小的倍数(x86 和 x64 上为 4KB),因此这些辅助内存管理器将较大的分配划分为较小的分配,以便不会浪费内存。
例如,如果应用程序想要一个 512 字节的缓冲区来存储一些数据,堆管理器会获取它已分配的区域之一,并记录前 512 字节正在使用中,返回指向该内存的指针,并将剩余的区域放入内存中。它用于跟踪空闲堆区域的列表上的内存。堆管理器使用空闲区域中的内存来满足后续分配,空闲区域从分配的 512 字节区域开始。
非分页池
内核和设备驱动程序使用非分页池来存储系统无法处理页面错误时可能访问的数据。内核在执行中断服务例程(ISR)和延迟过程调用(DPC)时进入这种状态,这些都是与硬件中断相关的功能。当内核或设备驱动程序获取自旋锁时,页错误也是非法的,因为自旋锁是唯一可以在 ISR 和 DPC 内使用的锁类型,因此必须使用它来保护从 ISR 或 DPC 内访问的数据结构。 DPC 和其他 ISR 或 DPC 或在内核线程上执行的代码。驱动程序未能遵守这些规则会导致最常见的崩溃代码IRQL_NOT_LESS_OR_EQUAL。
因此,非分页池始终保留在物理内存中,并且非分页池虚拟内存分配给物理内存。存储在非分页池中的常见系统数据结构包括表示进程和线程的内核和对象、互斥体、信号量和事件等同步对象、表示为文件对象的文件引用以及表示为文件对象的 I/O 请求数据包 (IRP)。代表 I/O 操作。
分页池
另一方面,分页池之所以得名,是因为 Windows 可以将其存储的数据写入分页文件,从而允许其占用的物理内存被重新利用。就像用户模式虚拟内存一样,当驱动程序或系统引用分页文件中的分页池内存时,会发生称为页面错误的操作,并且内存管理器将数据读回到物理内存中。分页池的最大使用者(至少在 Windows Vista 及更高版本上)通常是注册表,因为对注册表项和其他注册表数据结构的引用存储在分页池中。表示内存映射文件的数据结构(内部称为段)也存储在分页池中。
设备驱动程序使用ExAllocatePoolWithTag API 来分配非分页和分页池,并将所需的池类型指定为参数之一。另一个参数是 4 字节Tag,驱动程序应该使用它来唯一标识它们分配的内存,这对于跟踪泄漏池的驱动程序来说是一个有用的键,正如我稍后将展示的那样。
查看分页和非分页池使用情况
有三个性能计数器指示池使用情况:
- 池非分页字节
- 池分页字节(分页池的虚拟大小 - 有些可能会被分页)
- 池分页驻留字节(分页池的物理大小)
但是,没有针对这些池的最大大小的性能计数器。可以使用内核调试器 !vm 命令查看它们,但对于 Windows Vista 及更高版本,要在本地内核调试模式下使用内核调试器,您必须在调试模式下启动系统,这会禁用 MPEG2 播放。
因此,请使用 Process Explorer 查看当前分配的池大小以及最大值。要查看最大值,您需要将 Process Explorer 配置为使用操作系统的符号文件。首先,安装最新的Windows 调试工具包。然后运行 Process Explorer 并在“选项”菜单中打开“符号配置”对话框,并将其指向“Windows 调试工具”安装目录中的 dbghelp.dll,并将符号路径设置为指向 Microsoft 的符号服务器:
配置完符号后,打开“系统信息”对话框(单击“视图”菜单中的“系统信息”或按 Ctrl+I)以查看“内核内存”部分中的池信息。这是在 2GB Windows XP 系统上的样子:
2GB 32 位 Windows XP
非分页池限制
正如我在上一篇文章中提到的,在 32 位 Windows 上,系统地址空间默认为 2GB。这本质上将非分页池(或任何类型的系统虚拟内存)的上限限制为 2GB,但它必须与其他类型的资源共享该空间,例如内核本身、设备驱动程序、系统页表条目 (PTE)、和缓存的文件视图。
在 Vista 之前,32 位 Windows 上的内存管理器会计算在启动时为每种类型分配多少地址空间。其公式考虑了各种因素,其中主要因素是系统上的物理内存量。它分配给非分页池的容量在具有 512MB 的系统上从 128MB 开始,在具有略高于 1GB 或更多的系统上高达 256MB。在使用 /3GB 选项启动的系统上(以牺牲内核地址空间为代价将用户模式地址空间扩展到 3GB),最大非分页池为 128MB。前面显示的 Process Explorer 屏幕截图报告了在不使用 /3GB 开关启动的 2GB Windows XP 系统上的最大容量为 256MB。
32 位 Windows Vista 及更高版本(包括 Server 2008 和 Windows 7)中的内存管理器(没有 32 位版本的 Windows Server 2008 R2)不会静态地划分系统地址;相反,它根据不断变化的需求动态地将范围分配给不同类型的内存。但是,它仍然根据物理内存量设置非分页池的最大值,略高于物理内存的 75% 或 2GB,以较小者为准。以下是 2GB Windows Server 2008 系统上的最大值:
2GB 32 位 Windows Server 2008
64位Windows系统具有更大的地址空间,因此内存管理器可以静态地划分它,而不必担心不同类型可能没有足够的空间。 64 位 Windows XP 和 Windows Server 2003 将最大非分页池设置为略高于每 MB RAM 400K 或 128GB(以较小者为准)。下面是 2GB 64 位 Windows XP 系统的屏幕截图:
2GB 64 位 Windows XP
64 位 Windows Vista、Windows Server 2008、Windows 7 和 Windows Server 2008 R2 内存管理器通过设置最大值来匹配其 32 位对应项(如果适用 - 如前所述,没有 32 位版本的 Windows Server 2008 R2)大约 75% 的 RAM,但最大上限为 128GB,而不是 2GB。下面是 2GB 64 位 Windows Vista 系统的屏幕截图,该系统的非分页池限制与前面显示的 32 位 Windows Server 2008 系统类似。
2GB 32 位 Windows Server 2008
最后,这是 8GB 64 位 Windows 7 系统的限制:
8GB 64 位 Windows 7
下面的表格总结了不同 Windows 版本的非分页池限制:
32位 | 64位 | |
XP、服务器 2003 | 高达 1.2GB 内存:32-256 MB > 1.2GB 内存:256MB | 分钟(~400K/MB 内存,128GB) |
Vista、服务器 2008、Windows 7、服务器 2008 R2 | 分钟(约 RAM 的 75%,2GB) | 最小(~RAM 的 75%,128GB) |
Windows 8、服务器 2012 | 分钟(约 RAM 的 75%,2GB) | 最小(2x RAM,128GB) |
分页池限制
内核和设备驱动程序使用分页池来存储永远不会从 DPC 或 ISR 内部或在持有自旋锁时访问的任何数据结构。这是因为分页池的内容可以存在于物理内存中,或者如果内存管理器的工作集算法决定重新利用物理内存,则可以将其发送到分页文件,并在再次引用时按需故障返回到物理内存中。因此,分页池限制主要由内存管理器分配给分页池的系统地址空间量以及系统提交限制决定。
在 32 位 Windows XP 上,该限制是根据分配给其他资源(最显着的是系统 PTE)的地址空间大小来计算的,上限为 491MB。前面显示的 2GB Windows XP 系统的限制为 360MB,例如:
2GB 32 位 Windows XP
32位Windows Server 2003为页面缓冲池保留了更多的空间,因此其上限为650MB。
由于 32 位 Windows Vista 及更高版本具有动态内核地址空间,因此它们只是将限制设置为 2GB。因此,当系统地址空间已满或达到系统提交限制时,分页池将耗尽。
64 位 Windows XP 和 Windows Server 2003 将其最大值设置为非分页池限制的四倍或 128GB,以较小者为准。这里还是 64 位 Windows XP 系统的屏幕截图,它显示分页池限制恰好是非分页池的四倍:
2GB 64 位 Windows XP
最后,64 位版本的 Windows Vista、Windows Server 2008、Windows 7 和 Windows Server 2008 R2 只需将最大值设置为 128GB,允许分页池的限制跟踪系统提交限制。下面再次是64位Windows 7系统的截图:
8GB 64 位 Windows 7
以下是跨操作系统的分页池限制的摘要:
32位 | 64位 | |
XP、服务器 2003 | XP:最多 491MB Server 2003:最多 650MB | 分钟(4 * 非分页池限制,128GB) |
Vista、服务器 2008、Windows 7、服务器 2008 R2 | min(系统提交限制,2GB) | 分钟(系统提交限制,128GB) |
Windows 8、服务器 2012 | min(系统提交限制,2GB) | 分钟(系统提交限制,384GB) |
测试池限制
由于几乎每个内核操作都会使用内核池,因此耗尽它们可能会导致不可预测的结果。如果您想亲眼见证池运行不足时系统的行为方式,请使用Notmyfault工具。它具有一些选项,导致它以您指定的增量泄漏非分页池或分页池。如果您想更改泄漏率,则可以在泄漏时更改泄漏大小,并且 Notmyfault 在退出时会释放所有泄漏的内存:
除非您已做好可能丢失数据的准备,否则请勿在系统上运行此操作,因为当池耗尽时,应用程序和 I/O 操作将开始失败。如果驱动程序没有正确处理内存不足的情况(这被认为是驱动程序中的错误),您甚至可能会出现蓝屏。 Windows 硬件质量实验室 (WHQL) 强调使用驱动程序验证程序(Windows 内置的工具)的驱动程序,以确保它们能够容忍池外情况而不会崩溃,但您可能有尚未消失的第三方驱动程序通过此类测试或存在 WHQL 测试期间未发现的错误。
我在虚拟机中的各种测试系统上运行了 Notmyfault,看看它们的行为如何,没有遇到任何系统崩溃,但确实看到了不稳定的行为。例如,在 64 位 Windows XP 系统上的非分页池用完后,尝试启动命令提示符会导致出现以下对话框:
在我已经运行命令提示符的 32 位 Windows Server 2008 系统上,即使是更改当前目录和目录列表等简单操作,在非分页池耗尽后也会开始失败:
在一个测试系统上,我最终看到了这条错误消息,表明数据可能已丢失。我希望您永远不会在真实系统上看到此对话框!
用完分页池会导致类似的错误。以下是在分页池耗尽后尝试在 32 位 Windows XP 系统上从命令提示符启动记事本的结果。请注意 Windows 如何未能重绘窗口的标题栏以及每次尝试时遇到的不同错误:
以下是开始菜单的附件文件夹无法在分页池外的 64 位 Windows Server 2008 系统上填充的情况:
在这里,您可以看到系统提交级别(也显示在 Process Explorer 的“系统信息”对话框中)随着 Notmyfault 泄漏大块分页池并在 2GB 32 位 Windows Server 2008 系统上达到 2GB 最大值而快速上升:
当池耗尽时,即使系统不可用,Windows 也不会简单地崩溃,因为池耗尽可能是由极端工作负载峰值引起的临时情况,之后池被释放,系统恢复正常运行。然而,当驱动程序(或内核)泄漏池时,这种情况是永久性的,并且识别泄漏的原因变得很重要。这就是帖子开头描述的池标签发挥作用的地方。
跟踪水池泄漏
当您怀疑池泄漏并且系统仍然能够启动其他应用程序时,Poolmon(Windows 驱动程序工具包中的一个工具)会按池类型和传递到 ExAllocatePoolWithTag 调用中的标记显示分配数量和未完成的分配字节数。各种热键导致Poolmon按不同的列排序;要查找泄漏分配类型,请使用“b”按字节排序,或使用“d”按分配数和释放数之间的差异排序。下面是在一个系统上运行的 Poolmon,其中 Notmyfault 泄漏了 14 个分配,每个分配约 100MB:
识别出左栏中的有罪标签(在本例中为“泄漏”)后,下一步是找到使用它的驱动程序。由于标签存储在驱动程序映像中,因此您可以通过扫描驱动程序映像来查找相关标签来完成此操作。 Sysinternals 中的字符串实用程序将可打印字符串转储到您指定的文件中(默认情况下长度至少为三个字符),并且由于大多数设备驱动程序映像位于 %Systemroot%\System32\Drivers 目录中,因此您可以打开命令提示符下,切换到该目录并执行“strings * | findstr <标签>”。找到匹配项后,您可以使用 Sysinternals Sigcheck实用程序转储驱动程序的版本信息。以下是使用“Leak”查找驱动程序时的过程:
如果系统崩溃并且您怀疑是由于池耗尽所致,请将崩溃转储文件加载到 Windbg 调试器(包含在 Windows 调试工具包中),然后使用 !vm 命令进行确认。以下是 Notmyfault 已耗尽非分页池的系统上 !vm 的输出:
确认泄漏后,使用 !poolused 命令按与 Poolmon 类似的标签查看池使用情况。 !poolused 默认显示未排序的摘要信息,因此指定 1 作为按分页池使用情况排序的选项,指定 2 按非分页池使用情况排序:
在转储来源的系统上使用字符串,使用您发现导致问题的标签来搜索驱动程序。
到目前为止,在本博客系列中,我已经介绍了 Windows 中最基本的限制,包括物理内存、虚拟内存、分页和非分页池。下次我将讨论 Windows 支持的进程和线程数量的限制,这些限制是源自这些限制。
突破 Windows 的极限:进程和线程
这是我的“突破 Windows 极限”系列文章中的第四篇文章,该系列探讨了 Windows 中基本资源的边界。这次,我将讨论 Windows 支持的最大线程和进程数的限制。我将简要描述线程和进程之间的区别,调查线程限制,然后调查进程限制。我首先介绍线程限制,因为每个活动进程至少有一个线程(一个已终止的进程,但被另一个进程拥有的句柄引用,不会有任何线程),因此进程的限制直接受到以下上限的影响:限制线程。
与某些 UNIX 变体不同,Windows 中的大多数资源没有编译到操作系统中的固定上限,而是根据我已经介绍过的基本操作系统资源得出其限制。例如,进程和线程需要物理内存、虚拟内存和池内存,因此在给定的 Windows 系统上可以创建的进程或线程的数量最终由这些资源之一决定,具体取决于进程的运行方式或线程被创建以及首先遇到哪个约束。因此,如果您还没有阅读过,我建议您阅读前面的文章,因为我将提到保留内存、提交内存、系统提交限制以及我介绍过的其他概念。这是整个“挑战极限”系列的索引。虽然它们可以独立存在,但它们假设您按顺序阅读它们。
进程和线程
Windows 进程本质上是托管可执行映像文件执行的容器。它用内核进程对象表示,Windows 使用进程对象及其关联的数据结构来存储和跟踪有关映像执行的信息。例如,进程具有虚拟地址空间,该空间保存进程的私有和共享数据,并且可执行映像及其关联的 DLL 被映射到其中。 Windows 记录进程对资源的使用情况,以便通过诊断工具进行统计和查询,并在进程的句柄表中注册进程对操作系统对象的引用。进程使用称为令牌的安全上下文进行操作,它标识分配给进程的用户帐户、帐户组和权限。
最后,进程包括一个或多个实际执行进程中代码的线程(从技术上讲,进程不运行,线程运行),并且用内核线程对象表示。除了默认的初始线程之外,应用程序还创建线程的原因有多种:具有用户界面的进程通常会创建线程来执行工作,以便主线程保持对用户输入和窗口命令的响应;想要利用多个处理器实现可扩展性或者想要在线程被占用等待同步 I/O 操作完成时继续执行的应用程序也可以从多个线程中受益。
线程限制
除了有关线程的基本信息(包括其 CPU 寄存器状态、调度优先级和资源使用情况统计)之外,每个线程还分配有一部分进程地址空间(称为堆栈),线程在执行时可以将其用作临时存储传递函数参数、维护局部变量和保存函数返回地址的程序代码。为了避免不必要地浪费系统的虚拟内存,最初只分配或提交堆栈的一部分,而其余部分则简单地保留。由于堆栈在内存中向下增长,系统会将保护页放置在堆栈的已提交部分之外,从而在访问时触发额外内存的自动提交(称为堆栈扩展)。下图显示了当堆栈扩展时堆栈的提交区域如何向下增长以及保护页如何移动,以 32 位地址空间为例(未按比例绘制):
可执行映像的可移植可执行文件 (PE) 结构指定为线程堆栈保留和最初提交的地址空间量。链接器默认保留 1MB 并提交一页 (4K),但开发人员可以通过在链接程序时更改 PE 值或在调用CreateThread时更改单个线程的 PE 值来覆盖这些值。您可以使用 Visual Studio 附带的Dumpbin等工具来查看可执行文件的设置。以下是新 Visual Studio 项目生成的可执行文件的 Dumpbin 输出,其中包含 /headers 选项:
将数字从十六进制转换,您可以看到堆栈保留大小为 1MB,初始提交为 4K,使用新的 Sysinternals VMMap工具附加到该进程并查看其地址空间,您可以清楚地看到线程堆栈的初始提交页面,一个保护页,以及剩余的保留堆栈内存:
由于每个线程都消耗进程地址空间的一部分,因此进程对其可以创建的线程数量有一个基本限制,该限制是由其地址空间大小除以线程堆栈大小决定的。
32 位线程限制
即使线程没有代码或数据并且整个地址空间都可以用于堆栈,具有默认 2GB 地址空间的 32 位进程最多也可以创建 2,048 个线程。以下是在 32 位 Windows 上运行的Testlimit工具的输出,并使用 –t 开关(创建线程)确认该限制:
同样,由于部分地址空间已被代码和初始堆使用,因此并非所有 2GB 都可用于线程堆栈,因此创建的线程总数无法完全达到理论限制 2,048。
我将 Testlimit 可执行文件与大地址空间感知选项链接起来,这意味着如果它提供了超过 2GB 的地址空间(例如在使用 /3GB 或 /USERVA Boot.ini 选项或其等效 BCD 启动的 32 位系统上) Vista 上的选项以及后来增加的 userva),它将使用它。 32位进程在64位Windows上运行时被赋予4GB的地址空间,那么32位Testlimit在64位Windows上运行时可以创建多少个线程?根据我们到目前为止所讨论的内容,答案应该大约是 4096(4GB 除以 1MB),但实际上这个数字要小得多。以下是在 64 位 Windows XP 上运行的 32 位 Testlimit:
造成这种差异的原因在于,当您在 64 位 Windows 上运行 32 位应用程序时,它实际上是一个代表 32 位线程执行 64 位代码的 64 位进程,因此存在是一个64位线程栈和一个为每个线程保留的32位线程栈区域。 64 位堆栈的保留量为 256K(除了 Vista 之前的系统,初始线程的 64 位堆栈为 1MB)。由于每个 32 位线程都以 64 位模式开始其生命周期,并且启动时使用的堆栈空间超过一个页面,因此您通常会看到至少 16KB 的 64 位堆栈已提交。下面是 32 位线程的 64 位和 32 位堆栈的示例(标记为“Wow64”的是 32 位堆栈):
32 位 Testlimit 能够在 64 位 Windows 上创建 3,204 个线程,其中每个线程使用 1MB+256K 堆栈地址空间(同样,除了 Vista 之前的 Windows 版本上的第一个线程,它使用 1MB+1MB) ,正是您所期望的。然而,当我在 64 位 Windows 7 上运行 32 位 Testlimit 时,得到了不同的结果:
Windows XP 结果与 Windows 7 结果之间的差异是由 Windows Vista 中引入的地址空间布局(地址空间加载随机化 (ASLR))更加随机的性质造成的,这会导致一些碎片。 DLL 加载、线程堆栈和堆放置的随机化有助于防御恶意代码注入。从 VMMap 输出中可以看到,仍有 357MB 的地址空间可用,但最大的空闲块大小仅为 128K,小于 32 位堆栈所需的 1MB:
正如我所提到的,开发人员可以覆盖默认的堆栈保留。这样做的原因之一是当线程的堆栈使用量始终显着小于默认的 1MB 时,避免浪费地址空间。 Testlimit 将其 PE 映像中的默认堆栈保留设置为 64K,并且当您将 –n 开关与 –t 开关一起包含时,Testlimit 将创建具有 64K 堆栈的线程。以下是具有 256MB RAM 的 32 位 Windows XP 系统上的输出(我在小型系统上进行了此实验以突出显示此特定限制):
请注意不同的错误,这意味着地址空间不是这里的问题。事实上,64K 堆栈应该允许大约 32,000 个线程 (2GB/64K = 32,768)。在这种情况下,达到的极限是多少?查看可能的候选者,包括提交和池,没有给出任何线索,因为它们都低于极限:
只需查看内核调试器中的其他内存信息即可揭示所达到的阈值,即已耗尽的常驻可用内存:
常驻可用内存是可分配给必须保存在 RAM 中的数据或代码的物理内存。例如,非分页池和非分页驱动程序会对其进行计数,锁定在 RAM 中用于设备 I/O 操作的内存也是如此。每个线程都有一个用户模式堆栈(这就是我一直在讨论的内容),但它们也有一个内核模式堆栈,在内核模式下运行时使用,例如在执行系统调用时。当线程处于活动状态时,其内核堆栈会被锁定在内存中,以便线程可以在内核中执行无法出现页面错误的代码。
基本内核堆栈在 32 位 Windows 上为 12K,在 64 位 Windows 上为 24K。 14,225 个线程需要大约 170MB 的常驻可用内存,这正好对应于 Testlimit 未运行时系统上的可用内存量:
一旦达到常驻可用内存限制,许多基本操作就会开始失败。例如,当我双击桌面的 Internet Explorer 快捷方式时,出现以下错误:
正如预期的那样,当在具有 256MB RAM 的 64 位 Windows 上运行时,Testlimit 只能创建 6,600 个线程,大约是在具有 256MB RAM 的 32 位 Windows 上创建的线程的一半,然后就会耗尽常驻可用内存:
我之前提到“基本”内核堆栈的原因是,执行图形或窗口函数的线程在执行第一个调用时会获得一个“大”堆栈,在 32 位 Windows 上为 20K,在 64 位 Windows 上为 48K。 Testlimit 的线程不调用任何此类 API,因此它们具有基本的内核堆栈。
64 位线程限制
与 32 位线程一样,64 位线程也默认为堆栈保留 1MB,但 64 位进程具有更大的用户模式地址空间 (8TB),因此地址空间不应该成为问题创建大量线程。不过,驻留可用内存显然仍然是一个潜在的限制因素。 64 位版本的 Testlimit (Testlimit64.exe) 能够在 256MB 64 位 Windows XP 系统上使用或不使用 –n 开关创建大约 6,600 个线程,与 32 位版本创建的数量相同,因为它还达到常驻可用内存限制。然而,在具有 2GB RAM 的系统上,Testlimit64 只能创建 55,000 个线程,远低于以驻留可用内存为限制器时应能创建的数量 (2GB/24K = 89,000):
在这种情况下,初始线程堆栈提交导致系统耗尽虚拟内存并出现“分页文件太小”错误。一旦提交级别达到 RAM 的大小,线程创建的速度就会减慢,因为系统开始抖动,调出之前创建的线程堆栈,为新线程堆栈腾出空间,并且分页文件必须扩展。当指定 –n 开关时,结果是相同的,因为线程具有相同的初始堆栈承诺。
过程限制
Windows支持的进程数显然必须小于线程数,因为每个进程都有一个线程,而进程本身会导致额外的资源使用。在 2GB 64 位 Windows XP 系统上运行的 32 位 Testlimit 创建了大约 8,400 个进程:
查看内核调试器表明它达到了常驻可用内存限制:
如果进程相对于驻留可用内存的唯一成本是内核模式线程堆栈,则 Testlimit 将能够在 2GB 系统上创建远远超过 8,400 个线程。当 Testlimit 未运行时,该系统上的常驻可用内存量为 1.9GB:
将 Testlimit 使用的常驻内存量 (1.9GB) 除以它创建的进程数 (8,400),得出每个进程 230K 常驻内存。由于 64 位内核堆栈为 24K,因此还剩下大约 206K 未计算在内。剩下的费用从哪里来?创建进程时,Windows 会保留足够的物理内存来容纳进程的最小工作集大小。这可以保证进程无论如何都有足够的物理内存来保存足够的数据来满足其最小工作集。默认工作集大小恰好为 200KB,当您将“最小工作集”列添加到Process Explorer 的显示中时,这一事实就很明显:
剩余的大约 6K 是常驻可用内存,用于分配用于表示进程的额外不可分页内存。 32 位 Windows 上的进程将使用稍少的常驻内存,因为其内核模式线程堆栈较小。
与用户模式线程堆栈一样,进程可以使用SetProcessWorkingSetSize函数覆盖其默认工作集大小。 Testlimit 支持 –n 开关,当与 –p 结合使用时,会导致主 Testlimit 进程的子进程将其工作集设置为可能的最小值,即 80K。由于子进程必须运行以缩小其工作集,因此 Testlimit 在无法创建更多进程后会休眠,然后再次尝试为其子进程提供执行的机会。在具有 4GB RAM 的 Windows 7 系统上使用 –n 开关执行的 Testlimit 达到了常驻可用内存以外的限制:系统提交限制:
在这里,您可以看到内核调试器不仅报告已达到系统提交限制,而且在耗尽提交限制(系统提交限制为实际上,当分页文件被填充然后增长以提高限制时会命中几次):
Testlimit 运行之前的基线承诺约为 1.5GB,因此线程消耗了约 8GB 的承诺内存。因此,每个进程大约消耗 8GB/6,600,即 1.2MB。内核调试器的 !vm 命令的输出显示了每个活动进程分配的私有内存,证实了该计算:
前面描述的初始线程堆栈承诺的影响可以忽略不计,其余部分来自进程地址空间数据结构、页表条目、句柄表、进程和线程对象以及进程创建时创建的私有数据所需的内存。初始化。
多少个线程和进程就足够了?
那么问题的答案就是“Windows 支持多少线程?”以及“Windows 上可以同时运行多少个进程?”依靠。除了线程指定其堆栈大小和进程指定其最小工作集的方式的细微差别之外,确定任何特定系统上的答案的两个主要因素包括物理内存量和系统提交限制。无论如何,创建足够线程或进程以接近这些限制的应用程序应该重新考虑其设计,因为几乎总是有替代方法可以以合理的数量实现相同的目标。例如,可扩展应用程序的总体目标是保持运行的线程数量等于 CPU 数量(NUMA 更改此设置以考虑每个节点的 CPU),实现这一目标的一种方法是从使用同步 I/O 切换到使用异步 I/O 并依靠 I/O 完成端口来帮助将正在运行的线程数与 CPU 数相匹配。
突破 Windows 的极限:句柄
这是我的“突破 Windows 极限”系列文章的第五篇,其中我探讨了 Windows 管理的资源(例如物理内存、虚拟内存、进程和线程)数量和大小的上限。这是整个“挑战极限”系列的索引。虽然它们可以独立存在,但它们假设您按顺序阅读它们。
这次我将深入了解句柄的实现,以查找并解释它们的限制。句柄是表示应用程序与之交互的基本操作系统对象的打开实例的数据结构,例如文件、注册表项、同步原语和共享内存。有两个与进程可以创建的句柄数量相关的限制:系统为进程设置的最大句柄数量以及可用于存储句柄和应用程序通过其句柄引用的对象的内存量。
在大多数情况下,句柄的限制远远超出了典型应用程序或系统曾经使用的限制。然而,应用程序在设计时没有考虑到这些限制,可能会以开发人员意想不到的方式推动它们。出现一类更常见的问题是因为这些资源的生命周期必须由应用程序管理,就像虚拟内存一样,即使对于最优秀的开发人员来说,资源生命周期管理也充满挑战。未能释放不需要的资源的应用程序会导致资源泄漏,最终导致达到限制,从而导致该应用程序、其他应用程序或整个系统出现奇怪且难以诊断的行为。
与往常一样,我建议您阅读之前的文章,因为它们解释了本文引用的一些概念,例如分页池。
句柄和对象
Windows 的内核模式核心在%SystemRoot%\System32\Ntoskrnl.exe 映像中实现,由内存管理器、进程管理器、I/O 管理器、配置管理器(注册表)等各种子系统组成,它们分别是行政机关的所有部分。这些子系统中的每一个都使用对象管理器定义一种或多种类型来表示它们向应用程序公开的资源。例如,配置管理器定义密钥对象来表示打开的注册表项;内存管理器定义共享内存的Section对象; Executive 定义了Semaphore、M utant(互斥体的内部名称)和 Event同步对象(这些对象包装了操作系统内核子系统定义的基本数据结构); I/O 管理器定义File对象来表示设备驱动程序资源的打开实例,其中包括文件系统文件;进程管理器创建了我在上一篇《突破极限》文章中讨论过的线程和进程对象。每个版本的 Windows 都会引入新的对象类型,其中 Windows 7 总共定义了 42 个对象类型。您可以通过使用管理权限运行 Sysinternals Winobj实用程序并导航到对象管理器命名空间中的 ObjectTypes 目录来查看定义的对象:
当应用程序想要管理这些资源之一时,它首先必须调用适当的 API 来创建或打开该资源。例如,CreateFile函数打开或创建文件,RegOpenKeyEx函数打开注册表项,CreateSemaphoreEx函数打开或创建信号量。如果函数成功,Windows 将在应用程序进程的句柄表中分配一个句柄并返回句柄值,应用程序将其视为不透明,但实际上它是句柄表中返回句柄的索引。
有了句柄,应用程序就可以通过将句柄值传递给后续 API 函数(例如ReadFile、SetEvent、SetThreadPriority和MapViewOfFile )来查询或操作该对象。系统可以通过索引句柄表来查找句柄所引用的对象,以找到相应的句柄条目,其中包含指向该对象的指针。句柄条目还存储进程在打开对象时被授予的访问权限,这使系统能够确保不允许进程对它未请求权限的对象执行操作。例如,如果进程成功打开一个文件进行读访问,则句柄条目将如下所示:
如果进程尝试写入文件,该函数将失败,因为尚未授予访问权限,并且缓存的读访问权限意味着系统不必再次执行更昂贵的访问检查。
最大句柄数
您可以使用我在本系列中使用的 Testlimit 工具来探索第一个极限,以凭经验探索极限。可以在此处的Windows Internals 图书页面上下载它。为了测试进程可以创建的句柄数量,Testlimit 实现了 –h 开关,指示进程创建尽可能多的句柄。它通过使用CreateEvent创建事件对象,然后使用DuplicateHandle重复复制系统返回的句柄来实现此目的。通过复制句柄,Testlimit 避免创建新事件,它消耗的唯一资源是句柄表条目的资源。以下是在 64 位系统上使用 –h 选项进行 Testlimit 的结果:
然而,结果并不代表进程可以创建的句柄总数,因为系统 DLL 在进程初始化期间打开各种对象。您可以通过将句柄计数列添加到任务管理器或 Process Explorer 来查看进程的总句柄计数。本例中 Testlimit 显示的总数为 16,711,680:
当您在 32 位系统上运行 Testlimit 时,它可以创建的句柄数量略有不同:
其总句柄数也不同,为 16,744,448:
差异从何而来?答案在于负责管理句柄表的执行程序设置每个进程句柄限制以及句柄表条目的大小的方式。在 Windows 对资源设置硬编码上限的罕见情况之一中,执行器将 16,777,216 (16*1024*1024) 定义为进程可以分配的最大句柄数。任何在任何给定时间点打开超过一万个句柄的进程都可能设计不当或存在句柄泄漏,因此 1600 万个限制本质上是无限的,并且可以简单地帮助防止存在泄漏的进程影响进程。系统的其余部分。要了解为什么任务管理器显示的数字不等于硬编码的最大值,需要查看执行人员组织句柄表的方式。
句柄表条目必须足够大以存储授予访问掩码和对象指针。访问掩码是 32 位,但指针大小显然取决于它是 32 位还是 64 位系统。因此,句柄条目在 32 位 Windows 上为 8 字节,在 64 位 Windows 上为 12 字节。 64 位 Windows 在 64 位边界上对齐句柄条目数据结构,因此 64 位句柄条目实际上消耗 16 个字节。以下是 64 位 Windows 上句柄条目的定义,如使用 dt(转储类型)命令的内核调试器中所示:
输出显示该结构实际上是一个联合,有时可以存储除对象指针和访问掩码之外的信息,但这两个字段已突出显示。
执行程序根据需要在页面大小的块中分配句柄表,并将这些块划分为句柄表条目。这意味着一个页面(在 x86 和 x64 上都是 4096 字节)可以在 32 位 Windows 上存储 512 个条目,在 64 位 Windows 上可以存储 256 个条目。执行程序通过将硬编码最大值 16,777,216 除以页面中的句柄条目数来确定为句柄条目分配的最大页面数,结果在 32 位 Windows 上为 32,768,在 64 位 Windows 上为 65,536 。因为执行程序使用每个页面的第一个条目作为自己的跟踪信息,所以进程可用的句柄数实际上是 16,777,216 减去这些数字,这解释了 Testlimit 获得的结果:16,777,216-65,536 是 16,711,680 和 16,777,216-65,536-32,768是 16,744,448。
句柄和分页池
影响句柄的第二个限制是存储句柄表所需的内存量,执行程序从分页池中分配这些内存。执行程序使用三级方案,类似于处理器内存管理单元 (MMU) 管理虚拟到物理地址转换的方式,来跟踪它分配的句柄表页。我们已经看到了最低层和中间层的组织,它们存储实际的句柄表条目。顶层用作指向中层表的指针,并且在 32 位 Windows 上每页包含 1024 个条目。因此,对于 32 位 Windows,可以计算出存储最大句柄数所需的页面总数为 16,777,216/512*4096,即 128MB。这与任务管理器中显示的 Testlimit 的分页池使用情况一致:
在 64 位 Windows 上,顶级指针页中有 256 个指针。这意味着完整句柄表的分页池总使用量为 16,777,216/256*4096,即 256MB。查看 Testlimit 在 64 位 Windows 上的分页池使用情况证实了计算结果:
分页池通常足够大,足以容纳这些大小,但正如我之前所说,创建这么多句柄的进程几乎肯定会耗尽其他资源,如果它达到每个进程的句柄限制,它可能会自行失败因为它无法打开任何其他对象。
处理泄漏
句柄泄漏者的句柄计数会随着时间的推移而增加。句柄泄漏之所以如此阴险,是因为与 Testlimit 创建的句柄不同,这些句柄都指向同一个对象,泄漏句柄的进程也可能泄漏对象。例如,如果进程创建事件但无法关闭它们,则它将泄漏句柄条目和事件对象。事件对象消耗非分页池,因此除了分页池之外,泄漏还会影响非分页池。
您可以使用 Process Explorer 的句柄视图以图形方式发现进程正在泄漏的对象,因为它以绿色突出显示新句柄,以红色突出显示已关闭的句柄;如果您看到大量绿色和很少见的红色,那么您可能会看到泄漏。您可以通过打开命令提示符进程、在 Process Explorer 中选择进程、打开句柄视图下部窗格,然后在命令提示符中更改目录来观看 Process Explorer 的句柄突出显示操作。旧工作目录的句柄将以红色突出显示,新工作目录的句柄将以绿色突出显示:
默认情况下,Process Explorer 仅显示引用具有名称的对象的句柄,这意味着您不会看到进程正在使用的所有句柄,除非您从“视图”菜单中选择“显示未命名的句柄和映射” 。以下是命令提示符句柄表中的一些未命名句柄:
就像大多数错误一样,只有泄漏代码的开发人员才能修复它。如果您在可以托管多个组件或扩展的进程(例如 Explorer、服务主机或 Internet Explorer)中发现泄漏,那么问题是哪个组件对泄漏负责。弄清楚这一点可能会让您通过禁用或卸载有问题的扩展来避免问题,通过检查更新来解决问题,或者向供应商报告错误。
幸运的是,Windows 包含一个句柄跟踪工具,您可以使用它来帮助识别泄漏和负责的软件。它是在每个进程的基础上启用的,当激活时,执行程序会在创建或关闭每个句柄时记录堆栈跟踪。您可以通过使用应用程序验证器(可从 Microsoft 免费下载)或使用Windows 调试器(Windbg)来启用它。如果您希望系统从进程启动时开始跟踪进程的句柄活动,您应该使用应用程序验证器。无论哪种情况,您都需要使用调试器和!htrace调试器命令来查看跟踪信息。
为了演示实际跟踪,我启动了 Windbg 并附加到我之前打开的命令提示符。然后,我使用 -enable 开关执行 !htrace 命令来打开句柄跟踪:
我让进程继续执行并再次更改目录。然后我切换回 Windbg,停止进程的执行,并在不带任何选项的情况下执行 htrace,其中列出了自上一个 !htrace 快照(使用–snapshot选项创建)或从 when 句柄开始执行的所有打开和关闭操作跟踪已启用。以下是同一会话的命令的输出:
事件从最近操作到最少操作打印,因此从底部读取,命令提示符打开句柄 0xb8,然后关闭它,接下来打开句柄 0x22c,最后关闭句柄 0xec。如果在目录更改后刷新,Process Explorer 将以绿色显示句柄 0x22c,以红色显示 0xec,但可能不会看到 0xb8,除非它碰巧在该句柄的打开和关闭之间刷新。 0x22c 的堆栈显示,这是命令提示符 (cmd.exe) 执行其 ChangeDirectory 函数的结果。将句柄值列添加到 Process Explorer 可确认新句柄为 0x22c:
如果您只是寻找泄漏,则应该将 !htrace 与–diff开关一起使用,该开关仅显示自上次快照或跟踪开始以来的新句柄。执行该命令仅显示句柄 0x22c,如预期:
最后,第 9 频道对 Microsoft 升级工程师 Jeff Dailey 的采访,提供了有关调试句柄泄漏的更多技巧的精彩视频:https://channel9.msdn.com/posts/jeff_dailey/Understanding-handle -泄漏以及如何使用 htrace 来查找它们/
下次我将研究其他一些基于句柄的资源(GDI 对象和 USER 对象)的限制。这些资源的句柄由 Windows 子系统而不是执行体管理,因此使用不同的资源并具有不同的限制。
突破 Windows 的极限:USER 和 GDI 对象 – 第 1 部分
到目前为止,在“突破 Windows 的极限”系列中,我主要关注由 Windows 操作系统内核管理的资源,包括物理和虚拟内存、分页和非分页池、进程、线程和句柄。然而,在这篇文章和下一篇文章中,我将探讨由 Windows 窗口管理器管理的两种资源,即 USER 和 GDI 对象,它们代表窗口元素(如窗口和菜单)和图形结构(如笔、画笔和绘图表面)。就像我在之前的文章中讨论的其他资源一样,耗尽各种 USER 和 GDI 资源限制可能会导致不可预测的行为,包括应用程序故障和无法使用的系统。
与往常一样,我建议您在阅读这篇文章之前先阅读之前的文章,因为与 USER 和 GDI 资源相关的一些限制是基于我所介绍的限制。以下是我的其他《突破 Windows 极限》帖子的完整索引:
会话、窗口站和桌面
有几个概念使 USER 对象、GDI 对象和系统之间的关系更加清晰。首先是会议。会话代表交互式用户登录,具有自己的键盘、鼠标和显示器,并代表安全和资源边界。
会话概念首先是在 Windows NT 4 终端服务器版中的终端服务(现在称为远程桌面服务)中引入的,其中物理显示器、键盘和鼠标概念针对每个远程交互登录到系统的用户进行了虚拟化,而核心终端服务该功能内置于 Windows 2000 Server 中。在 Windows XP 中,利用会话来创建快速用户切换 (FUS) 功能,该功能允许您在同一物理显示器、键盘和鼠标上的多个交互式登录之间进行切换。
因此,会话可以与连接到系统的物理显示和输入设备连接,也可以与逻辑显示和输入设备(例如远程桌面客户端应用程序提供的设备)连接,或者处于断开连接状态,例如当您从具有快速用户切换的会话或终止远程桌面客户端连接而不注销会话。
每个进程都与特定会话唯一关联,当您将会话列添加到 Sysinternals Process Explorer时可以看到该会话。在这个屏幕截图中,我折叠了进程树以仅显示没有父进程的进程,该屏幕截图来自具有四个活动会话的远程桌面服务(RDS - 以前的终端服务器服务)系统:会话 0 是专用会话,其中系统进程在 Windows Vista 及更高版本上执行;第 1 场会议是我写这篇文章的会议;会话 2 是我同时从另一个系统登录的另一个用户帐户的会话;最后,会话 3 是远程桌面服务主动创建的会话,为下一次交互式登录做好准备:
由于每个进程都与特定会话相关联,并且操作系统通常只需要访问当前进程会话的会话特定数据,因此 Windows 在进程的虚拟地址空间中定义了进程会话数据的视图。这样,当系统在不同进程的线程之间切换时,它也会切换地址空间,从而切换当前会话视图。例如,当Session 0的Csrss.exe进程是当前进程时,地址空间映射包括系统地址空间(包含在每个进程的地址空间中)、Csrss的地址空间和Session 0地址空间。映射会话数据的内存区域称为会话视图空间或会话空间。当系统从 Session 1 的 Explorer 进程切换到线程时,映射会相应更改,而当系统从 Notepad 切换到线程时,Session 1 会话空间仍保持映射状态:
请注意,对于 32 位 Windows Vista 及更高版本,该数字并不完全正确,因为动态系统地址空间意味着会话空间不一定是连续的,并且可以根据这些系统的需要增长和缩小。
下一个概念是桌面,它是由窗口管理器定义的对象,用于表示虚拟显示,其中包括与桌面关联的窗口(请注意,这与资源管理器对桌面的定义不同,桌面是包含快捷方式和其他对象的用户目录用户放置在那里)。默认桌面被命名为“Default”,但应用程序可以创建其他桌面并将连接切换到逻辑显示器,Sysinternals Desktops实用程序使用它来创建最多四个用户可以在之间切换的虚拟桌面。
最后,为了支持与同一窗口管理器实例关联的多个虚拟显示器,窗口管理器定义了窗口站对象。一个窗口站与一个特定的会话相关联,一个会话可以有多个窗口站,但每个会话只有一个交互式窗口站,称为Winsta0,它可以连接物理或逻辑显示器、键盘和鼠标;其他窗口站本质上是“无头”的,对它们的支持只是为了隔离需要窗口管理器服务的进程,但事实并非如此。例如,系统为每个服务帐户创建非交互式窗口站,并将其与帐户中运行的进程关联起来,因为 Windows 服务不应显示用户界面。
您可以通过使用Sysinternals Winobj工具查看 \Windows 目录下的对象管理器命名空间来查看与会话 0 关联的窗口站(查看该目录需要以管理权限提升运行权限)。在这里您可以看到 Microsoft Windows 搜索服务创建的一个窗口站,用于在其中运行搜索过滤器、三个内置服务帐户(系统、网络服务和本地服务)中每一个的窗口站,以及会话 0 的交互式窗口站:
您可以在对象管理器命名空间的 Sessions 目录中看到与其他会话关联的窗口站。这是与我的登录会话关联的唯一窗口站,交互式 WinSta0 窗口站:
该图显示了一个系统的会话、窗口站和桌面之间的关系,该系统有一个用户在物理控制台上登录到会话 1,另一个用户通过远程桌面连接登录到会话 2,其中用户运行了虚拟桌面实用程序并切换了会话。显示到Desktop1。
除了与特定会话关联之外,进程还与特定窗口站和桌面关联,尽管进程可以在两者之间切换,线程也可以在桌面之间切换。因此,每个进程的关联都可以用这样的分层路径表示:“Session 1\WinSta0\Default”。在大多数情况下,您可以通过在 Process Explorer 的句柄视图中查看进程的句柄表来查看其打开的对象的名称,从而间接确定进程连接到哪个窗口站和桌面。 Explorer 进程的句柄表的屏幕截图显示它已连接到会话 1 的 WinSta0 上的默认桌面:
用户对象
掌握了基本概念后,让我们首先将注意力转向 USER 对象。USER 对象之所以得名,是因为它们代表用户界面元素,如桌面、窗口、菜单、光标、图标和加速表(菜单键盘快捷键)。尽管 USER 对象与特定桌面相关联,但它们必须可从会话的所有桌面访问,例如允许一个桌面上的进程注册可在其中任何一个桌面上输入的热键。因此,窗口管理器分配作用域为窗口站的 USER 对象标识符。
窗口管理器强加的一个基本限制是任何进程都不能创建超过 10,000 个 USER 对象。该限制试图防止单个进程耗尽与 USER 对象相关的资源,因为它使用可以创建过多对象的算法进行编程,或者因为它通过分配对象而不是在使用对象时删除它们而泄漏对象。您可以通过使用 –u 开关运行 Sysinternals Testlimit实用程序来轻松验证此限制,该实用程序指示 Testlimit 创建尽可能多的 USER 对象:
窗口管理器会跟踪进程分配的 USER 对象数量,当您将 USER Objects 列添加到 Process Explorer 的显示中时,您可以看到这些信息,以便您可以密切关注进程分配的对象数量。此屏幕截图显示,正如预期的那样,Windows 系统进程,包括 Lsass.exe(本地安全机构子系统)和 Svchost 等服务进程,不会分配 USER 对象,因为它们没有用户界面:
Process Explorer 在进程的进程属性对话框的性能页面上显示进程已分配的 USER 对象的数量:
USER 对象数量的一个基本限制来自于这样一个事实:在 Windows 的第一个版本中,它们的标识符是 16 位值,而 Windows 是 16 位的。当在更高版本中添加 32 位支持时,USER 标识符必须保留为 16 位值,以便 16 位进程可以与窗口和 32 位进程创建的其他 USER 对象进行交互。因此,65,535 (2^16) 是一个会话上可以创建的 USER 对象总数的限制(并且由于历史原因,窗口必须具有偶数标识符,因此每个会话最多可以有 32,768 个窗口) )。您可以通过使用 –u 开关运行 Testlimit 的多个副本来验证此限制,直到无法再创建为止。假设您已经运行的进程没有使用过多的对象,您应该能够运行 7 个副本,其中前 6 个分配 10,000 个对象,最后一个分配已分配的对象数量与 65,535 之间的差值:
执行此操作时,请确保准备好硬关闭系统电源,因为桌面可能会变得无法使用。许多操作,例如打开开始菜单的关闭菜单,都需要 USER 对象,当无法分配更多对象时,系统将以奇怪的方式运行。在 USER 对象耗尽后,我什至无法通过单击其关闭菜单按钮来终止正在运行的记事本进程。
到目前为止,我只讨论了与进程或窗口站可以分配的 USER 对象的绝对数量相关的限制,但还有由 USER 对象本身使用的存储引起的其他限制。每个桌面都有自己的内存区域,称为桌面堆,在桌面上创建的大多数 USER 对象都是从该区域分配的。由于桌面堆存储在会话空间中,并且 32 位地址空间限制了内核模式地址空间的数量,因此桌面堆的大小被限制在一个相对适中的数量。它们的大小也有所不同,具体取决于它们所适用的桌面类型以及系统是 32 位还是 64 位系统。
NT 调试博客中的Matthew Justice 的桌面堆概述和桌面堆第 2 部分 文章出色地记录了 Windows Vista SP1 中的桌面堆大小。下表总结了从 Windows Server 2008 R2 开始的各个 Windows 版本的大小:
互动桌面 | 非交互式桌面 | Winlogon桌面 | 断开桌面连接 | |
Windows XP 32 位 | 3MB | 512 KB | 128KB | 64KB |
Windows Server 2003 32 位 | 3MB | 512 KB | 128KB | 64KB |
Windows Server 2003 64 位 | 20MB | 768 KB | 192 KB | 96KB |
Windows Vista/Windows Server 2008 32 位 | 12MB | 512 KB | 128KB | 64KB |
Windows Vista/Windows Server 2008 64 位 | 20MB | 768 KB | 192 KB | 96KB |
Windows 7 32 位 | 12MB | 512 KB | 128KB | 64KB |
Windows 7/Windows Server 2008 R2 64 位 | 20MB | 768 KB | 192 KB | 96KB |
值得注意的是,Windows Vista 32 位的原始版本的交互式堆大小与以前的 32 位版本的 Windows 相同,为 3 MB。发布后,我们的遥测显示,一些用户偶尔会耗尽堆,可能是因为他们在具有更多内存的系统上运行更多应用程序,因此 SP1 将大小提高到 12 MB。还可以使用 Matthew 的文章中描述的注册表设置覆盖默认桌面堆大小。
在 Windows Vista 之前的 Windows 版本上,您可以使用 Microsoft桌面堆监视器工具来查看桌面堆的大小以及每个桌面堆的使用量。以下是该工具在 32 位 Windows XP 系统上的输出,显示交互式桌面(默认)上仅消耗了 5.6% 的堆 (172 KB):
该工具尚未针对 Windows Vista 进行更新,因为较新版本的 Windows 上的桌面堆大小较大,这意味着在达到其他 USER 对象限制之前桌面堆很少会耗尽。但是,您可以将 Testlimit 与 –u 和 –i 开关一起使用,以查看发生交互式桌面堆耗尽时系统的行为。开关组合让 Testlimit 创建具有 4 KB 额外类存储的窗口类数据结构,直到失败。这是我捕获上述桌面堆监视器输出后立即运行的 Testlimit 的输出。 2823 KB 加上桌面堆监视器表示已分配的 172 KB 大约等于 3 MB:
尽管无法确定较新系统上使用了多少堆,但当堆耗尽时,窗口管理器会向系统事件日志写入一个事件,这有助于解决窗口管理器问题:
这涵盖了 USER 对象限制。请继续关注第 2 部分,我将在其中讨论与窗口管理器 GDI 对象相关的限制。
突破 Windows 的极限:USER 和 GDI 对象 – 第 2 部分
上次,我介绍了两个关键窗口管理器资源之一(USER 对象)的限制以及如何测量其使用情况。这次,我将介绍其他关键资源,GDI 对象。与往常一样,我建议您在阅读这篇文章之前先阅读之前的文章,因为与 USER 和 GDI 资源相关的一些限制是基于我所介绍的限制。以下是我的其他《突破 Windows 极限》帖子的完整索引:
GDI 对象
GDI 对象表示图形设备接口资源,如字体、位图、画笔、笔和设备上下文(绘图表面)。与对待 USER 对象一样,窗口管理器将进程限制为最多 10,000 个 GDI 对象,您可以使用 –g 开关通过 Testlimit 进行验证:
您可以在 Process Explorer 进程属性对话框的 Performance 页面上查看单个进程的 GDI 对象使用情况,并将 GDI Objects 列添加到 Process Explorer 以观察跨进程的 GDI 对象使用情况:
与 USER 对象一样,16 位互操作性意味着 USER 对象具有 16 位标识符,每个会话将其限制为 65,535 个。这是当 Testlimit 在 Windows Vista 64 位系统上达到该限制时出现的桌面:
请注意左下角的“开始”按钮所属的位置,但任务栏的其余部分位于屏幕顶部。桌面变黑,侧边栏失去了大部分颜色。您的情况可能会有所不同,但您可以看到奇怪的事情开始发生,可能导致无法以可靠的方式与桌面交互。以下是我按下“开始”按钮时显示屏切换到的内容:
与 USER 对象不同,GDI 对象不是从桌面堆分配的;而是从桌面堆中分配的。相反,在未安装终端服务的 Windows XP 和 Windows Server 2003 系统上,它们从通用分页池中分配;在所有其他系统上,它们从每个会话的会话池中分配。
内核调试器的“!vm 4”命令转储一般虚拟内存信息,包括输出末尾的会话信息。在 Windows XP 系统上,它显示会话分页池未使用:
在没有终端服务的 Windows Server 2003 系统上,输出类似:
因此,这些系统上的 GDI 对象内存限制是分页池限制,如我之前的文章《推动 Windows 的限制:分页和非分页池》中所述。但是,当终端服务安装在同一个 Windows Server 2003 系统上时,从非零会话池使用情况可以看出 GDI 对象来自会话池:
上述输出中的 !vm 4 命令还显示会话分页池最大值和会话池大小,但会话分页池最大值和会话空间大小在 Windows Vista 及更高版本上不显示,因为它们是可变的。这些系统上的会话分页池使用量受到其可以增长到的地址空间量或系统提交限制(以较小者为准)的限制。以下是 Windows 7 系统上命令的输出,显示了当前会话分页池的使用情况(按会话):
正如您所期望的,主要的交互式会话(会话 1)正在消耗最多的会话分页池。
您可以使用带有“-g 0”开关的 Testlimit 工具来查看当用于 GDI 对象的存储空间耗尽时会发生什么情况。 -g 之后指定的数字是 Testlimit 分配的 GDI 位图对象的大小,但大小为 0 时 Testlimit 只是尝试分配尽可能大的对象。以下是 32 位 Windows XP 系统上的结果:
在未安装终端服务的 Windows XP 或 Windows Server 2003 上,您可以使用 Windows 驱动程序工具包 (WDK) 中的 Poolmon 实用程序来按池标记查看 GDI 对象分配。当 Testlimit 在 WIndows XP 系统上耗尽分页池时,Poolmon 的输出看起来像这样,当按分配的字节排序时(在 Poolmon 显示中键入“b”以按分配的字节排序),通过推断表明 Gh05 是位图的标记Windows Server 2003 上的对象:
在安装了终端服务的 Windows Server 2003 系统以及 Windows Vista 和更高版本上,您必须使用带有 /s 开关的 Poolmon 来指定要查看的会话。下面是在安装了终端服务的 Windows Server 2003 系统上执行的 Testlimit:
命令“poolmon /s1”显示对会话 1 贡献最大的分配的标签。您可以在顶部看到 Gh15 标签,表明位图分配使用了不同的池标签:
请注意 Testlimit 如何能够在 Windows XP 系统上分配大约 58 MB 的位图数据(该数字不考虑位图对象的 GDI 内部开销),但在 Windows Server 2003 系统上只能分配 10MB。较小的数字是因为 Windows Server 2003 终端服务器系统上的会话池只有 32 MB,这大约是 Poolmon 显示的归因于 Gh15 标记的内存量。 “!vm 4”的输出确认 Session1 的会话池已被消耗,并且后续从会话池分配 GDI 对象的尝试失败:
您还可以使用 !poolused 内核调试器命令来查看会话池的使用情况。首先,使用带有 /p 开关的 .process 命令以及连接到该会话的进程对象的地址来切换到正确的会话。要查看特定会话中正在运行哪些进程,请使用 !sprocess 命令。以下是同一 Windows Server 2003 系统上 !poolmon 的输出,其中 !poolused 的“c”选项按分配的字节对输出进行排序:
不幸的是,窗口管理器的堆标记和它们所代表的对象之间没有公共映射,但内核调试器的 !poolused 命令使用调试器安装目录中的 triage.ini 文件来打印有关标记的更多描述性信息。该命令报告 Gh15 是 GDITAG_HMGR_SPRITE_TYPE,这只是稍微有用一点,但其他的更清楚。
幸运的是,大多数 GDI 和 USER 对象问题仅限于达到每个进程 10,000 个对象限制的特定进程,因此不需要进行更高级的调查来找出哪个进程负责耗尽会话池或将 GDI 对象分配给耗尽分页池。
下次我将看一下系统页表条目(系统 PTE),这是另一个可能受到限制的关键系统资源,特别是在 Windows Server 2003 系统上的远程桌面会话上。