更新记录(仅首版,2026年08月20日 发布)
- 首次发布
引用本文(DOI(已登记存档): 10.5281/zenodo.22176198)
以下 DOI 指向先前登记的存档,内容可能与当前正文不同。引用当前正文时,请使用本页网址。
Go Komura(2026)。《Windows 内存底层原理(第 3 篇)——节对象与写时复制:DLL 与文件映射的真相》。小村软件有限公司。 https://comcomponent.com/zh-CN/blog/windows-memory-internals-section-copy-on-write/
- DOI(已登记存档)
- 10.5281/zenodo.22176198
- DOI(上次登记版本)
- 10.5281/zenodo.22176199
“如果 100 个进程都使用同一个 kernel32.dll,是不是连代码页也需要 100 组?”第 3 篇的起点,正是多个进程使用相同内容时产生的疑问。两个进程把同一个文件做内存映射时,一方读过的页,另一方能不能拿来用?
答案在于这样一个机制:从不同的虚拟地址映射到同一个物理页。共享单位的核心是节对象。而在写入时只把必要的页变成私有的,就是写时复制(Copy-on-Write,CoW)。
上一篇“Windows 内存底层原理(第 2 篇)——物理页的一生”追踪了离开 Working Set 的页如何在 Modified、Standby、Free、Zeroed 之间移动。Standby 上也会留下 DLL、EXE、映射文件和文件缓存的页。本回在此基础上,再加入来自多个进程的共享这一视角。
本文依次考察 EXE 与 DLL、数据文件、以页面文件为后备的共享内存和文件缓存,然后区分同一文件流上的缓存、数据、映像三条路径。数字本身怎么读,以入门篇“Windows 的“内存使用量”究竟表示什么”为前提。
“Windows 内存底层原理”全 3 篇
本系列按照获得物理页 → 追踪驻留与回收的流程 → 理解共享与私有化的顺序展开。
| 回次 | 主题 | 本回追踪的内容 |
|---|---|---|
| 第 1 篇 | 虚拟地址与页错误 | 用 VirtualAlloc 分配的区域,何时获得物理 RAM |
| 第 2 篇 | 物理页的一生 | 离开 Working Set 的页的状态转移,以及页面文件的作用 |
| 第 3 篇(本文) | 节对象与写时复制 | DLL、文件映射、共享内存如何共享物理页 |
第 3 篇要回答的疑问只有一个。
为什么多个进程能把同一个 DLL 或文件当作一组物理页来使用。
| 阅读之前 | 内容 |
|---|---|
| 目标读者 | 希望从内部结构理解 DLL 共享、CreateFileMapping、MapViewOfFile、共享内存、CoW 与文件缓存之间关系的开发者和运维人员 |
| 前提环境 | Windows 10/11 或现行的 Windows Server |
| 前提知识 | 虚拟地址、页错误、Working Set 的基础 |
| 难度 | 中级。也会涉及 Control Area、Prototype PTE 这类内部术语 |
观测使用 VMMap、Process Explorer 和 QueryWorkingSetEx。
图中实线表示始终成立的关系,虚线表示带条件的关系(成立条件写在详情页各关系的说明中)。关系的完整列表(共 19 条,附依据与确信度)以及主要概念的定义,汇总在知识地图详情页(日文)。数据:JSON-LD / Turtle
1. 先说结论
整体图景可以归纳为以下三点。
- 把共享的内容与各进程看到的地址分开。 节对象表示一段可共享的内存范围,各进程把其中一部分作为视图映射进来。即使使用同一个节偏移,视图的虚拟地址也不必一致。12
- 把内容由什么来支撑、通过哪条路径使用分开。 节分为由实际文件支撑的和由页面文件支撑的两种。EXE/DLL 是映像节,普通的文件映射是数据节。
SEC_IMAGE的页保护由 PE 内部的属性决定。缓存、数据、映像这三条路径也不能揉成一条。234 - CoW 的私有化要按页查看共享状态来确认。 读取期间共享,只复制被写入的那一页,并替换该进程的 PTE。
FILE_MAP_COPY会先对整个视图收取 Commit,因此仅凭 Private Bytes 无法断定 CoW 是否发生。确认时请使用QueryWorkingSetEx的 Shared 位。567
用一句话概括:被共享的不是虚拟地址,而是节内的内容,以及此刻与之对应的物理页。
| 想了解的问题 | 阅读的章节 |
|---|---|
| 节、视图与后备存储的关系 | 第 2~3 节 |
| DLL 的共享,以及写入时的 CoW | 第 4~6 节 |
| 与 Cache Manager 的区别,以及关闭后仍然残留的原因 | 第 7~8 节 |
| 想用两个进程确认共享与私有化 | 第 9~10 节 |
2. 节对象与视图
按 Microsoft 的定义,节对象表示一段可共享的内存区域,同时也是把文件映射到进程地址空间的机制。1
理解的关键,是把节本身和各进程看到的视图分开考虑。
| 概念 | 作用 |
|---|---|
| 节对象 | 表示要共享的内容、大小、后备存储以及保护的上限 |
| 视图 | 把节的一部分呈现为某个进程的虚拟地址范围 |
| PTE | 把视图内的每个虚拟页,绑定到当前的物理页或尚未落实的状态 |
| PFN | 表示实际存在于 RAM 中的物理页 |
Win32 API 也按照这种职责差异来分别理解。3
| 阶段 | 得到的结果 |
|---|---|
CreateFileMapping |
文件映射对象的句柄 |
MapViewOfFile |
放置在进程虚拟空间中的视图 |
| 对视图的首次访问 | 通过页错误,把被访问的页映射到物理内存 |
先看一个映射只读文件的例子。
HANDLE mapping = CreateFileMappingW(
file,
nullptr,
PAGE_READONLY,
0,
0,
nullptr);
void* view = MapViewOfFile(
mapping,
FILE_MAP_READ,
0,
0,
0);
只调用 CreateFileMapping,还得不到进程可以读取的地址。而且即使创建了视图,也不是所有页都会立刻进入 RAM。从最先被碰到的页开始,通过页错误读入文件内容,并把物理页绑定到 PTE。8 也就是说,第 1 篇看到的按需分页同样适用于节视图。
2.1. 同一个节,不同的虚拟地址
即使进程 A 和进程 B 映射同一个节的同一个偏移,视图的起始地址也可能不同。
flowchart LR
accTitle: 从不同虚拟地址映射到同一个物理页
accDescr: 进程 A 与进程 B 各自在不同的虚拟地址上持有视图,但通过同一个节偏移到达同一个物理页 PFN X
viewA["进程 A 的 0x000001A00000 + 0x3000"] --> offset["节偏移 0x3000"]
viewB["进程 B 的 0x000002700000 + 0x3000"] --> offset
offset --> pfnX["同一个物理页 PFN X"]
图1:被共享的是节内的内容和物理页,而不是虚拟地址。
不能把原始指针保存到共享内存里,原因就在这里。进程 A 的指针值,在进程 B 中可能是毫不相干的地址。
共享结构应使用相对视图起点的偏移、固定宽度整数,以及明确的版本和对齐方式。Microsoft 的 MapViewOfFileEx 文档也建议保存相对基址的偏移而不是指针,因为无法保证将来还能使用同一个地址。6
3. 文件后备与页面文件后备
按照内容从何处还原,节大致分为两类。
flowchart TB
accTitle: 文件后备与页面文件后备的区分方式
accDescr: 给 CreateFileMapping 传入实际文件就会得到文件后备节,clean 页可以从原文件重新读入。传入 INVALID_HANDLE_VALUE 则得到页面文件后备节,内容由页面文件支撑,对象销毁后就消失
create["CreateFileMapping"] -->|传入实际文件的句柄| fileBacked["文件后备节"]
create -->|传入 INVALID_HANDLE_VALUE| pfBacked["页面文件后备节"]
fileBacked --> restore1["clean 页可以从原文件重新读入"]
pfBacked --> restore2["内容由页面文件支撑,销毁后消失"]
图2:后备存储的差异,决定了内容能从哪里还原,以及能存活多久。
3.1. 文件后备节
把实际文件传给 CreateFileMapping,就会得到文件后备节。
- 只读视图会从文件中读取需要的页。
- 读写视图的更改会被当作该文件的数据。
- CoW 视图的更改不会写入原文件,而是变成私有页。
只要文件后备页是 clean 的,即使丢弃物理页,也能从原文件重新读入。这个性质支撑了第 2 篇看到的 Standby 与文件缓存的效率。
3.2. 页面文件后备节
给 CreateFileMapping 的 hFile 传入 INVALID_HANDLE_VALUE 并指定大小,就会得到页面文件后备节。
HANDLE mapping = CreateFileMappingW(
INVALID_HANDLE_VALUE,
nullptr,
PAGE_READWRITE,
0,
64 * 1024,
L"Local\\KomuraMemoryDemo");
这种节没有明确的数据文件,内容由页面文件支撑。初始内容为零。要让多个进程使用同一个对象,可以借助名称、句柄继承、DuplicateHandle 等方式。93
对共享页的更改是可见的,但并不是持久保存。 映射同一共享页的进程可以看到这些更改。另一方面,节对象一旦销毁,内容也不会留下,因此不适合用来代替持久化文件。2
有一点需要注意,共享内存并不会自动带上互斥控制。互斥体、信号量、事件、无锁协议等要另行设计。9
4. 映像映射与数据映射
说“EXE 和 DLL 也是文件映射”时,仍然要保留它与普通数据文件之间的差异。
| 项目 | 映像映射 | 数据映射 |
|---|---|---|
| 主要用途 | 加载 EXE、DLL | 普通文件、共享数据 |
| 创建属性 | SEC_IMAGE |
PAGE_READONLY、PAGE_READWRITE 等 |
| 页保护 | 由 PE 映像内的属性决定 | 由映射与视图的指定决定 |
| 写入 | 可能通过 writable section 或 CoW 变成私有 | 可以选择 shared write 或 CoW |
VirtualQuery 的 Type |
MEM_IMAGE |
MEM_MAPPED |
使用 SEC_IMAGE 时,决定视图页保护的不是传给 CreateFileMapping 的普通保护值,而是可执行映像自身的节属性。3
4.1. 并不是整个 DLL 都一定被共享
像代码这种不会被修改的页,可以在众多进程之间共享同一个物理页。而需要做进程专有修改的页,则会通过 CoW 分叉。
不过,ASLR 带来的重定位、加载器的修正、热补丁,以及实际的 PE 节属性都会产生影响。即使加载的是同一个 DLL,也并不是所有页都一定被共享。
重要的是这样一种设计:先共享可以共享的页,只对需要修改的页做延迟复制。
5. 写时复制的全过程
从两个进程都在读同一个 CoW 页的状态出发,追踪到只有 Process A 写入 1 个字节为止。
5.1. 写入之前
在写入发生之前,两个进程的 PTE 在概念上都到达同一个共享页,读取可以直接成功。
flowchart LR
accTitle: 写时复制之前的共享状态
accDescr: 在写入发生之前,进程 A 与进程 B 的 PTE 都到达同一个共享页 PFN X,读取可以直接成功
pteA["Process A PTE"] --> pfnX["共享 PFN X(read / copy-on-write)"]
pteB["Process B PTE"] --> pfnX
图3:写入之前,两个进程的 PTE 指向同一个物理页。
5.2. 写入触发保护错误
CoW 页从一开始就不是普通的可共享写入页。当 Process A 试图写入时,CPU 会产生保护错误。接过控制权的内存管理器会判定,这不是非法写入,而是对带 CoW 属性的页的写入。
5.3. 创建新的物理页
做出判定之后,Windows 会执行以下操作。
- 取得一页供 Process A 使用的物理页。
- 把 PFN X 的内容复制到新页 PFN Y。
- 把 Process A 的 PTE 替换为指向 PFN Y。
- 把 Process A 一侧的保护改为普通的读写。
- 重新执行失败的写入指令。
flowchart LR
accTitle: 写时复制之后的分叉状态
accDescr: 因进程 A 的写入,只有进程 A 的 PTE 被替换为指向复制了内容的私有页 PFN Y,进程 B 的 PTE 继续指向原来的共享页 PFN X
pteA2["Process A PTE"] --> pfnY["私有 PFN Y(read/write,修改后)"]
pteB2["Process B PTE"] --> pfnX2["共享 PFN X(原有内容)"]
pfnX2 -.->|写入时复制内容| pfnY
图4:只有执行写入的进程的 PTE 被换到新的私有页,另一个进程继续读取原有内容。
Process B 继续读取原有内容,看不到 Process A 的修改。这就是 Copy-on-Write。DLL 的共享和 FILE_MAP_COPY 用的都是不写就不复制这一相同原理。56
5.4. 与 FILE_MAP_WRITE 的区别
选择的标准是:是想让对方也看到这次写入,还是想让修改只属于自己。6
| 视图 | 写入之后会发生的事 |
|---|---|
FILE_MAP_WRITE |
作为共享写入,使用同一文件映射的其他视图也能看到更改 |
FILE_MAP_COPY |
只有被写入的页变成该进程专有。更改不会写回原文件,解除视图映射后就会丢失 |
是“想通过共享内存传递更新”,还是“想让各进程从共同的初始数据出发做私有修改”。目的不同,选择正好相反。
6. CoW 之后仍然是 MEM_MAPPED / MEM_IMAGE
这里要把区域的来源与当前物理页的共享状态分开。
CoW 之后的页在物理上是 Private 的,但 VirtualQuery 的 Type 不会变成 MEM_PRIVATE。数据视图仍然是 MEM_MAPPED,可执行映像仍然是 MEM_IMAGE。因为 Type 表示的是该区域来自哪一种初始分配。7
要按页查看是否已经发生 CoW,可以使用以下步骤。
- 访问目标页,让它驻留。
- 用
QueryWorkingSetEx取得该页的 Working Set 信息。 - 查看
Shared位。 - 如果
Shared == 0,那么这个驻留页就是 Private 的。
用 VMMap 确认时,也不要只看区域的 Type,还要看 Working Set 中 Private/Shareable 的构成。
6.1. Private Bytes 也可能不增加
FILE_MAP_COPY 只把被写入的页变成私有。不过,收取 Commit 的时机是另一回事。
为了应对将来可能写入视图内所有页的情况,Windows 在映射时就会收取相当于整个视图的 Commit charge。6 因此,写入第一页的瞬间,Private Bytes 未必会增加 4KiB。
观测 CoW 时优先看的指标如下。
QueryWorkingSetEx的 Shared 位- VMMap 的 Private WS / Shareable WS
- RAMMap 的物理页信息
- Private Bytes 只是辅助信息
如果只用“写入的瞬间 Private Bytes 有没有增加”来判定成败,就会漏掉正常工作的 CoW。
7. 与 Cache Manager 的接点——区分三条路径
说“加载 EXE 和 DLL、文件缓存、共享内存全都是节”,确实能把握整体图景,但不能把实现压成一个对象。
文件流上有一个供内存管理器和 Cache Manager 使用的 SECTION_OBJECT_POINTERS。
typedef struct _SECTION_OBJECT_POINTERS {
PVOID DataSectionObject;
PVOID SharedCacheMap;
PVOID ImageSectionObject;
} SECTION_OBJECT_POINTERS;
这个结构把文件对象与文件流的节联系起来,用于追踪内存中的内容和缓存信息。4 不过,各字段所指的状态和处理路径是彼此独立的。
| 路径 | 对应的字段 | 处理上的职责 |
|---|---|---|
cached ReadFile / WriteFile |
SharedCacheMap |
Cache Manager 使用缓存视图 |
| 数据文件的映射页错误 | DataSectionObject |
内存管理器在数据节一侧处理,并与同一文件流上的 cached I/O 协作,保持内容一致 |
| EXE/DLL 的映像页错误 | ImageSectionObject |
内存管理器用映像节和分页 I/O 处理。这条路径不经过 SharedCacheMap |
与 I/O 系列的接点在于,这三者被联系在同一个文件流上。不能把它读成“每条路径都会经过 Cache Manager”。
flowchart TB
accTitle: 连接到同一个文件流的三条路径
accDescr: cached ReadFile/WriteFile 使用 SharedCacheMap,数据映射的页错误使用 DataSectionObject,EXE/DLL 的映像页错误使用 ImageSectionObject,三者通过 SECTION_OBJECT_POINTERS 连接到同一个文件流
cached["cached ReadFile / WriteFile"] --> scm["SharedCacheMap"]
dataFault["数据映射的页错误"] --> dso["DataSectionObject"]
imageFault["EXE/DLL 的映像页错误"] --> iso["ImageSectionObject"]
scm --> stream["同一个文件流(SECTION_OBJECT_POINTERS)"]
dso --> stream
iso --> stream
图5:三条路径各自独立处理,但都连接在同一个文件流上。
三者的共同点不是“全都进入 Cache Manager”,而是同一个文件流通过 SECTION_OBJECT_POINTERS,把缓存、数据节、映像节这些彼此独立的状态联系起来。4 缓存读写、Lazy Writer 以及 Cc/Mm 的关系,在“Windows I/O 底层原理(第 4 篇)——缓存管理器与 WriteFile”中讨论。
另外,如果把内存映射视图与 ReadFile/WriteFile 混用,并不保证任何时刻看到的都是同一瞬间的内容。需要把同步、flush 和文件共享模式一并纳入设计。36
8. 对象与视图的生存期
关闭句柄和解除视图映射是两回事。 仅仅关闭 CreateFileMapping 得到的句柄,已有的视图不会消失。
视图持有对节的内部引用。只有把所有视图都 UnmapViewOfFile、把所有句柄都 CloseHandle 之后,对象才进入可以销毁的状态。3
UnmapViewOfFile(view);
CloseHandle(mapping);
CloseHandle(file);
这种生存期上的分离,正是“文件明明已经关闭却仍在使用中”这一现象的原因。即使关闭了文件句柄,只要映像节或数据视图还在引用文件流,文件最终的 close 就会推迟发生。
与 I/O 一侧 cleanup/close 的关系请参见“Windows I/O 底层原理(第 1 篇)”,实现上的坑请参见“共享内存的陷阱与实务最佳实践”。
9. 亲眼确认
9.1. 用两个进程查看同一个 DLL
先用已有的进程确认 DLL 的共享。
- 以管理员身份启动 Process Explorer。
- 启动两个
cmd.exe。 - 选择 View > Lower Pane View > DLLs。
- 在两个进程中确认同一个 DLL 的路径与映射。
- 用 VMMap 打开每个
cmd.exe,比较 Images 的 Working Set、Private、Shareable。
只看到同一个 DLL,还不能证明 PFN 一致
在 Process Explorer 中看到同一个 DLL,说明两者映射的是同一份映像。但仅凭这一点,还不能证明每一页的 PFN 都相同。请结合 VMMap 的 Shareable 构成、RAMMap 和 QueryWorkingSetEx,按页确认共享情况。Process Explorer 和 VMMap 由 Sysinternals 提供。1011
9.2. 用两个进程观测 FILE_MAP_COPY
下面的程序把同一个文件映射成 CoW 视图,并显示 QueryWorkingSetEx 的 Shared 位。文件映射对象用 PAGE_READONLY 创建,但这个保护与 FILE_MAP_COPY 的视图兼容,视图一侧的首次写入会触发 CoW。3
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <psapi.h>
#include <cstdio>
#include <cwchar>
#pragma comment(lib, "Psapi.lib")
void PrintPage(const char* stage, void* address)
{
MEMORY_BASIC_INFORMATION mbi{};
if (!VirtualQuery(address, &mbi, sizeof(mbi))) {
std::printf("VirtualQuery failed: %lu\n", GetLastError());
return;
}
for (int attempt = 0; attempt < 3; ++attempt) {
// The page may have been trimmed while the user was waiting.
// Touch it immediately before querying the working-set attributes.
volatile unsigned char resident =
*static_cast<volatile unsigned char*>(address);
(void)resident;
PSAPI_WORKING_SET_EX_INFORMATION ws{};
ws.VirtualAddress = address;
if (!QueryWorkingSetEx(GetCurrentProcess(), &ws, sizeof(ws))) {
std::printf("QueryWorkingSetEx failed: %lu\n", GetLastError());
return;
}
if (!ws.VirtualAttributes.Valid) {
Sleep(0);
continue;
}
std::printf(
"%s: Type=0x%lx Valid=1 Shared=%llu ShareCount=%llu\n",
stage,
static_cast<unsigned long>(mbi.Type),
static_cast<unsigned long long>(ws.VirtualAttributes.Shared),
static_cast<unsigned long long>(ws.VirtualAttributes.ShareCount));
return;
}
std::printf(
"%s: page is not resident; Shared/ShareCount were not interpreted\n",
stage);
}
int wmain(int argc, wchar_t** argv)
{
if (argc != 3) {
std::fwprintf(stderr, L"usage: cow_demo <file> <read|write>\n");
return 2;
}
HANDLE file = CreateFileW(
argv[1], GENERIC_READ, FILE_SHARE_READ,
nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (file == INVALID_HANDLE_VALUE) return 3;
HANDLE mapping = CreateFileMappingW(
file, nullptr, PAGE_READONLY, 0, 0, nullptr);
if (!mapping) {
CloseHandle(file);
return 4;
}
auto* view = static_cast<unsigned char*>(
MapViewOfFile(mapping, FILE_MAP_COPY, 0, 0, 0));
if (!view) {
CloseHandle(mapping);
CloseHandle(file);
return 5;
}
volatile unsigned char value = view[0];
(void)value;
std::puts("Start the other process. When both are waiting, press Enter...");
(void)std::getchar();
PrintPage("before", view);
if (std::wcscmp(argv[2], L"write") == 0) {
std::puts("Press Enter to trigger copy-on-write...");
(void)std::getchar();
view[0] ^= 0x5a;
PrintPage("after write", view);
} else {
std::puts("After the writer changes its page, press Enter...");
(void)std::getchar();
PrintPage("reader after peer write", view);
}
std::puts("Press Enter to exit...");
(void)std::getchar();
UnmapViewOfFile(view);
CloseHandle(mapping);
CloseHandle(file);
}
构建,并用两个进程打开同一个文件
先进行构建和准备。
cl /std:c++20 /EHsc /W4 cow_demo.cpp
$path = "$env:TEMP\\cow-demo.bin"
[IO.File]::WriteAllBytes($path, [byte[]]::new(65536))
接着,在两个控制台中打开同一个文件。
.\\cow_demo.exe "$env:TEMP\\cow-demo.bin" read
.\\cow_demo.exe "$env:TEMP\\cow-demo.bin" write
区分按 Enter 的顺序和要看的值
| 顺序 | 操作 | 需要确认的内容 |
|---|---|---|
| 1 | 启动两个进程,先在 read 一侧、再在 write 一侧按 Enter | 两侧的 before 中 Shared 都为 1 |
| 2 | 在 write 一侧再按一次 Enter | 执行写入的进程,其页的 Shared 变为 0 |
| 3 | 之后在 read 一侧按 Enter | reader 一侧继续读取原来的页 |
VirtualQuery 的 Type 在写入之后仍然是 MEM_MAPPED。请把共享状态的变化和 Type 的值分开确认。
只解读 Valid 为 1 的结果
另外,PrintPage 会在查询前重新触碰目标页,如果 Valid == 0,就不解读 Shared 和 ShareCount,最多重试三次。若仍未驻留,则不输出结果,而是显示相应说明。由于 ShareCount 会随时机和内存压力而变化,请不要盯着固定值,而要看在 Valid == 1 时确认到的 Shared 位的变化。
10. 实务中要避免的五种误读
10.1. “共享内存会落在同一个虚拟地址”
被共享的是节和物理页。视图的虚拟地址可能因进程而异,所以要保存偏移而不是原始指针。
10.2. “只要是同一个 DLL,所有页就一定被共享”
clean 的代码页确实容易共享,但重定位、writable section、CoW 以及测量时刻的驻留状态,也会产生 Private 页。
10.3. “CoW 之后会变成 MEM_PRIVATE”
VirtualQuery 的 Type 仍然是 MEM_MAPPED 或 MEM_IMAGE。实际的共享状态请用 QueryWorkingSetEx 确认。7
10.4. “Private Bytes 不增加就说明没有发生 CoW”
FILE_MAP_COPY 会先对整个视图收取 Commit。请优先看 Private WS 和 Shared 位。6
10.5. “既然是共享页就不需要同步”
能看到同一个物理页,和能从多个 CPU 核心安全地更新,是两个不同的问题。要设计原子性、内存序、互斥、崩溃时的中间状态以及版本兼容性。
把引用和生存期分开追踪的思路,同样适用于 Excel COM 互操作中进程残留的问题。请一并参见“Excel COM 互操作中进程残留的原因”。
11. 小结
- 节对象表示一段可共享的内存范围,各进程把它作为视图映射到自己的虚拟空间。1
- 同一个节的同一个偏移,可以从不同的虚拟地址映射到同一个物理页。2
- 文件后备节支撑实际文件,页面文件后备节支撑命名共享内存等场景。9
- EXE/DLL 按映像节处理,普通文件按数据节处理,两者的保护和写回目标不同。3
- CoW 在读取期间共享物理页,在首次写入时只复制该页并替换 PTE。5
- CoW 之后
VirtualQuery仍然返回MEM_MAPPED/MEM_IMAGE,所以要用QueryWorkingSetEx的 Shared 位来确认。7 - 使用
FILE_MAP_COPY时会先收取整个视图的 Commit,因此不能仅凭 Private Bytes 判定 CoW。6 - Cache Manager 的 cached I/O、数据映射、映像映射分别使用
SharedCacheMap、DataSectionObject、ImageSectionObject,作为彼此独立的路径连接在同一个文件流上。4 - 只有把视图与句柄的生存期、同步、ACL、偏移设计都纳入进来,共享内存才算是安全的。
至此,“Windows 内存底层原理”全 3 篇就结束了。Reserve/Commit 虚拟地址,通过页错误获得物理页,从 Working Set 移到页链表,经由节进行共享,只对被写入的页用 CoW 分叉——Windows 的内存管理,就是由这一连串流程串联起来的。
相关文章
- Windows 内存底层原理(第 1 篇)——虚拟地址变成物理 RAM 的那一刻
- Windows 内存底层原理(第 2 篇)——物理页的一生
- Windows I/O 底层原理(第 4 篇)——缓存管理器与 WriteFile
- 共享内存的陷阱与实务最佳实践
- Excel COM 互操作中进程残留的原因
- Process Explorer / Handle / VMMap 实战
相关咨询领域
小村软件有限公司承接 Windows 应用程序的共享内存、文件映射、DLL 加载、文件锁定、进程间通信以及内存使用量方面的故障调查。
参考链接
-
Microsoft Learn, Section Objects and Views。关于节对象表示一段可共享的内存范围,以及各进程把节的一部分作为视图映射进来。 ↩ ↩2 ↩3
-
Microsoft Learn, File-Backed and Page-File-Backed Sections。关于文件后备节与页面文件后备节、CoW,以及可以从不同进程的虚拟地址共享同一块物理内存。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, CreateFileMappingW function。关于文件映射对象、页面文件后备、
SEC_IMAGE、视图与句柄的生存期,以及支撑同一文件的各视图之间的一致性。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 -
Microsoft Learn, SECTION_OBJECT_POINTERS structure。关于 DataSectionObject、SharedCacheMap、ImageSectionObject 把文件流的映射与缓存信息连接到内存管理器和 Cache Manager。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Memory Protection。关于多个进程共享同一个 DLL 的物理页,以及一方写入时复制到新的物理页并更新 PTE 的 CoW。 ↩ ↩2 ↩3
-
Microsoft Learn, MapViewOfFileEx function。关于
FILE_MAP_COPY的 CoW、私有页由页面文件支撑、对整个视图收取 Commit charge,以及应当保存偏移而不是虚拟地址。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 -
Microsoft Learn, VirtualQuery function。关于 CoW 之后 Type 仍然是
MEM_MAPPED/MEM_IMAGE,以及可以通过QueryWorkingSetEx的 Shared 位确认私有化。 ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, Managing Memory Sections。关于在视图被访问之前不会分配物理内存,以及首次访问的页错误会读入文件内容。 ↩
-
Microsoft Learn, Sharing Files and Memory。关于通过名称或句柄共享同一个文件映射对象、用
INVALID_HANDLE_VALUE创建以页面文件为后备的共享内存,以及同步需要另行处理。 ↩ ↩2 ↩3 -
Microsoft Learn, Process Explorer - Sysinternals。关于 Process Explorer 可以显示进程的句柄、已加载的 DLL 与内存映射文件。 ↩
-
Microsoft Learn, VMMap - Sysinternals。关于 VMMap 可以把进程的虚拟内存分解为 Image、Mapped File、Private 等类别,并显示 Working Set 中 Private/Shareable 的构成。 ↩
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
DllMain 与加载器锁——“DLL 初始化里什么都别做”的真正原因
为什么不能在 DllMain 里调用 LoadLibrary 或与线程同步。本文依据一手资料,从串行化全部 DLL 通知的加载器锁机制,讲到死锁成立的典型场景、延迟初始化等正确设计,以及挂起的调查步骤。
Windows 内存底层原理(第 2 篇)——物理页的一生:五个列表与页面文件的真相
串联 PFN 数据库、Standby、Modified、内存压缩与页面文件,讲解物理页离开 Working Set 之后会去往哪里。
Windows 内存底层原理(第 1 篇)——虚拟地址变成物理 RAM 的瞬间:页错误的全过程
串联 VirtualAlloc、VAD、页表、TLB、按需置零与硬错误,讲解虚拟地址被分配到物理 RAM 的那一瞬间。
Windows 的“内存使用量”究竟表示什么——正确解读 Working Set、Private Bytes、Commit 与页面文件
任务管理器的内存、Working Set、Private Bytes 与 Commit 并不是同一个值。本文讲解 Windows 虚拟内存与物理内存的关系、页面文件的作用,以及排查内存不足和内存泄漏时应当关注的指标。
Windows 的进程间通信该怎么选 ── 命名管道 / TCP / gRPC / 共享内存 / COM 判断表
整理 Windows 应用程序之间应该如何选择通信方式。用判断表整理命名管道、本地 TCP、gRPC、共享内存、文件对接、COM 各自的优势与陷阱,并从实务角度说明 UI+服务分离・32bit/64bit 桥接・权限边界等常见架构,以及命名管道的实现示例。
相关主题
与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。
Windows 技术主题
汇整 KomuraSoft LLC 关于 Windows 开发、故障调查与既有资产活用文章的主题中心。
常见问题
汇总了咨询这一主题时常见的问题。
- 使用同一个 DLL 的每个进程,都会把整个 DLL 复制到 RAM 里吗?
- 通常不会复制。同一映像中未被修改的页,会从各进程不同的虚拟地址映射到同一个物理页。只有需要写入的页,才会通过写时复制等机制变成该进程专有的物理页。
- CreateFileMapping 会在调用的那一刻就为进程分配内存吗?
- CreateFileMapping 创建的是文件映射对象,把它呈现到进程虚拟空间中的是 MapViewOfFile。而且视图的物理页通常是从第一次访问的那一页开始,通过页错误才真正落实的。
- FILE_MAP_WRITE 和 FILE_MAP_COPY 有什么区别?
- FILE_MAP_WRITE 的更改是落到共享文件数据一侧的写入。FILE_MAP_COPY 共享初始页,但只有被写入的页会变成该进程专有的副本,更改不会写回原文件,解除视图映射后就会丢失。
- 写时复制之后,VirtualQuery 会返回 MEM_PRIVATE 吗?
- 不会。数据视图仍然是 MEM_MAPPED,映像视图仍然是 MEM_IMAGE。要确认页是否真的变成私有,可靠的做法是先让该页驻留,再查看 QueryWorkingSetEx 的 Shared 位。
- 可以把原始指针保存到共享内存里吗?
- 通常要避免。即使是同一个节,也无法保证各进程的视图被放到同一个虚拟地址。共享结构应使用相对基址的偏移、固定宽度整数,以及明确定义的布局和同步方式。