Windows 内存底层原理(第 1 篇)——虚拟地址变成物理 RAM 的瞬间:页错误的全过程

· 更新日期: · · Windows, 内存管理, VirtualAlloc, 页错误, VAD, 性能监视

更新记录(仅首版,2026年08月20日 发布)
首次发布
引用本文(DOI(已登记存档): 10.5281/zenodo.22176094)

以下 DOI 指向先前登记的存档,内容可能与当前正文不同。引用当前正文时,请使用本页网址。

Go Komura(2026)。《Windows 内存底层原理(第 1 篇)——虚拟地址变成物理 RAM 的瞬间:页错误的全过程》。小村软件有限公司。 https://comcomponent.com/zh-CN/blog/windows-memory-internals-page-fault/

DOI(已登记存档)
10.5281/zenodo.22176094
DOI(上次登记版本)
10.5281/zenodo.22176095

“明明 Commit 了 256MiB,Working Set 却没有增加同样多。”使用 VirtualAlloc 时产生的这个疑问,就是第 1 篇的出发点。

在普通的私有内存中,“已经 Commit”和“这一页已经放进物理 RAM”是两回事。在应用程序真正碰到某一页之前,Windows 会一直推迟物理页的分配。首次访问触发页错误时,内存管理器会检查 VAD、PTE、保护属性和后备存储,把需要的 RAM 一页一页地绑定上去。1

本文追踪“第一次碰到的那 1 个字节”抵达物理 RAM 的全过程。如果想先梳理 Working Set、Commit 这些数字的含义,请看入门篇《Windows 的“内存使用量”究竟表示什么——正确解读 Working Set、Private Bytes、Commit 与页面文件》。本系列不重新定义术语,而是从机制层面深挖为什么会得到那个数字。

“Windows 内存底层原理”系列全 3 篇

本系列按照取得物理页 → 追踪常驻与回收的流程 → 理解共享与私有化的顺序展开。

回 主题 本回追踪的内容
第 1 篇(本文) 虚拟地址与页错误 用 VirtualAlloc 分配的区域何时取得物理 RAM
第 2 篇 物理页的一生 离开 Working Set 的页如何状态迁移,以及页面文件的作用
第 3 篇 节对象与写时复制 DLL、文件映射和共享内存如何共享物理页

第 1 篇要回答的疑问只有一个。

已经 Commit 的虚拟地址,究竟在哪一瞬间变成物理 RAM。

开始阅读前 内容
目标读者 希望从机制上理解内存使用量、刚启动时的页错误、0xC0000005、VMMap 与 PerfMon 数值的开发者和运维人员
前提环境 Windows 10/11 或现行的 Windows Server
前提知识 指针与 VirtualAlloc 的基础
难度 中级。不需要页表位布局或内核调试器的经验

本文会提到内部结构的名称,但不以依赖某个特定 Windows 内部版本的未公开布局为前提。

图中实线表示始终成立的关系,虚线表示带条件的关系(成立条件写在详情页各关系的说明中)。关系的完整列表(共 14 条,附依据与确信度)以及主要概念的定义,汇总在知识地图详情页(日文)。数据:JSON-LD / Turtle

1. 先看结论

在普通的私有内存中,把下面三个阶段分开看,就能看清 Commit 与 Working Set 的区别。

  1. Reserve 是在虚拟空间中占住地址的阶段。 它预留这段范围,但不在 RAM 或页面文件中分配物理保存区域。1
  2. Commit 是系统承诺将来能够保存这些内容的阶段。 它收取提交费用并增加 Commit Total,但通常还不会把物理 RAM 绑定到全部页上。12
  3. Touch 是真正需要物理页的阶段。 首次访问会触发页错误,如果访问是合法的,内存管理器就分配物理页并重新执行该指令。

MEM_COMMIT 不是“立刻给我分配 RAM”的命令。但它也不是空头承诺,而是整个系统作出的承诺:将来能够用 RAM 或合适的后备存储保存这些内容。“已 Commit 的页初始内容为零”和“物理页要到首次访问才分配”这两件事并不矛盾。1

另外,“Reserve/Commit 只是往 VAD 里写一笔”这种理解也不准确。Reserve 主要会创建表示范围与属性的 VAD;Commit 则会增加系统的 Commit Total,并记录该范围的提交状态。页表的中间层级和各个 PTE,要到真正需要时才延迟构建。

