上一篇「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 位元。
- 可以在共享記憶體裡存放原始指標嗎?
- 通常不應該。即使是同一區段,也不保證各行程的檢視會放在同一虛擬位址。共享結構請用相對於基底的位移、固定寬度整數,以及明確的配置與同步方式。