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 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。

回到部落格一覽