想了解的内容 阅读的章节
Reserve、Commit、Touch 各自改变了什么 第 2~3 节
VAD、PTE、TLB 分别判断什么 第 4~6 节
想区分正常的错误与 I/O 等待、异常 第 7~9 节
想用手头的数值验证 第 10~11 节
Reserve、Commit 与首次访问分别会发生什么MEM_RESERVE 把范围和属性记录到 VAD,MEM_COMMIT 消耗 Commit Total 并承诺保存,首次访问的页错误分配物理页并把它加入 Working Set1. MEM_RESERVE2. MEM_COMMIT3. 首次访问(Touch)把范围与属性记录到 VAD消耗 Commit Total(尚无物理页)页错误把已置零的物理页绑定到 PTE加入 Working Set 并重新执行指令

图1:Reserve、Commit、Touch 是不同的事件。物理 RAM 被绑定上去,是在最后的首次访问时。

2. 追踪虚拟页的三张登记表

Windows 持有的这几张登记表,用不同的单位看同一块内存。首先要把范围、虚拟页、物理页这三者分开。

登记表 单位 作用
VAD 虚拟地址范围 管理该区域是什么、Reserve/Commit、保护属性以及与节的对应关系
页表 / PTE 虚拟页 表示到当前物理页的转换,或者尚未实体化的状态
PFN 数据库 物理页 跟踪每个 RAM 页的所有者、引用和状态

VAD 持有范围的信息,PTE 持有虚拟页的信息,PFN 数据库持有物理页的信息。页错误处理程序会把它们对照起来,判断这次访问能否继续下去。

从虚拟地址到物理 RAM 的三张登记表虚拟地址由 VAD 按范围管理、由 PTE 按虚拟页管理,而 PTE 转换到的物理页则由 PFN 数据库按物理页跟踪判定 Reserve/Commit 与保护属性有效的转换虚拟地址VAD(范围登记表)PTE(虚拟页登记表)PFN 数据库(物理页登记表)物理 RAM 页

图2:粒度各不相同的三张登记表。错误处理会对照 VAD 与 PTE,并把结果写入 PFN 一侧。

本文的主角是 VAD 和 PTE。PFN 数据库将在第 2 篇从物理页一侧来看。

3. Reserve、Commit、Touch 是不同的事件

3.1. Reserve —— 占住地址

首先预留一段 256MiB 的连续虚拟地址范围。

void* base = VirtualAlloc(
    nullptr,
    256ull * 1024 * 1024,
    MEM_RESERVE,
    PAGE_NOACCESS);

此时发生的只有一件事:在进程的虚拟空间里占下这段地址,使它不会被其他分配占用。MEM_RESERVE 既不在 RAM 中、也不在页面文件中分配物理保存区域。1

64 位进程可以使用极大的虚拟空间,因此先 Reserve 一大段范围、之后只对需要的部分执行 Commit 的设计是切实可行的。

3.2. Commit —— 承诺能够保存

接着对已预留的范围执行 Commit。

void* committed = VirtualAlloc(
    base,
    256ull * 1024 * 1024,
    MEM_COMMIT,
    PAGE_READWRITE);

成功之后,系统的 Commit Total 会增加,通常这份承诺量也会体现在进程的 Private Bytes 上。即便如此,256MiB 的物理页也不会一次性就位。普通的页在首次访问之前,仍然不会真正分配物理内存。12

那么 Commit 的意义在哪里?在于当系统无法承担这份承诺时,可以不必等到使用内存的途中,而是在 Commit 这一刻就返回失败。

3.3. Touch —— 真正需要物理页

最后,用下面这条赋值语句第一次向首页写入。

static_cast<unsigned char*>(base)[0] = 1;

CPU 试图把虚拟地址转换成物理地址,但 PTE 中还没有指向有效物理页的转换。于是这里发生页错误。

取得控制权的内存管理器判定这是“对已 Commit 且可写的私有页的首次访问”,于是取得一个已置零的物理页,把它绑定到 PTE,并加入 Working Set。随后让失败的那条写入指令重新执行一次。

从应用程序看只是一次普通赋值,内部却跑完了在赋值途中进入内核、分配物理页、再回到同一条指令这样一整套处理。

