Windows 内存底层原理(第 3 篇)——节对象与写时复制:DLL 与文件映射的真相

· 更新日期: · · Windows, 内存管理, 共享内存, 文件映射, 写时复制, DLL, Cache Manager

更新记录(仅首版,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. 先说结论

整体图景可以归纳为以下三点。

  1. 把共享的内容与各进程看到的地址分开。 节对象表示一段可共享的内存范围,各进程把其中一部分作为视图映射进来。即使使用同一个节偏移,视图的虚拟地址也不必一致。12
  2. 把内容由什么来支撑、通过哪条路径使用分开。 节分为由实际文件支撑的和由页面文件支撑的两种。EXE/DLL 是映像节,普通的文件映射是数据节。SEC_IMAGE 的页保护由 PE 内部的属性决定。缓存、数据、映像这三条路径也不能揉成一条。234
  3. 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 映射同一个节的同一个偏移,视图的起始地址也可能不同。

从不同虚拟地址映射到同一个物理页进程 A 与进程 B 各自在不同的虚拟地址上持有视图,但通过同一个节偏移到达同一个物理页 PFN X进程 A 的 0x000001A00000 + 0x3000节偏移 0x3000进程 B 的 0x000002700000 + 0x3000同一个物理页 PFN X

图1:被共享的是节内的内容和物理页,而不是虚拟地址。

不能把原始指针保存到共享内存里,原因就在这里。进程 A 的指针值,在进程 B 中可能是毫不相干的地址。

共享结构应使用相对视图起点的偏移、固定宽度整数,以及明确的版本和对齐方式。Microsoft 的 MapViewOfFileEx 文档也建议保存相对基址的偏移而不是指针,因为无法保证将来还能使用同一个地址。6

3. 文件后备与页面文件后备

按照内容从何处还原,节大致分为两类。

文件后备与页面文件后备的区分方式给 CreateFileMapping 传入实际文件就会得到文件后备节,clean 页可以从原文件重新读入。传入 INVALID_HANDLE_VALUE 则得到页面文件后备节,内容由页面文件支撑,对象销毁后就消失传入实际文件的句柄传入 INVALID_HANDLE_VALUECreateFileMapping文件后备节页面文件后备节clean 页可以从原文件重新读入内容由页面文件支撑,销毁后消失

图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 在概念上都到达同一个共享页,读取可以直接成功。

写时复制之前的共享状态在写入发生之前,进程 A 与进程 B 的 PTE 都到达同一个共享页 PFN X,读取可以直接成功Process A PTE共享 PFN X(read / copy-on-write)Process B PTE

图3:写入之前,两个进程的 PTE 指向同一个物理页。

5.2. 写入触发保护错误

CoW 页从一开始就不是普通的可共享写入页。当 Process A 试图写入时,CPU 会产生保护错误。接过控制权的内存管理器会判定,这不是非法写入,而是对带 CoW 属性的页的写入。

5.3. 创建新的物理页

做出判定之后,Windows 会执行以下操作。

  1. 取得一页供 Process A 使用的物理页。
  2. 把 PFN X 的内容复制到新页 PFN Y。
  3. 把 Process A 的 PTE 替换为指向 PFN Y。
  4. 把 Process A 一侧的保护改为普通的读写。
  5. 重新执行失败的写入指令。
写时复制之后的分叉状态因进程 A 的写入,只有进程 A 的 PTE 被替换为指向复制了内容的私有页 PFN Y,进程 B 的 PTE 继续指向原来的共享页 PFN X写入时复制内容Process A PTE私有 PFN Y(read/write,修改后)Process B PTE共享 PFN X(原有内容)

图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,可以使用以下步骤。

  1. 访问目标页,让它驻留。
  2. 用 QueryWorkingSetEx 取得该页的 Working Set 信息。
  3. 查看 Shared 位。
  4. 如果 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”。

连接到同一个文件流的三条路径cached ReadFile/WriteFile 使用 SharedCacheMap,数据映射的页错误使用 DataSectionObject,EXE/DLL 的映像页错误使用 ImageSectionObject,三者通过 SECTION_OBJECT_POINTERS 连接到同一个文件流cached ReadFile / WriteFileSharedCacheMap数据映射的页错误DataSectionObjectEXE/DLL 的映像页错误ImageSectionObject同一个文件流(SECTION_OBJECT_POINTERS)

图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 的共享。

  1. 以管理员身份启动 Process Explorer。
  2. 启动两个 cmd.exe。
  3. 选择 View > Lower Pane View > DLLs。
  4. 在两个进程中确认同一个 DLL 的路径与映射。
  5. 用 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 应用程序的共享内存、文件映射、DLL 加载、文件锁定、进程间通信以及内存使用量方面的故障调查。

参考链接

  1. Microsoft Learn, Section Objects and Views。关于节对象表示一段可共享的内存范围,以及各进程把节的一部分作为视图映射进来。 ↩ ↩2 ↩3

  2. Microsoft Learn, File-Backed and Page-File-Backed Sections。关于文件后备节与页面文件后备节、CoW,以及可以从不同进程的虚拟地址共享同一块物理内存。 ↩ ↩2 ↩3 ↩4

  3. Microsoft Learn, CreateFileMappingW function。关于文件映射对象、页面文件后备、SEC_IMAGE、视图与句柄的生存期,以及支撑同一文件的各视图之间的一致性。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8

  4. Microsoft Learn, SECTION_OBJECT_POINTERS structure。关于 DataSectionObject、SharedCacheMap、ImageSectionObject 把文件流的映射与缓存信息连接到内存管理器和 Cache Manager。 ↩ ↩2 ↩3 ↩4

  5. Microsoft Learn, Memory Protection。关于多个进程共享同一个 DLL 的物理页,以及一方写入时复制到新的物理页并更新 PTE 的 CoW。 ↩ ↩2 ↩3

  6. Microsoft Learn, MapViewOfFileEx function。关于 FILE_MAP_COPY 的 CoW、私有页由页面文件支撑、对整个视图收取 Commit charge,以及应当保存偏移而不是虚拟地址。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8

  7. Microsoft Learn, VirtualQuery function。关于 CoW 之后 Type 仍然是 MEM_MAPPED/MEM_IMAGE,以及可以通过 QueryWorkingSetEx 的 Shared 位确认私有化。 ↩ ↩2 ↩3 ↩4

  8. Microsoft Learn, Managing Memory Sections。关于在视图被访问之前不会分配物理内存,以及首次访问的页错误会读入文件内容。 ↩

  9. Microsoft Learn, Sharing Files and Memory。关于通过名称或句柄共享同一个文件映射对象、用 INVALID_HANDLE_VALUE 创建以页面文件为后备的共享内存,以及同步需要另行处理。 ↩ ↩2 ↩3

  10. Microsoft Learn, Process Explorer - Sysinternals。关于 Process Explorer 可以显示进程的句柄、已加载的 DLL 与内存映射文件。 ↩

  11. Microsoft Learn, VMMap - Sysinternals。关于 VMMap 可以把进程的虚拟内存分解为 Image、Mapped File、Private 等类别,并显示 Working Set 中 Private/Shareable 的构成。 ↩

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

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

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

常见问题

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

使用同一个 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 位。
可以把原始指针保存到共享内存里吗?
通常要避免。即使是同一个节,也无法保证各进程的视图被放到同一个虚拟地址。共享结构应使用相对基址的偏移、固定宽度整数,以及明确定义的布局和同步方式。

作者简介

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

Go Komura

小村软件有限公司 代表

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

返回博客列表