上一篇「Windows 内存的深层(第2回)── 物理页的一生」追踪了离开 Working Set 的物理页如何在 Modified、Standby、Free、Zeroed 之间移动。那份 Standby 也会留下 DLL、EXE、映射文件与文件缓存的页。
这里会出现一个问题。当 100 个进程使用同一个 kernel32.dll 时,Windows 会在 RAM 里放 100 组代码页吗?两个进程把同一个文件映射到内存时,一方读过的页,另一方也能用吗?
答案是 把多个虚拟地址映射到同一物理页。代表这个共享单位的中心是节对象,而只在写入时才把共享拆开的机制是写时复制(Copy-on-Write,CoW)。
本文追踪 EXE 与 DLL、数据文件、以页面文件为后盾的共享内存,以及文件缓存如何接到同一文件流,处理路径又在哪里分开。数字本身的读法,以入门文「Windows的「内存使用量」究竟表示什么」为前提。
「Windows 内存的深层」全 3 回
- 第1回:虚拟地址与缺页
追踪已 Commit 的虚拟页获得物理 RAM 的那一刻。 - 第2回:物理页的一生
追踪离开 Working Set 的页的状态转移。 - 第3回(本文):节对象与写时复制
追踪 DLL、文件映射与共享内存共享物理页的机制。
第3回要回答的问题只有一个。
为什么多个进程可以把同一个 DLL 或文件当成一组物理页来用?
预期读者是希望从内部结构(而不只是 API 用法)理解 DLL 共享、CreateFileMapping、MapViewOfFile、共享内存、CoW 与文件缓存关系的开发者与运维人员。前提环境是 Windows 10/11 或现行 Windows Server,所需背景是虚拟地址、缺页与 Working Set 的基础。难度为中级;也会用到 Control Area、Prototype PTE 等内部用语,但观测可用 VMMap、Process Explorer 与 QueryWorkingSetEx 重现。
1. 先讲结论
先看整体图像。
- 节对象代表一段可共享的内存范围。
每个进程把该节的一部分,以「视图」映射到自己的虚拟空间。1 - 同一节的视图不必落在同一虚拟地址。
进程 A 的0x000001...与进程 B 的0x000002...可以指向同一节偏移与同一物理页。2 - 有文件后盾与页面文件后盾两种节。
前者使用真正的文件;后者用于没有明确文件的共享内存等情况。2 - 加载 EXE/DLL 是映像节;普通文件映射是数据节。
使用SEC_IMAGE时,PE 内部的节属性决定页保护。3 - 读取期间可以共享同一物理页。
一方写入 CoW 页时,只复制该页并抽换写入进程的 PTE。4 - 即使在同一文件流上,缓存、数据、映像路径也会分开。
Cache Manager 的缓存 I/O 使用SharedCacheMap,数据映射使用DataSectionObject,EXE/DLL 使用ImageSectionObject。三者通过SECTION_OBJECT_POINTERS接到同一文件流,但映像缺页不会经过 Cache Manager。5 - 单看 Private Bytes 无法确认发生了 CoW。
FILE_MAP_COPY会在映射时先对整个视图收取 Commit,以防之后每一页都被私有化。逐页确认请用QueryWorkingSetEx的 Shared 位。67
一句话:被共享的不是虚拟地址,而是节内的内容,以及当下对应到的物理页。
2. 节对象与视图
按 Microsoft 的定义,节对象代表一段可共享的内存区域,也是把文件映射到进程地址空间的机制。1
理解的关键是把节本身,与各进程看到的视图分开。
| 概念 | 角色 |
|---|---|
| 节对象 | 代表要共享的内容、大小、后盾存储与保护上限 |
| 视图 | 把节的一部分显示成某个进程的虚拟地址范围 |
| PTE | 把视图中的每个虚拟页绑到当前的物理页或尚未实现的状态 |
| PFN | 代表 RAM 中实际存在的物理页 |
在 Win32 中,CreateFileMapping 返回文件映射对象的句柄,MapViewOfFile 在进程虚拟空间创建视图。3
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 会得到文件后盾节,干净页可从原始文件重读。传入 INVALID_HANDLE_VALUE 则得到页面文件后盾节;页面文件撑住内容,对象销毁后内容消失
create["CreateFileMapping"] -->|传入真实文件句柄| fileBacked["文件后盾节"]
create -->|传入 INVALID_HANDLE_VALUE| pfBacked["页面文件后盾节"]
fileBacked --> restore1["干净页可从原始文件重读"]
pfBacked --> restore2["页面文件撑住内容,销毁后消失"]
图 2: 后盾存储的差异决定内容从哪里还原、能活多久。
3.1. 文件后盾节
把真正的文件传给 CreateFileMapping,就会得到文件后盾节。
- 只读视图从文件读出需要的页。
- 读写视图的更改被视为该文件的数据。
- CoW 视图的更改不会写入原始文件,而会变成私有页。
若文件后盾页是干净的,可以丢弃物理页再从原始文件重读。这个性质支撑了第2回见过的 Standby 与文件缓存效率。
3.2. 页面文件后盾节
把 INVALID_HANDLE_VALUE 传给 CreateFileMapping 的 hFile 并指定大小,就会得到页面文件后盾节。
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 映像内的属性决定 | 由映射与视图指定决定 |
| 写入 | 可经可写节或 CoW 私有化 | 可选择共享写入或 CoW |
VirtualQuery Type |
MEM_IMAGE |
MEM_MAPPED |
使用 SEC_IMAGE 时,决定视图页保护的,主要是可执行映像自身的节属性,而不是传给 CreateFileMapping 的普通保护值。3
靠这个机制,代码等未修改页可以跨许多进程共享同一物理页,只有需要进程私有更改的页才经 CoW 分岔。不过并非每个 DLL 页都一定被共享——还有 ASLR 重定位、加载器修正、热补丁、实际 PE 节属性等因素。
重要的设计是 先共享可共享的页,再延迟复制真正需要更改的页。
5. 写时复制从头到尾
让我们从两个进程正在读同一 CoW 页的状态,一路跟到进程 A 写入一个字节。
5.1. 写入之前
写入发生前,两个进程的 PTE 在概念上都到达同一共享页,读取可以直接成功。
flowchart LR
accTitle: 写时复制前的共享状态
accDescr: 写入发生前,进程 A 的 PTE 与进程 B 的 PTE 都到达同一共享页 PFN X,读取可以直接成功
pteA["进程 A PTE"] --> pfnX["共享 PFN X(读取 / 写时复制)"]
pteB["进程 B PTE"] --> pfnX
图 3: 写入前,两个进程的 PTE 都指向同一物理页。
5.2. 写入时的保护错误
CoW 页一开始就不是普通的共享可写页。进程 A 尝试写入时,CPU 会发出保护错误。接手的内存管理器判断这不是非法写入,而是对 CoW 属性的写入。
5.3. 创建新的物理页
依该判断,Windows 会做下列事情。
- 为进程 A 取得一页物理页。
- 把 PFN X 的内容复制到新页 PFN Y。
- 把进程 A 的 PTE 抽换成 PFN Y。
- 把进程 A 的保护改成普通读写。
- 重新执行失败的写入指令。
flowchart LR
accTitle: 写时复制后的分裂状态
accDescr: 进程 A 写入后,只有进程 A 的 PTE 被抽换到收到内容副本的私有页 PFN Y,进程 B 的 PTE 仍指向原来的共享页 PFN X
pteA2["进程 A PTE"] --> pfnY["私有 PFN Y(R/W,写入后)"]
pteB2["进程 B PTE"] --> pfnX2["共享 PFN X(原来)"]
pfnX2 -.->|写入时复制| pfnY
图 4: 只有写入进程的 PTE 被抽换到新的私有页;另一侧继续读原来的内容。
进程 B 继续读原来的内容,看不到进程 A 的更改。这就是写时复制。DLL 共享与 FILE_MAP_COPY 都用同一原则:写之前不要复制。46
5.4. 与 FILE_MAP_WRITE 的差异
经 FILE_MAP_WRITE 共享写入的页,设计上会让一方的更改也从使用同一文件映射的其他视图可见。相对地,FILE_MAP_COPY 只有被写入的页变成进程私有;更改不会写回原始文件,解除映射后就会消失。6
你要的是「经共享内存传递更新」,还是「各进程从共同初始数据做私有更改」?目的相反,正确选择也相反。
6. CoW 之后仍是 MEM_MAPPED / MEM_IMAGE
CoW 之后的页在物理上已是 Private。你可能会以为 VirtualQuery 的 Type 也会变成 MEM_PRIVATE,但实际上数据视图仍是 MEM_MAPPED,可执行映像仍是 MEM_IMAGE。VirtualQuery 回报的是该区域来自哪一种初始分配。7
要逐页确认 CoW 是否已发生,请用下列步骤。
- 访问目标页,让它驻留。
- 用
QueryWorkingSetEx取得该页的 Working Set 信息。 - 看
Shared位。 - 若
Shared == 0,该驻留页就是 Private。
用 VMMap 确认时,也不要只看区域的 Type,还要看 Working Set 的 Private/Shareable 分解。
6.1. Private Bytes 可能不会增加
使用 FILE_MAP_COPY 时,进程之后可能写入视图中的每一页。因此 Windows 在映射时就收取 相当于整个视图的 Commit 费用。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;
DataSectionObject:数据文件的节状态SharedCacheMap:Cache Manager 跟踪的缓存视图ImageSectionObject:可执行映像的节状态
Microsoft 文档说明此结构把文件对象绑到文件流的节,并跟踪内存中的内容与缓存信息。5
I/O 系列与内存系列在此相接。理解时仍要把三条路径分开。
- 缓存的
ReadFile/WriteFile使用 Cache Manager 的SharedCacheMap与缓存视图。 - 数据文件的映射缺页 由内存管理器在
DataSectionObject一侧处理。它与同一文件流上的缓存 I/O 合作,以保持内容一致。 - EXE/DLL 的映像缺页 由内存管理器用
ImageSectionObject与分页 I/O 处理。这不是经过 Cache Manager 的SharedCacheMap的路径。
flowchart TB
accTitle: 接到同一文件流的三条路径
accDescr: 缓存的 ReadFile/WriteFile 使用 SharedCacheMap,数据映射缺页使用 DataSectionObject,EXE/DLL 映像缺页使用 ImageSectionObject;三者经 SECTION_OBJECT_POINTERS 接到同一文件流
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 绑住缓存、数据节、映像节这些分开的状态。5 缓存读写、Lazy Writer,以及 Cc 与 Mm 的关系,见「Windows I/O 的深层(第4回) ── 缓存管理器:你的 WriteFile 何时才能到达磁盘」。
请注意,把内存映射视图与 ReadFile/WriteFile 混用时,不保证永远看到同一瞬间的内容。设计需要包含同步、刷新与文件共享模式。36
8. 对象与视图的生命周期
只关闭 CreateFileMapping 句柄并不会销毁既有视图。视图对节保有内部引用,必须所有视图都 UnmapViewOfFile、所有句柄都 CloseHandle 之后,对象才能被销毁。3
UnmapViewOfFile(view);
CloseHandle(mapping);
CloseHandle(file);
这种生命周期分离,正是「文件已关闭,却仍在使用中」现象的原因。即使关闭了文件句柄,只要映像节或数据视图仍引用该文件流,文件的最终关闭就会更晚。
与 I/O 端清理/关闭的关系见「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。
在 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,再在写入端按 Enter,确认两边的 before 都设置了 Shared。再在写入端按一次 Enter,该进程的页会变成 Shared 0。然后在读取端按 Enter,即可确认读取端仍在读原来的页。写入后 VirtualQuery 的 Type 仍是 MEM_MAPPED。
请注意 PrintPage 会在查询直前再次触碰目标页;若 Valid == 0,它不解释 Shared 与 ShareCount,最多重试三次。若页仍非驻留,就不产出结果并报告此事。ShareCount 会随时机与内存压力变化,因此请看 Valid == 1 时确认到的 Shared 位变化,而不是固定数值。
10. 实务上要避开的五种误读
10.1. 「共享内存会落在同一虚拟地址」
被共享的是节与物理页。视图的虚拟地址可依进程而异,因此请存偏移而非原始指针。
10.2. 「同一 DLL 就代表每一页都一定共享」
干净的代码页容易共享,但重定位、可写节、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 互操作后进程残留的问题。另见「C# 操作 Excel 时 EXCEL.EXE 残留的问题 ── COM 引用释放模式与替换判断」。
11. 总结
- 节对象代表可共享的内存范围,各进程以视图映射到自己的虚拟空间。1
- 同一节的同一偏移,会从不同虚拟地址映射到同一物理页。2
- 文件后盾节支撑真正的文件;页面文件后盾节支撑具名共享内存等用途。9
- EXE/DLL 被当成映像节,普通文件被当成数据节;保护与回写目的地不同。3
- CoW 在读取期间共享物理页,第一次写入时只复制该页并抽换 PTE。4
- CoW 之后
VirtualQuery仍返回MEM_MAPPED/MEM_IMAGE,因此用QueryWorkingSetEx的 Shared 位确认。7 FILE_MAP_COPY会先收取整个视图的 Commit,因此不能单靠 Private Bytes 判断 CoW。6- Cache Manager 的缓存 I/O、数据映射、映像映射分别使用
SharedCacheMap、DataSectionObject、ImageSectionObject,以分开的路径接到同一文件流。5 - 共享内存必须把视图与句柄的生命周期、同步、ACL 与偏移设计都纳入,才算安全。
「Windows 内存的深层」三回到此结束。你 Reserve/Commit 虚拟地址,经缺页取得物理页,把页从 Working Set 移到页列表,经节共享,再只对写过的页做 CoW 拆分——Windows 内存管理是连成这一条流的。
相关文章
- Windows 内存的深层(第1回)── 虚拟地址变成物理 RAM 的那一刻:缺页从头到尾
- Windows 内存的深层(第2回)── 物理页的一生:五份列表与页面文件的真相
- Windows I/O 的深层(第4回) ── 缓存管理器:你的 WriteFile 何时才能到达磁盘
- 共享内存的陷阱与实务最佳实践
- C# 操作 Excel 时 EXCEL.EXE 残留的问题 ── 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, Memory Protection. 关于多个进程共享同一 DLL 的物理页,以及一方写入时 CoW 复制到新物理页并更新 PTE。 ↩ ↩2 ↩3
-
Microsoft Learn, SECTION_OBJECT_POINTERS structure. 关于 DataSectionObject、SharedCacheMap、ImageSectionObject 如何把文件流的映射与缓存信息绑到内存管理器 / Cache Manager。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, MapViewOfFileEx function. 关于
FILE_MAP_COPY的 CoW、私有页由页面文件撑住、对整个视图收取 Commit,以及应存放偏移而非虚拟地址。 ↩ ↩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、demand-zero 与硬错误串起来,说明虚拟地址被赋予物理 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 的进程,都会在 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 位。
- 可以在共享内存里存放原始指针吗?
- 通常不应该。即使是同一节,也不保证各进程的视图会放在同一虚拟地址。共享结构请用相对于基址的偏移、固定宽度整数,以及明确的布局与同步方式。