4. VAD —— 虚拟空间的范围登记表

VAD 是 Virtual Address Descriptor 的缩写,Windows 把进程正在使用的地址范围作为 VAD 树来管理。用 WinDbg 的 !vad 命令,可以查看起始与结束 VPN、Commit、保护属性、Private/Mapped、Control Area 等信息。3

VAD 表示的典型信息如下。

  • 地址范围的起点与终点
  • Private、Mapped、Image 等种类
  • Reserve/Commit 的状态
  • 读取、写入、执行、Copy-on-Write 等保护属性
  • 与文件或节的对应关系
  • 保护页之类的特殊属性

之所以按范围为单位保存,是为了减少管理上的浪费。 256MiB 换算成 4KiB 的页,就是 65,536 页。

Windows 不会一开始就为所有页建立完整的管理结构,而是用 VAD 记录“这段连续范围属于一次预留”。在此基础上,再从真正需要的页开始逐个具体化。

4.1. 找到 VAD 并不等于一定能恢复

“在 VAD 里就能解决错误,不在 VAD 里就是访问违规”这种说法作为入门很方便,但过于简化了。即使找到了 VAD,在下面这些情况下也无法照常继续访问。

  • 只做了 Reserve,目标页并未 Commit
  • 页的保护属性是 PAGE_NOACCESS
  • 向只读页执行了写入
  • 从不可执行的页取指执行
  • 第一次碰到了保护页
  • 访问超出了节的有效范围

反过来,即使 PTE 无效,只要能从 VAD 和 PTE 的软件状态判断出这是合法访问,就可以按按需置零、从 Transition 恢复、页面调入、CoW 等方式解决。准确地说,答案是把 VAD、PTE、保护属性和访问类型合在一起判定。

5. 页表与 TLB

应用程序持有的指针是虚拟地址。CPU 要访问 RAM,就必须把虚拟页号转换成物理页号。承担这项转换的多级表就是页表,其末端条目就是 PTE(Page Table Entry)。

有效的 PTE 在概念上保存着 PFN、读写执行的保护属性、是否允许用户模式访问、Accessed/Dirty 等信息。实际的位布局取决于 CPU 和 Windows 的版本。

不过,每次都遍历页表实在太慢,因此 CPU 会把最近的转换结果缓存到 TLB(Translation Lookaside Buffer)中。地址转换按下面的顺序进行。

  1. TLB 中有转换,且这次访问符合其保护属性时,直接使用该结果。
  2. TLB 中没有转换时,由 CPU 遍历页表。
  3. 存在有效的 PTE 且符合保护属性时,登记到 TLB 并继续执行。
  4. 没有有效的转换,或者存在保护违规时,走向页错误的入口。保护检查在转换来自 TLB 时同样会执行。

这里容易混淆的一点是,TLB 未命中和页错误是两回事。

情况 接下来会发生什么
TLB 中有转换,且符合保护属性 用缓存的转换继续执行
TLB 中没有转换,但存在有效的 PTE 且符合保护属性 通过页表遍历取得转换并继续执行
没有有效的转换,或者违反保护属性 走向页错误的入口

保护检查在 TLB 命中时同样起作用。向只读页写入、在不可执行的页上取指执行,即使转换已经缓存也会产生错误。向 CoW 页写入之所以能触发错误,也是同样的道理。

地址转换的流程与页错误的入口即使 TLB 中有转换,只要不符合保护属性就会走向页错误的入口。TLB 中没有转换时先遍历页表,PTE 有效且符合保护属性就登记到 TLB 并继续执行,无效或保护违规时走向页错误的入口有符合保护违规没有是无效或保护违规内存访问TLB 中有转换吗?符合保护属性吗?用该转换继续执行走向页错误的入口页表遍历PTE 有效且符合保护属性?登记到 TLB 并继续执行(无错误)

图3:TLB 未命中可以通过页表遍历解决。走向页错误的是转换无效或保护违规的情况,而保护违规在 TLB 命中时也会发生。

5.1. 无效的 PTE 并不只是一格空白

