更新记录(仅首版,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 的区别。
- Reserve 是在虚拟空间中占住地址的阶段。 它预留这段范围,但不在 RAM 或页面文件中分配物理保存区域。1
- Commit 是系统承诺将来能够保存这些内容的阶段。 它收取提交费用并增加 Commit Total,但通常还不会把物理 RAM 绑定到全部页上。12
- 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 节 |
flowchart TB
accTitle: Reserve、Commit 与首次访问分别会发生什么
accDescr: MEM_RESERVE 把范围和属性记录到 VAD,MEM_COMMIT 消耗 Commit Total 并承诺保存,首次访问的页错误分配物理页并把它加入 Working Set
reserve["1. MEM_RESERVE"] --> commit["2. MEM_COMMIT"]
commit --> touch["3. 首次访问(Touch)"]
reserve -.-> vad["把范围与属性记录到 VAD"]
commit -.-> charge["消耗 Commit Total(尚无物理页)"]
touch --> fault["页错误"]
fault --> zero["把已置零的物理页绑定到 PTE"]
zero --> ws["加入 Working Set 并重新执行指令"]
图1:Reserve、Commit、Touch 是不同的事件。物理 RAM 被绑定上去,是在最后的首次访问时。
2. 追踪虚拟页的三张登记表
Windows 持有的这几张登记表,用不同的单位看同一块内存。首先要把范围、虚拟页、物理页这三者分开。
| 登记表 | 单位 | 作用 |
|---|---|---|
| VAD | 虚拟地址范围 | 管理该区域是什么、Reserve/Commit、保护属性以及与节的对应关系 |
| 页表 / PTE | 虚拟页 | 表示到当前物理页的转换,或者尚未实体化的状态 |
| PFN 数据库 | 物理页 | 跟踪每个 RAM 页的所有者、引用和状态 |
VAD 持有范围的信息,PTE 持有虚拟页的信息,PFN 数据库持有物理页的信息。页错误处理程序会把它们对照起来,判断这次访问能否继续下去。
flowchart TB
accTitle: 从虚拟地址到物理 RAM 的三张登记表
accDescr: 虚拟地址由 VAD 按范围管理、由 PTE 按虚拟页管理,而 PTE 转换到的物理页则由 PFN 数据库按物理页跟踪
va["虚拟地址"] --> vad["VAD(范围登记表)"]
va --> pte["PTE(虚拟页登记表)"]
vad -.->|判定 Reserve/Commit 与保护属性| pte
pte -->|有效的转换| pfn["PFN 数据库(物理页登记表)"]
pfn --> ram["物理 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)中。地址转换按下面的顺序进行。
- TLB 中有转换,且这次访问符合其保护属性时,直接使用该结果。
- TLB 中没有转换时,由 CPU 遍历页表。
- 存在有效的 PTE 且符合保护属性时,登记到 TLB 并继续执行。
- 没有有效的转换,或者存在保护违规时,走向页错误的入口。保护检查在转换来自 TLB 时同样会执行。
这里容易混淆的一点是,TLB 未命中和页错误是两回事。
| 情况 | 接下来会发生什么 |
|---|---|
| TLB 中有转换,且符合保护属性 | 用缓存的转换继续执行 |
| TLB 中没有转换,但存在有效的 PTE 且符合保护属性 | 通过页表遍历取得转换并继续执行 |
| 没有有效的转换,或者违反保护属性 | 走向页错误的入口 |
保护检查在 TLB 命中时同样起作用。向只读页写入、在不可执行的页上取指执行,即使转换已经缓存也会产生错误。向 CoW 页写入之所以能触发错误,也是同样的道理。
flowchart TB
accTitle: 地址转换的流程与页错误的入口
accDescr: 即使 TLB 中有转换,只要不符合保护属性就会走向页错误的入口。TLB 中没有转换时先遍历页表,PTE 有效且符合保护属性就登记到 TLB 并继续执行,无效或保护违规时走向页错误的入口
access["内存访问"] --> tlb{"TLB 中有转换吗?"}
tlb -->|有| perm{"符合保护属性吗?"}
perm -->|符合| go["用该转换继续执行"]
perm -->|保护违规| entry["走向页错误的入口"]
tlb -->|没有| walk["页表遍历"]
walk --> valid{"PTE 有效且符合保护属性?"}
valid -->|是| register["登记到 TLB 并继续执行(无错误)"]
valid -->|无效或保护违规| entry
图3:TLB 未命中可以通过页表遍历解决。走向页错误的是转换无效或保护违规的情况,而保护违规在 TLB 命中时也会发生。
5.1. 无效的 PTE 并不只是一格空白
虽说 PTE 无效,但并不意味着里面是空的。Windows 会根据无效 PTE 的软件状态,区分出下面这样的情况。
- 一次都没有实体化过的按需置零页
- 仍留在 RAM 中的 Transition 页
- 引用 Prototype PTE 的共享页
- 已保存到页面文件的私有页
- 保护违规或无效区域
CPU 的工作只到判断出“这不是通常的有效转换”并交给内核为止,再往后的含义解读由内存管理器负责。
6. 页错误的全过程
下面分六个阶段,追踪对已 Commit 的私有页的首次写入。
- CPU 试图写入。
它查找 TLB 和页表,但目标 PTE 中没有有效的 PFN。 - CPU 产生页错误。
它把出错的虚拟地址、读写执行的类别、用户态还是内核态、是缺少转换还是保护违规,一并交给内核。 - 内存管理器检查 VAD 和 PTE。
判断是否已 Commit、是否符合保护属性,以及属于按需置零、Transition、共享、页面调入、CoW、异常中的哪一种。 - 如果是按需置零,就取得一个已置零的物理页。
为了不泄露其他进程的数据,新交出去的页必须是全零的。 - 更新 PTE 与 PFN 管理信息。
把 PFN 和保护属性写入 PTE,把物理页置为 Active,并加入进程的 Working Set。 - 重新执行失败的指令。
由于已经正常解决,用户模式收不到异常,应用程序按普通赋值继续处理。
ETW 的页错误事件也把 Transition、Demand Zero、Copy-on-Write、Guard Page、Hard Page Fault、Access Violation 记录为不同的种类。4
也就是说,页错误这个词本身并不等于“异常”。它是 CPU 无法通过常规路径完成转换时,请求操作系统作出判断的公共入口。
flowchart LR
accTitle: 页错误解决方式的分支
accDescr: 内存管理器判定 VAD、PTE、保护属性和访问类型,再分流到按需置零、重新挂接 RAM 内的页、从后备存储读取的硬错误、写时复制、保护页通知或异常
faultIn["发生页错误"] --> judge["判定 VAD、PTE、保护属性与类别"]
judge -->|首次访问| dz["按需置零(软错误)"]
judge -->|仍留在 RAM 中| soft["从 Standby 等重新挂接(软错误)"]
judge -->|需要读磁盘| hard["硬错误(磁盘 I/O)"]
judge -->|CoW 写入| cow["复制后替换 PTE"]
judge -->|保护页| guard["解除保护并通知"]
judge -->|无法解决| av["异常(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/secMemory\\Pages Input/secMemory\\Page Reads/secMemory\\Available MBytesProcess(<目标进程>)\\Working Set - PrivateProcess(<目标进程>)\\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 或页面文件中的物理区域。1MEM_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 的“内存使用量”究竟表示什么——正确解读 Working Set、Private Bytes、Commit 与页面文件
- Windows I/O 底层原理(第 1 篇)——Windows 的 I/O 架构与 IRP
- Windows I/O 底层原理(第 4 篇)——缓存管理器与 WriteFile
- 用 WinDbg + SOS 分析故障转储
- Windows 应用程序故障转储收集入门
相关咨询领域
小村软件有限公司承接 Windows 应用程序的内存使用量调查、访问违规、启动延迟、分页,以及原生代码的缺陷分析。
参考链接
-
Microsoft Learn, VirtualAlloc function. 关于
MEM_RESERVE只预留虚拟地址范围而不分配物理存储、MEM_COMMIT会对系统全局内存和页面文件收取提交费用、已 Commit 的页初始内容为零、实际的物理页要到被访问时才分配。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 -
Microsoft Learn, PERFORMANCE_INFORMATION structure. 关于
CommitTotal是当前系统已 Commit 的页数,CommitLimit是不扩展页面文件即可 Commit 的上限。 ↩ ↩2 ↩3 -
Microsoft Learn, !vad (WinDbg). 关于
!vad显示 VAD 树,可以查看起始与结束 VPN、Commit、Mapped/Private、保护属性、Control Area 等信息。 ↩ -
Microsoft Learn, PageFault_TypeGroup1 class. 关于 ETW 会区分记录 Transition Fault、Demand Zero Fault、Copy-on-Write、Guard Page Fault、Hard Page Fault 和 Access Violation。 ↩
-
Microsoft Learn, Working Set. 关于软错误无需访问后备存储即可解决,会在其他进程的 Working Set、Transition、首次引用的按需置零等情形下发生。 ↩ ↩2 ↩3
-
Microsoft Learn, PageFault_HardFault class. 关于 HardFault 事件包含 FileObject、ReadOffset、ByteCount、VirtualAddress 和线程 ID,可用于追踪读取来源。 ↩ ↩2
-
Microsoft Learn, Access Violation C0000005. 关于
0xC0000005在读取、写入或执行无效内存地址时发生,异常参数会给出访问类别和违规地址。 ↩ ↩2 -
Microsoft Learn, Creating Guard Pages. 关于
PAGE_GUARD提供针对页访问的一次性通知,并产生STATUS_GUARD_PAGE_VIOLATION。 ↩ -
Microsoft Learn, VMMap - Sysinternals. 关于 VMMap 可以把已 Commit 的虚拟内存按类别拆解,并显示各类别的 Working Set 和详细的地址映射。 ↩
-
Microsoft Learn, Performance Analysis of Logs (PAL) Tool. 关于
Memory\\Pages Input/sec表示为解决硬页错误而从磁盘读入的页数。 ↩
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
Windows 内存底层原理(第 2 篇)——物理页的一生:五个列表与页面文件的真相
串联 PFN 数据库、Standby、Modified、内存压缩与页面文件,讲解物理页离开 Working Set 之后会去往哪里。
Windows 的“内存使用量”究竟表示什么——正确解读 Working Set、Private Bytes、Commit 与页面文件
任务管理器的内存、Working Set、Private Bytes 与 Commit 并不是同一个值。本文讲解 Windows 虚拟内存与物理内存的关系、页面文件的作用,以及排查内存不足和内存泄漏时应当关注的指标。
Windows 内存底层原理(第 3 篇)——节对象与写时复制:DLL 与文件映射的真相
串联节对象、映像映射与数据映射、共享缓存以及写时复制,讲解 DLL 和共享内存共享物理页的机制。
为什么“剩余1秒”迟迟不结束?── 进度条与剩余时间的工作原理
剩余1秒持续很久、卡在99%、一直显示准备中,分别是怎么回事?从进度的分母、速度预测、最后的处理步骤和界面更新逐一解释,并提供同一任务不同进度显示的交互演示。
Windows 共享文件夹为什么时好时坏——排查 Kerberos、NTLM 与凭据问题
通过症状和日志排查 Windows 共享文件夹时而能访问、时而无法访问的问题。说明 IP 与名称的差异、仅应用失败、空密码、1219、重启及 SMB 签名的检查步骤,以及每项结果能证明什么。
相关主题
与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。
Windows 技术主题
汇整 KomuraSoft LLC 关于 Windows 开发、故障调查与既有资产活用文章的主题中心。
常见问题
汇总了咨询这一主题时常见的问题。
- 用 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 和磁盘等待时间放在同一条时间轴上看相关性。