向 VirtualAlloc 传入 MEM_COMMIT 时,Commit 会立刻增加。不过 Working Set 不一定会同步增加同样的量。那么,你以为已经分配的内存在哪里?
答案是 大多数页还没有对应的物理 RAM。Windows 会把物理页的指派推迟到应用程序真正碰到该页。第一次访问让 CPU 发出页错误时,内存管理器会检查 VAD、PTE、保护属性和后备存储,必要时一页一页把 RAM 绑上去。1
本文跟着「你碰到的第一个字节」走到物理 RAM。若想先整理 Working Set 与 Commit 等数字的含义,请看入门篇「Windows的「内存使用量」究竟表示什么 ── 正确解读 Working Set・Private Bytes・Commit・页面文件」。本系列不重定义那里用过的术语,而是从机制面追「为什么会出现那个数字」。
「Windows 内存的深层」全 3 回
- 第1回(本文):虚拟地址与页错误
跟着以VirtualAlloc分配的区域何时取得物理 RAM。 - 第2回:物理页的一生
跟着离开 Working Set 的页如何在 Modified、Standby、Free、Zeroed 之间移动。 - 第3回:节对象与写时复制
跟着 DLL、文件映射与共享内存为何能共享物理页。
第1回要回答的问题只有一个。
已提交的虚拟地址,在哪一瞬间变成物理 RAM?
预定读者是想从机制理解 Windows 应用内存用量、启动直後的页错误、0xC0000005,以及 VMMap 与 PerfMon 数字的开发者与运维人员。前提环境是 Windows 10/11 或现行 Windows Server,必要背景是指针与 VirtualAlloc 基础;不需要页表位布局或内核调试器经验。难度为中级。会用到内部结构名称,但不假设依赖特定 Windows 版本的未公开布局。
1. 先讲结论
普通私有内存的流程,可以用一句话说完。
Reserve 预留虚拟地址范围,Commit 从系统提交限额计入 commit charge 以保证将来有地方保存内容,第一次访问的页错误才指派物理页。
也就是说,MEM_COMMIT 不是「现在立刻分配 RAM」的命令。Microsoft 的 VirtualAlloc 文档也保证已提交页的初始内容为零,同时说明实际物理页要到访问该虚拟地址才会被指派。1
不过,说「Reserve/Commit 只写进 VAD」也不准确。实务上,Reserve 主要建立代表虚拟地址范围与属性的 VAD,Commit 则增加系统的 Commit Total 并记录该范围的提交状态。中间页表层与各个 PTE 会在需要时延迟建立,与物理 RAM 的最终绑定通常发生在第一次访问。
Commit 不是空头支票,而是 全系统保证将来能把内容保存在 RAM 或适当后备存储的承诺。把这项承诺一页一页兑现的入口,就是页错误。
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. 追踪虚拟页的三本账本
要理解从虚拟地址到物理 RAM 的路径,必须分清 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 还没有对物理页的有效转换。页错误就发生在这里。
接到控制权的内存管理器判断这是「对已提交、可写私有页的第一次访问」,取得已清零的物理页、绑到 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 状态
- 读、写、执行、写时复制等保护
- 与文件或节的对应
- 防护页等特殊属性
用范围管理是为了效率。256MiB 以 4KiB 计是 65,536 页。与其一开始就为每一页建完整管理结构,不如在 VAD 里记下「这段连续范围是一次预留」,需要时再实现各页,比较不浪费。
4.1. 找到 VAD 不保证能恢复
「在 VAD 里就能解决错误,不在就访问违规」当作入门说明很方便,但过度简化。即使找到 VAD,下列情况仍无法继续一般访问。
- 只有 Reserve,目标页尚未提交
PAGE_NOACCESS- 对只读页写入
- 在不可执行页上执行指令
- 第一次碰到防护页
- 碰到节有效范围之外
反过来说,即使 PTE 无效,只要 VAD 与 PTE 的软件状态显示这是正当访问,就能以 demand-zero、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 没有转换而 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 的软件状态区分下列情况。
- 从未实现的 demand-zero 页
- 仍留在 RAM 的 Transition 页
- 引用 Prototype PTE 的共享页
- 保存在页面文件的私有页
- 保护违规或无效区域
CPU 的工作只是判断「这不是普通的有效转换」并交给内核;之后的含义由内存管理器补上。
6. 页错误从头到尾
让我们用六个阶段跟着对已提交私有页的第一次写入。
- CPU 尝试写入。
它检查 TLB 与页表,但目标 PTE 没有有效 PFN。 - CPU 发出页错误。
把发生错误的虚拟地址、读写执行类型、用户/内核,以及问题是缺少转换还是保护违规,交给内核。 - 内存管理器检查 VAD 与 PTE。
判断页是否已提交、保护是否符合,以及属于 demand-zero、Transition、共享、换入、CoW 或异常的哪一种。 - 若是 demand-zero,取得已清零的物理页。
新交出的页必须是零,以免泄漏另一个进程的数据。 - 更新 PTE 与 PFN 管理信息。
在 PTE 设置 PFN 与保护,把物理页设为 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、保护属性与访问类型,分派到 demand-zero、重新接上仍在 RAM 的页、从后备存储来的硬错误、写时复制、防护页通知或异常
faultIn["发生页错误"] --> judge["判断 VAD、PTE、保护、类型"]
judge -->|第一次访问| dz["Demand-zero(软)"]
judge -->|仍在 RAM| soft["从 Standby 重接(软)"]
judge -->|需要读磁盘| hard["硬错误(磁盘 I/O)"]
judge -->|CoW 写入| cow["复制并更换 PTE"]
judge -->|防护页| guard["清掉防护并通知"]
judge -->|无法解决| av["异常(0xC0000005 等)"]
图 4: 从同一入口进来的错误,依判断分成六种结果。防护页细节见第 9 节。
7. Demand-zero ── 不读磁盘的软错误
Demand-zero 是第一次碰到已提交私有页时发生的代表性软错误。Microsoft 的 Working Set 文档也把「进程第一次引用已分配的虚拟页」列为软错误的例子。5
Demand-zero 有下列特征。
- 不必从磁盘读原始数据
- 初始内容为零
- 绑上可用的物理页
- Working Set 与累计 Page Fault Count 增加
- 单靠这次处理不会增加
Memory\\Pages Input/sec
因此启动直後 Page Faults/sec 飙高,本身并不代表存储是瓶颈。
延迟分配的取舍也值得整理。若 Commit 了 256MiB 却实际只用 8MiB,把剩下 248MiB 留在 RAM 外是合理的。代价是第一次访问要付错误处理成本。对延迟敏感的工作,有一种设计是开始前先碰每一页做预错误(prefault),但那是先增加 RAM 常驻的取舍。
8. 软错误与硬错误
8.1. 软错误
软错误是不必对后备存储做读取 I/O 就能解决的错误。代表性例子包括下列项目。
- Demand-zero
- 把仍在 Standby/Transition 的页重新接上
- 接上已在另一进程 Working Set 的共享页
- 接上已预取的页
- 原始页仍常驻的写时复制
内核转换、锁、PTE/PFN 更新、TLB 一致性等仍有 CPU 成本,但没有存储等待。5
8.2. 硬错误
另一方面,需要的页不在 RAM 任何地方、必须从后备存储读取时,就是硬错误。读取来源不只是页面文件。
- 被写出到页面文件的私有页
- 内存映射文件
- EXE 或 DLL 映像
- 文件缓存引用的数据文件
ETW HardFault 事件包含 FileObject、ReadOffset、ByteCount,因此可以追到实际读取来源。6
所以 Hard Fault = 读 pagefile.sys 并不成立。
需要读后备存储时,请求会进入 Windows I/O 栈。IRP 的发出与完成流程见「Windows I/O 的深层(第1回)」,与文件缓存的接点见「Windows I/O 的深层(第4回)」。页在 RAM 里时,内存管理器可以自己回来;不在时就发出 I/O,并让发生错误的线程等到完成。
9. 无法解决的错误会变成异常
检查 VAD 与 PTE 后,无法当成正当分配、换入或 CoW 解决的错误,会以异常交给用户模式。
代表性情况是 STATUS_ACCESS_VIOLATION,异常码 0xC0000005。它发生在对无效地址读、写或执行;第一个异常参数指出访问类型,第二个指出违规地址。7
典型模式包括下列项目。
- 读取 NULL、已释放地址或数组外的地址
- 对只读页写入
- 从 DEP/NX 设成不可执行的页执行指令
- 碰到尚未提交的预留范围
PAGE_GUARD 的含义稍有不同。它是对访问的一次性通知:引发 STATUS_GUARD_PAGE_VIOLATION,用于堆栈增长等情况。8
正常的延迟分配、换入、CoW、防护通知,以及最后的访问违规,从 CPU 看来都聚在同一个页错误入口。决定结果的是 VAD、PTE、保护属性与访问类型的组合。
10. 自己动手看
到目前为止的流程,可以在自己的机器上观察。下面的 C++ 程序 Reserve 256MiB、Commit,对每页写一个字节,最后 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(<target>)\\Page Faults/secMemory\\Pages Input/secMemory\\Page Reads/secMemory\\Available MBytesProcess(<target>)\\Working Set - PrivateProcess(<target>)\\Private Bytes
Process\\Page Faults/sec 同时包含软错误与硬错误。另一方面,Memory\\Pages Input/sec 是为了解决硬错误而从磁盘读入的页数。10
在这个程序的 Touch 阶段,Page Faults/sec 应该跳升,而 Pages Input/sec 不该升太多。新提交的页由 demand-zero 实现,不必从磁盘读原始数据。
多个进程共用同一名称时,PerfMon 的 process#1 这类编号可能在重启后改变。请用显示 PID 的计数器交叉核对,或用 Process V2 或 ETW/WPA 按 PID 识别。
11. 实务上要避开的三种误读
11.1. 「Commit 上去了,所以是 RAM 泄漏」
Commit 是承诺保存的内容量;还没碰到的页可能不常驻在 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 有效时,只走页表就能解决。
- Demand-zero、Transition 还原、接上共享页,是不必磁盘 I/O 就能解决的软错误。5
- 若必须从页面文件、DLL、EXE 或映射文件读取,就是硬错误。6
- 检查 VAD、PTE 与保护属性后仍无法解决,会得到
0xC0000005这类异常。7 - 判断性能时不要只看
Page Faults/sec;把Pages Input/sec、Available、Working Set、Private Bytes 与存储等待放在同一时间轴。
续篇是第2回,「物理页的一生:五份列表与页面文件的真相」。
Commit 承诺变成物理页之后,我们从 PFN 数据库与页列表跟着该页离开 Working Set 后去了哪里。
相关文章
- Windows的「内存使用量」究竟表示什么 ── 正确解读 Working Set・Private Bytes・Commit・页面文件
- Windows I/O 的深层(第1回) ── 所有读写都会变成 IRP:I/O 系统全貌
- Windows I/O 的深层(第4回) ── 缓存管理器:你的 WriteFile 何时才能到达磁盘
- 用 WinDbg + SOS 解读崩溃转储 ── 采集之后的实务分析入门
- Windows应用的crash dump收集入门 - 先搞清楚 WER / ProcDump / WinDbg怎么分工
相关咨询领域
小村软件有限公司承接 Windows 应用程序内存用量、访问违规、启动延迟、分页与原生代码缺陷的调查。
参考链接
-
Microsoft Learn, VirtualAlloc function. 关于
MEM_RESERVE预留虚拟地址范围而不指派物理存储;MEM_COMMIT对系统整体内存与页面文件计入 commit charge;已提交页的初始内容为零;以及实际物理页要到被访问才指派。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 -
Microsoft Learn, PERFORMANCE_INFORMATION structure. 关于
CommitTotal是当前系统 Commit 页数,以及CommitLimit是不扩展页面文件也能提交的上限。 ↩ ↩2 -
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、第一次引用的 demand-zero 等而发生。 ↩ ↩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 按类型拆解已提交虚拟内存,并显示各类型的 Working Set 与详细地址图。 ↩
-
Microsoft Learn, Performance Analysis of Logs (PAL) Tool. 关于
Memory\\Pages Input/sec是为了解决硬页错误而从磁盘读入的页数。 ↩
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
Windows 内存的深层(第2回) ── 物理页的一生:五份列表与页面文件的真相
把 PFN 数据库、Standby、Modified、内存压缩和页面文件串起来,说明物理页离开 Working Set 之后会去哪里。
Windows 内存的深层(第3回)── 节对象与写时复制:DLL 与文件映射的真面目
把节对象、映像与数据映射、共享缓存、写时复制串起来,说明 DLL 与共享内存如何共享物理页。
Windows的「内存使用量」究竟表示什么 ── 正确解读 Working Set・Private Bytes・Commit・页面文件
任务管理器中的内存、Working Set、Private Bytes、Commit并不是同一个值。本文讲解Windows虚拟内存与物理内存的关系、页面文件的作用,以及在排查内存不足或泄漏时应该关注的指标。
Windows 虚拟化的深层(第3回) ── 数秒启动的虚拟机:WSL2、Windows Sandbox 与容器为何这么轻
WSL2 和 Windows Sandbox 为何数秒启动、用起来这么轻?本文从动态基础映像、直接映射、动态内存分配讲到 Hyper-V 隔离容器,讲解其机制。
Windows 虚拟化的深层(第2回) ── 连内核也看不见的内存:VBS、HVCI 与 Credential Guard 的机制
在兼容硬件上全新安装时,VBS 默认启用,并用虚拟机监控程序与 SLAT 创建比内核更强的隔离。本文讲解 VTL、安全内核、HVCI 与 Credential Guard 的结构。
相关主题
与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。
Windows 技术主题
汇整 KomuraSoft LLC 关于 Windows 开发、故障调查与既有资产活用文章的主题中心。
常见问题
汇总了咨询这一主题时常见的问题。
- 向 VirtualAlloc 传入 MEM_COMMIT 的当下就会分配 RAM 吗?
- 在普通的私有内存里,Commit 会消耗系统的提交余量,但对应的物理页要到第一次访问才会被指派。以写入首次碰到的页,是在 demand-zero 错误处理中获得物理页。
- 页错误代表异常或有性能问题吗?
- 不是。像 demand-zero 或从 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、磁盘等待时间放在同一时间轴上对照。