虽说 PTE 无效,但并不意味着里面是空的。Windows 会根据无效 PTE 的软件状态,区分出下面这样的情况。

  • 一次都没有实体化过的按需置零页
  • 仍留在 RAM 中的 Transition 页
  • 引用 Prototype PTE 的共享页
  • 已保存到页面文件的私有页
  • 保护违规或无效区域

CPU 的工作只到判断出“这不是通常的有效转换”并交给内核为止,再往后的含义解读由内存管理器负责。

6. 页错误的全过程

下面分六个阶段,追踪对已 Commit 的私有页的首次写入。

  1. CPU 试图写入。
    它查找 TLB 和页表,但目标 PTE 中没有有效的 PFN。
  2. CPU 产生页错误。
    它把出错的虚拟地址、读写执行的类别、用户态还是内核态、是缺少转换还是保护违规,一并交给内核。
  3. 内存管理器检查 VAD 和 PTE。
    判断是否已 Commit、是否符合保护属性,以及属于按需置零、Transition、共享、页面调入、CoW、异常中的哪一种。
  4. 如果是按需置零,就取得一个已置零的物理页。
    为了不泄露其他进程的数据,新交出去的页必须是全零的。
  5. 更新 PTE 与 PFN 管理信息。
    把 PFN 和保护属性写入 PTE,把物理页置为 Active,并加入进程的 Working Set。
  6. 重新执行失败的指令。
    由于已经正常解决,用户模式收不到异常,应用程序按普通赋值继续处理。

ETW 的页错误事件也把 Transition、Demand Zero、Copy-on-Write、Guard Page、Hard Page Fault、Access Violation 记录为不同的种类。4

也就是说,页错误这个词本身并不等于“异常”。它是 CPU 无法通过常规路径完成转换时,请求操作系统作出判断的公共入口。

页错误解决方式的分支内存管理器判定 VAD、PTE、保护属性和访问类型,再分流到按需置零、重新挂接 RAM 内的页、从后备存储读取的硬错误、写时复制、保护页通知或异常首次访问仍留在 RAM 中需要读磁盘CoW 写入保护页无法解决发生页错误判定 VAD、PTE、保护属性与类别按需置零(软错误)从 Standby 等重新挂接(软错误)硬错误(磁盘 I/O)复制后替换 PTE解除保护并通知异常(0xC0000005 等)

图4:从同一个入口进来的错误,会按判定结果分成六种结局。保护页的细节在第 9 节讨论。

7. 按需置零 —— 不读磁盘的软错误

按需置零(demand-zero)是第一次碰到已 Commit 的私有页时发生的典型软错误。Microsoft 关于 Working Set 的说明,也把“进程首次引用已分配的虚拟页”列为软错误的例子。5

按需置零有以下特点。

  • 不需要从磁盘读取原始数据
  • 初始内容为零
  • 绑定一个可用的物理页
  • Working Set 和累计的 Page Fault Count 会增加
  • 仅这一处理不会让 Memory\\Pages Input/sec 增加

因此,即使刚启动时 Page Faults/sec 飙升,仅凭这一点也不能说存储已经堵住了。

7.1. 延迟分配是在 RAM 与首次访问开销之间做交换

即使 Commit 了 256MiB,如果实际只用到 8MiB,那么让剩下的 248MiB 不必占用 RAM 的延迟分配就是合理的。代价是首次访问要额外承担错误处理的开销。

对延迟要求严苛的处理,有时会选择在开始前逐页触碰的“预错误”做法。不过这并不是免费的优化,而是提前完成首次访问的处理,同时也提前抬高 RAM 常驻量的一种取舍。

8. 软错误与硬错误

判断的分界线是是否需要对后备存储发起读取 I/O。仅看页错误的次数,分不出这个区别。

8.1. 软错误

软错误是无需对后备存储发起读取 I/O 就能解决的错误。典型的例子如下。

  • 按需置零
  • 重新挂接仍留在 Standby/Transition 中的页
  • 挂接位于其他进程 Working Set 中的共享页
  • 挂接已经预读进来的页
  • 原页仍然常驻时的 Copy-on-Write

这类处理仍有内核态切换、加锁、PTE/PFN 更新、TLB 一致性维护等 CPU 开销,但没有存储等待。5

8.2. 硬错误

