Windows 内存的深层(第3回)── 节对象与写时复制:DLL 与文件映射的真面目

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

上一篇「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. 第1回:虚拟地址与缺页
    追踪已 Commit 的虚拟页获得物理 RAM 的那一刻。
  2. 第2回:物理页的一生
    追踪离开 Working Set 的页的状态转移。
  3. 第3回(本文):节对象与写时复制
    追踪 DLL、文件映射与共享内存共享物理页的机制。

第3回要回答的问题只有一个。

为什么多个进程可以把同一个 DLL 或文件当成一组物理页来用?

预期读者是希望从内部结构(而不只是 API 用法)理解 DLL 共享、CreateFileMappingMapViewOfFile、共享内存、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 映射同一节的同一偏移,视图的起始地址仍可能不同。

把不同虚拟地址映射到同一物理页进程 A 与进程 B 各自拥有不同虚拟地址的视图,但经同一节偏移到达同一物理页 PFN X进程 A: 0x000001A00000 + 0x3000节偏移 0x3000进程 B: 0x000002700000 + 0x3000同一物理页 PFN X

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

因此不该在共享内存里存放原始指针。进程 A 的指针值,在进程 B 里可能是无关的地址。

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

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

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

文件后盾与页面文件后盾如何分开把真正的文件传给 CreateFileMapping 会得到文件后盾节,干净页可从原始文件重读。传入 INVALID_HANDLE_VALUE 则得到页面文件后盾节;页面文件撑住内容,对象销毁后内容消失传入真实文件句柄传入 INVALID_HANDLE_VALUECreateFileMapping文件后盾节页面文件后盾节干净页可从原始文件重读页面文件撑住内容,销毁后消失

图 2: 后盾存储的差异决定内容从哪里还原、能活多久。

3.1. 文件后盾节

把真正的文件传给 CreateFileMapping,就会得到文件后盾节。

  • 只读视图从文件读出需要的页。
  • 读写视图的更改被视为该文件的数据。
  • CoW 视图的更改不会写入原始文件,而会变成私有页。

若文件后盾页是干净的,可以丢弃物理页再从原始文件重读。这个性质支撑了第2回见过的 Standby 与文件缓存效率。

3.2. 页面文件后盾节

INVALID_HANDLE_VALUE 传给 CreateFileMappinghFile 并指定大小,就会得到页面文件后盾节。

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_READONLYPAGE_READWRITE
页保护 由 PE 映像内的属性决定 由映射与视图指定决定
写入 可经可写节或 CoW 私有化 可选择共享写入或 CoW
VirtualQuery Type MEM_IMAGE MEM_MAPPED

使用 SEC_IMAGE 时,决定视图页保护的,主要是可执行映像自身的节属性,而不是传给 CreateFileMapping 的普通保护值。3

靠这个机制,代码等未修改页可以跨许多进程共享同一物理页,只有需要进程私有更改的页才经 CoW 分岔。不过并非每个 DLL 页都一定被共享——还有 ASLR 重定位、加载器修正、热补丁、实际 PE 节属性等因素。

重要的设计是 先共享可共享的页,再延迟复制真正需要更改的页

5. 写时复制从头到尾

让我们从两个进程正在读同一 CoW 页的状态,一路跟到进程 A 写入一个字节。

5.1. 写入之前

写入发生前,两个进程的 PTE 在概念上都到达同一共享页,读取可以直接成功。

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

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

5.2. 写入时的保护错误

CoW 页一开始就不是普通的共享可写页。进程 A 尝试写入时,CPU 会发出保护错误。接手的内存管理器判断这不是非法写入,而是对 CoW 属性的写入。

5.3. 创建新的物理页

依该判断,Windows 会做下列事情。

  1. 为进程 A 取得一页物理页。
  2. 把 PFN X 的内容复制到新页 PFN Y。
  3. 把进程 A 的 PTE 抽换成 PFN Y。
  4. 把进程 A 的保护改成普通读写。
  5. 重新执行失败的写入指令。
写时复制后的分裂状态进程 A 写入后,只有进程 A 的 PTE 被抽换到收到内容副本的私有页 PFN Y,进程 B 的 PTE 仍指向原来的共享页 PFN X写入时复制进程 A PTE私有 PFN Y(R/W,写入后)进程 B PTE共享 PFN X(原来)

图 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_IMAGEVirtualQuery 回报的是该区域来自哪一种初始分配。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 时,进程之后可能写入视图中的每一页。因此 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 的路径。
接到同一文件流的三条路径缓存的 ReadFile/WriteFile 使用 SharedCacheMap,数据映射缺页使用 DataSectionObject,EXE/DLL 映像缺页使用 ImageSectionObject;三者经 SECTION_OBJECT_POINTERS 接到同一文件流缓存的 ReadFile / WriteFileSharedCacheMap数据映射缺页DataSectionObjectEXE/DLL 映像缺页ImageSectionObject同一文件流(SECTION_OBJECT_POINTERS)

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

  1. 以管理员身份启动 Process Explorer。
  2. 启动两个 cmd.exe 进程。
  3. 选择 View > Lower Pane View > DLLs。
  4. 在两个进程中确认同一 DLL 的路径与映射。
  5. 在 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,它不解释 SharedShareCount,最多重试三次。若页仍非驻留,就不产出结果并报告此事。ShareCount 会随时机与内存压力变化,因此请看 Valid == 1 时确认到的 Shared 位变化,而不是固定数值。

10. 实务上要避开的五种误读

10.1. 「共享内存会落在同一虚拟地址」

被共享的是节与物理页。视图的虚拟地址可依进程而异,因此请存偏移而非原始指针。

10.2. 「同一 DLL 就代表每一页都一定共享」

干净的代码页容易共享,但重定位、可写节、CoW,以及测量当下的驻留状态也会产生 Private 页。

10.3. 「CoW 之后就变成 MEM_PRIVATE

VirtualQuery 的 Type 仍是 MEM_MAPPEDMEM_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、数据映射、映像映射分别使用 SharedCacheMapDataSectionObjectImageSectionObject,以分开的路径接到同一文件流。5
  • 共享内存必须把视图与句柄的生命周期、同步、ACL 与偏移设计都纳入,才算安全。

「Windows 内存的深层」三回到此结束。你 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, Memory Protection. 关于多个进程共享同一 DLL 的物理页,以及一方写入时 CoW 复制到新物理页并更新 PTE。  2 3

  5. Microsoft Learn, SECTION_OBJECT_POINTERS structure. 关于 DataSectionObject、SharedCacheMap、ImageSectionObject 如何把文件流的映射与缓存信息绑到内存管理器 / Cache Manager。  2 3 4

  6. Microsoft Learn, MapViewOfFileEx function. 关于 FILE_MAP_COPY 的 CoW、私有页由页面文件撑住、对整个视图收取 Commit,以及应存放偏移而非虚拟地址。  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 的进程,都会在 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 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。

返回博客列表