另一方面,所需的页在 RAM 中哪里都找不到、必须从后备存储读取时,就是硬错误。此时的读取来源并不只有页面文件。

  • 被换出到页面文件的私有页
  • 内存映射文件
  • EXE 或 DLL 的映像
  • 文件缓存所引用的数据文件

ETW 的 HardFault 事件包含 FileObject、ReadOffset、ByteCount,可以据此追踪实际的读取来源。6

因此,硬错误并不等于“读了 pagefile.sys”。

一旦需要读取后备存储,请求就会进入 Windows 的 I/O 栈。IRP 以及发起与完成的流程见《Windows I/O 底层原理(第 1 篇)》,与文件缓存的汇合见《Windows I/O 底层原理(第 4 篇)》。页在 RAM 中时,内存管理器自己就能返回;不在时就要发起 I/O,并让出错的线程一直等到 I/O 完成。

9. 无法解决的错误会变成异常

即使检查了 VAD 和 PTE,仍然不能按合法分配、页面调入或 CoW 解决的错误,会作为异常送到用户模式。

9.1. 会变成访问违规的情况

其代表就是 STATUS_ACCESS_VIOLATION,异常代码为 0xC0000005。它在读取、写入或执行无效地址时发生,第 1 个异常参数表示访问类别,第 2 个参数表示违规地址。7

典型的发生模式如下。

  • 读取 NULL、已释放或越界的地址
  • 向只读页写入
  • 从被 DEP/NX 标记为不可执行的页取指执行
  • 碰到尚未 Commit 的 Reserve 范围

9.2. 保护页用作一次性的通知

另外,PAGE_GUARD 的含义略有不同。它是只通知一次访问的机制,会产生 STATUS_GUARD_PAGE_VIOLATION,用于栈扩展等场景。8

正常的延迟分配、页面调入、CoW、保护页通知,以及最终的访问违规,从 CPU 的角度看都汇集到同一个页错误入口。决定结局的,是 VAD、PTE、保护属性和访问类别的组合。

10. 亲自动手确认

实验的目的是把 Commit 增加的阶段和 Working Set 增加的阶段分开观测。

下面这个 C++ 程序会 Reserve 256MiB、执行 Commit、向每一页写入 1 个字节,最后 Release。每个阶段都会等待 Enter 键,这时就可以查看 VMMap 和 PerfMon 的数值。

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <psapi.h>

#include <cstdio>
#include <cstdlib>

#pragma comment(lib, "Psapi.lib")

constexpr SIZE_T kSize = 256ull * 1024 * 1024;

void PrintMemory(const char* stage)
{
    PROCESS_MEMORY_COUNTERS_EX c{};
    c.cb = sizeof(c);
    if (!GetProcessMemoryInfo(
            GetCurrentProcess(),
            reinterpret_cast<PROCESS_MEMORY_COUNTERS*>(&c),
            sizeof(c))) {
        std::printf("GetProcessMemoryInfo failed: %lu\n", GetLastError());
        return;
    }

    std::printf(
        "%-10s WS=%zu MiB  Private=%zu MiB  Faults=%lu\n",
        stage,
        c.WorkingSetSize / 1024 / 1024,
        c.PrivateUsage / 1024 / 1024,
        c.PageFaultCount);
}

void Pause(const char* message)
{
    PrintMemory(message);
    std::puts("Press Enter...");
    (void)std::getchar();
}

int main()
{
    SYSTEM_INFO si{};
    GetSystemInfo(&si);
    std::printf("PID=%lu, page=%lu bytes\n",
                GetCurrentProcessId(), si.dwPageSize);

    void* base = VirtualAlloc(nullptr, kSize, MEM_RESERVE, PAGE_NOACCESS);
    if (!base) {
        std::fprintf(stderr, "Reserve failed: %lu\n", GetLastError());
        return EXIT_FAILURE;
    }
    Pause("reserved");

    if (!VirtualAlloc(base, kSize, MEM_COMMIT, PAGE_READWRITE)) {
        std::fprintf(stderr, "Commit failed: %lu\n", GetLastError());
        VirtualFree(base, 0, MEM_RELEASE);
        return EXIT_FAILURE;
    }
    Pause("committed");

    auto* bytes = static_cast<volatile unsigned char*>(base);
    for (SIZE_T offset = 0; offset < kSize; offset += si.dwPageSize) {
        bytes[offset] = 1;
    }
    Pause("touched");

    if (!VirtualFree(base, 0, MEM_RELEASE)) {
        std::fprintf(stderr, "Release failed: %lu\n", GetLastError());
        return EXIT_FAILURE;
    }
    Pause("released");
}

在 Visual Studio 的 x64 Native Tools Command Prompt 中,可以用下面的命令编译。

cl /std:c++20 /EHsc /W4 memory_fault_demo.cpp

10.1. VMMap 里要看什么

VMMap 是按类别显示已预留虚拟内存、Commit、Working Set、Private、Shareable 的工具。9 各阶段预期的变化如下。

阶段 预期的变化
Reserve Address Space 的 Size 会增加,但 Commit/WS 不会增加同样的量
Commit Private 的 Commit 增加约 256MiB
Touch Working Set 与 Private WS 大幅增加,Fault Count 也会增加
Release 目标范围消失,Commit 与 WS 下降

要看的是阶段之间的变化,而不是数值是否吻合

实际数值会随运行时、安全产品、内存压力和观测时机而变化。请不要盯着是不是正好等于 256MiB,而要看阶段之间朝哪个方向变动。

10.2. 用 PerfMon 区分软错误与硬错误

在 PerfMon 中,把下面这些计数器放到同一条时间轴上。

  • Process(<目标进程>)\\Page Faults/sec
  • Memory\\Pages Input/sec
  • Memory\\Page Reads/sec
  • Memory\\Available MBytes
  • Process(<目标进程>)\\Working Set - Private
  • Process(<目标进程>)\\Private Bytes

Process\\Page Faults/sec 同时包含软错误和硬错误。而 Memory\\Pages Input/sec 是为解决硬错误而从磁盘读入的页数。10

在这个程序的 Touch 阶段,Page Faults/sec 即使飙升,Pages Input/sec 也不应大幅增加。因为新 Commit 的页是通过按需置零实体化的,不需要从磁盘读取原始数据。

同名进程还要用 PID 确认

另外,存在多个同名进程时,PerfMon 中 process#1 这类编号可能在重启后改变。请与显示 PID 的计数器对照,或者用 Process V2、ETW/WPA 按 PID 来识别。

11. 实际工作中要避免的三种误读

11.1. “Commit 增加了,所以是 RAM 泄漏”

Commit 是保存内容的承诺量,尚未 Touch 的页可能并不常驻在 RAM 中。判断是否泄漏时,要看 Private Bytes 随时间的走势、分配的构成明细,以及处理结束后是否回到基准值。

11.2. “Page Faults/sec 高,所以磁盘慢”

软错误不伴随磁盘 I/O。要把 Page Faults/sec、Pages Input/sec 和存储等待分开来看,必要时用 ETW 的 HardFault 事件一直追到读取来源文件和调用栈。

11.3. “清空 Working Set 就能修好泄漏”

把页移出 Working Set,并不会释放 Commit 和所有权。页只是转移到 Standby 或 Modified,之后再通过错误回来而已。要修好泄漏,必须由分配方执行 VirtualFree、释放堆、销毁对象等操作。

被移出的那个物理页究竟去往何处,将在第 2 篇追踪。

12. 小结

  • MEM_RESERVE 占住虚拟地址范围,但不分配 RAM 或页面文件中的物理区域。1
  • MEM_COMMIT 消耗 Commit,保证将来能够保存内容,但普通的物理页要到首次访问才分配。12
  • VAD 是范围的登记表,PTE 是虚拟页的登记表,PFN 数据库是物理页的登记表。
  • TLB 未命中不是页错误。只要存在有效的 PTE,仅靠页表遍历就能解决。
  • 按需置零、从 Transition 恢复、挂接共享页,都是无需磁盘 I/O 就能解决的软错误。5
  • 需要从页面文件、DLL、EXE、映射文件读取时,就是硬错误。6
  • 检查 VAD、PTE、保护属性后仍无法解决时,就会变成 0xC0000005 之类的异常。7
  • 判断性能时不要只看 Page Faults/sec,而要把 Pages Input/sec、Available、Working Set、Private Bytes 和存储等待放在同一条时间轴上看。

接下来请看第 2 篇《物理页的一生:五份列表与页面文件的真相》。

把 Commit 这份承诺变成物理页之后,这一页如果离开 Working Set 会去往何处,我们将从 PFN 数据库和各个页列表来追踪。

相关文章

相关咨询领域

小村软件有限公司承接 Windows 应用程序的内存使用量调查、访问违规、启动延迟、分页,以及原生代码的缺陷分析。

参考链接

  1. Microsoft Learn, VirtualAlloc function. 关于 MEM_RESERVE 只预留虚拟地址范围而不分配物理存储、MEM_COMMIT 会对系统全局内存和页面文件收取提交费用、已 Commit 的页初始内容为零、实际的物理页要到被访问时才分配。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8

  2. Microsoft Learn, PERFORMANCE_INFORMATION structure. 关于 CommitTotal 是当前系统已 Commit 的页数,CommitLimit 是不扩展页面文件即可 Commit 的上限。 ↩ ↩2 ↩3

  3. Microsoft Learn, !vad (WinDbg). 关于 !vad 显示 VAD 树,可以查看起始与结束 VPN、Commit、Mapped/Private、保护属性、Control Area 等信息。 ↩

  4. Microsoft Learn, PageFault_TypeGroup1 class. 关于 ETW 会区分记录 Transition Fault、Demand Zero Fault、Copy-on-Write、Guard Page Fault、Hard Page Fault 和 Access Violation。 ↩

  5. Microsoft Learn, Working Set. 关于软错误无需访问后备存储即可解决,会在其他进程的 Working Set、Transition、首次引用的按需置零等情形下发生。 ↩ ↩2 ↩3

  6. Microsoft Learn, PageFault_HardFault class. 关于 HardFault 事件包含 FileObject、ReadOffset、ByteCount、VirtualAddress 和线程 ID,可用于追踪读取来源。 ↩ ↩2

  7. Microsoft Learn, Access Violation C0000005. 关于 0xC0000005 在读取、写入或执行无效内存地址时发生,异常参数会给出访问类别和违规地址。 ↩ ↩2

  8. Microsoft Learn, Creating Guard Pages. 关于 PAGE_GUARD 提供针对页访问的一次性通知,并产生 STATUS_GUARD_PAGE_VIOLATION。 ↩

  9. Microsoft Learn, VMMap - Sysinternals. 关于 VMMap 可以把已 Commit 的虚拟内存按类别拆解,并显示各类别的 Working Set 和详细的地址映射。 ↩

  10. Microsoft Learn, Performance Analysis of Logs (PAL) Tool. 关于 Memory\\Pages Input/sec 表示为解决硬页错误而从磁盘读入的页数。 ↩

共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。

与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。

本文与以下服务页面相关联,欢迎从最接近的入口查看。

常见问题

汇总了咨询这一主题时常见的问题。

用 VirtualAlloc 执行 MEM_COMMIT 的那一刻就会拿到 RAM 吗?
在普通的私有内存中,Commit 时会消耗系统的提交余量,但对应的物理页要到第一次访问才会分配。通过写入第一次碰到的页,是在按需置零错误的处理过程中获得物理页的。
页错误是否意味着异常或性能问题?
不是。像按需置零、从 Standby 恢复这类不伴随磁盘 I/O 的软错误属于正常运行。判断性能时,不能只看 Page Faults/sec,还要同时观察 Pages Input/sec、存储等待和 Available MBytes。
TLB 未命中和页错误是一回事吗?
是两回事。即使 TLB 中没有转换结果,只要页表中的 PTE 有效,CPU 也只是遍历页表重新登记转换而已。只有 PTE 无效或存在保护违规时,才会走向页错误的入口。
只要 VAD 中有这个地址范围,就不会发生访问违规吗?
不一定。除了 VAD 是否存在,内存管理器还会评估是 Reserve 还是 Commit、读写执行的保护属性、保护页、PTE 的状态等。无法解决时就会变成 0xC0000005 之类的异常。
Page Faults/sec 很高就说明 RAM 不足吗?
仅凭这一点无法判断。Page Faults/sec 中也包含大量软错误。需要把 Memory\Pages Input/sec、Memory\Page Reads/sec、Available MBytes 和磁盘等待时间放在同一条时间轴上看相关性。

作者简介

本文作者的个人简介页面。

Go Komura

小村软件有限公司 代表

以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。

返回博客列表