更新紀錄(僅初版,2026年08月20日 發布)
- 初次發布
引用本文(DOI(已登錄存檔): 10.5281/zenodo.22176096)
以下 DOI 指向先前登錄的存檔,內容可能與目前正文不同。引用目前正文時,請使用本頁網址。
Go Komura(2026)。〈Windows 記憶體的深層(第 1 回) ── 虛擬位址變成實體 RAM 的瞬間:分頁錯誤的來龍去脈〉。小村軟體有限公司。 https://comcomponent.com/zh-TW/blog/windows-memory-internals-page-fault/
- DOI(已登錄存檔)
- 10.5281/zenodo.22176096
- DOI(上次登錄版本)
- 10.5281/zenodo.22176097
「明明 Commit 了 256MiB,Working Set 卻沒有增加同樣多」。使用 VirtualAlloc 時冒出的這個疑問,就是第 1 回的起點。
一般的私有記憶體中,「已經 Commit」和「那一頁已經放進實體 RAM」是兩件事。Windows 會把實體頁的指派往後延,直到應用程式真的碰到那一頁為止。第一次存取引發分頁錯誤時,記憶體管理員會查看 VAD、PTE、保護屬性與後備存放區,把需要的 RAM 一頁一頁綁上去。1
本文要追的,就是這「第一次碰到的那 1 個位元組」抵達實體 RAM 的全程。想先把 Working Set 與 Commit 這些數字的意義整理清楚,請看入門篇「Windows 的「記憶體使用量」代表什麼 ── 正確解讀 Working Set、Private Bytes、Commit 與分頁檔」。本系列不是要重新定義術語,而是從機制面挖出為什麼會是那個數字。
「Windows 記憶體的深層」全 3 回
本系列依照取得實體頁 → 追常駐與回收的流程 → 理解共用與私有化的順序推進。
| 回 | 主題 | 這一回要追的內容 |
|---|---|---|
| 第 1 回(本文) | 虛擬位址與分頁錯誤 | 用 VirtualAlloc 配置的區域,何時取得實體 RAM |
| 第 2 回 | 實體頁的一生 | 離開 Working Set 的頁如何狀態轉移,以及分頁檔的角色 |
| 第 3 回 | 區段物件與寫入時複製 | DLL、檔案對應與共用記憶體如何共用實體頁 |
第 1 回要回答的疑問,只有一個。
已經 Commit 的虛擬位址,究竟在哪一瞬間變成實體 RAM。
| 開始閱讀前 | 內容 |
|---|---|
| 適合讀者 | 想從機制理解記憶體使用量、剛啟動時的分頁錯誤、0xC0000005,以及 VMMap 與 PerfMon 數字的開發者與維運人員 |
| 前提環境 | Windows 10/11 或現行的 Windows Server |
| 前提知識 | 指標與 VirtualAlloc 的基礎 |
| 難度 | 中級。不需要分頁表位元配置或核心偵錯工具的經驗 |
文中會提到內部結構的名稱,但不以依賴特定 Windows 組建的未公開配置為前提。
圖中實線表示始終成立的關係,虛線表示附帶條件的關係(成立條件寫在詳細頁面中各關係的說明中)。關係的完整清單(共 14 條,附依據與可信度)以及主要概念的定義,彙整在知識地圖詳細頁面(日文)。資料:JSON-LD / Turtle
1. 先講結論
一般的私有記憶體中,把下面三個階段分開看,就能看出 Commit 與 Working Set 的差別。
- Reserve 是在虛擬空間裡佔住位址的階段。 它預留那段範圍,但不在 RAM 或分頁檔配置實體的保存區域。1
- Commit 是系統承諾將來保得住那些內容的階段。 它會計入 Commit Charge 並增加 Commit Total,但通常還不會把實體 RAM 綁到全部的頁上。12
- Touch 是真的需要實體頁的階段。 第一次存取會引發分頁錯誤,若這次存取正當,記憶體管理員就配置實體頁並重新執行該指令。
MEM_COMMIT 不是「現在馬上給我 RAM」的命令。但它也不是空頭支票,而是整個系統作出的承諾:將來能用 RAM 或適當的後備存放區保住那些內容。已 Commit 的頁初始內容為零,和實體頁要到第一次存取才指派,這兩件事並不衝突。1
另外,「Reserve/Commit 只是往 VAD 寫一筆」這種理解也不精確。Reserve 主要會建立代表範圍與屬性的 VAD;Commit 則會增加系統的 Commit Total,並記錄該範圍的認可狀態。分頁表的中間層級與個別 PTE,要到需要時才延遲建立。
| 想知道的事 | 要讀的章節 |
|---|---|
| Reserve、Commit、Touch 各改變了什麼 | 第 2〜3 節 |
| VAD、PTE、TLB 各判斷什麼 | 第 4〜6 節 |
| 想分清楚正常的錯誤與 I/O 等待、例外 | 第 7〜9 節 |
| 想用手邊的數字驗證 | 第 10〜11 節 |
flowchart TB
accTitle: Reserve、Commit 與第一次存取時發生的事
accDescr: MEM_RESERVE 把範圍與屬性記錄到 VAD,MEM_COMMIT 消耗 Commit Total 以承諾保存,第一次存取的分頁錯誤配置實體頁並加入 Working Set
reserve["1. MEM_RESERVE"] --> commit["2. MEM_COMMIT"]
commit --> touch["3. 第一次存取(Touch)"]
reserve -.-> vad["把範圍與屬性記錄到 VAD"]
commit -.-> charge["消耗 Commit Total(尚無實體頁)"]
touch --> fault["分頁錯誤"]
fault --> zero["把已歸零的實體頁綁到 PTE"]
zero --> ws["加入 Working Set 並重新執行指令"]
圖 1: Reserve、Commit、Touch 是不同的事件。實體 RAM 被綁上去,是在最後那次第一次存取的時候。
2. 追蹤虛擬頁的三本台帳
Windows 持有的台帳,用不同的單位在看同一塊記憶體。首先把範圍、虛擬頁、實體頁這三者分開。
| 台帳 | 單位 | 角色 |
|---|---|---|
| VAD | 虛擬位址範圍 | 管理這是什麼區域、Reserve/Commit、保護,以及與區段的對應 |
| 分頁表/PTE | 虛擬頁 | 表示目前對實體頁的轉譯,或尚未實體化的狀態 |
| PFN 資料庫 | 實體頁 | 追蹤每一頁 RAM 的擁有者、參照與狀態 |
VAD 持有的是範圍的資訊,PTE 是虛擬頁,PFN 資料庫是實體頁。分頁錯誤處理常式會交叉比對這幾本台帳,判斷這次存取能不能繼續。
flowchart TB
accTitle: 從虛擬位址到實體 RAM 的三本台帳
accDescr: 虛擬位址由 VAD 以範圍為單位管理、由 PTE 以虛擬頁為單位管理,PTE 的轉譯目標實體頁則由 PFN 資料庫以實體頁為單位追蹤
va["虛擬位址"] --> vad["VAD(範圍的台帳)"]
va --> pte["PTE(虛擬頁的台帳)"]
vad -.->|判斷 Reserve/Commit 與保護| pte
pte -->|有效的轉譯| pfn["PFN 資料庫(實體頁的台帳)"]
pfn --> ram["實體 RAM 頁"]
圖 2: 粒度不同的三本台帳。錯誤處理會交叉比對 VAD 與 PTE,再把結果反映到 PFN 那一側。
本文的主角是 VAD 與 PTE。PFN 資料庫留到第 2 回,從實體頁那一側來看。
3. Reserve、Commit、Touch 是不同的事件
3.1. Reserve ── 先佔住位址
先預留一段 256MiB 的連續虛擬位址範圍。
void* base = VirtualAlloc(
nullptr,
256ull * 1024 * 1024,
MEM_RESERVE,
PAGE_NOACCESS);
這個時點發生的事,只是在處理程序的虛擬空間裡佔住位址,不讓其他配置用到這段範圍而已。MEM_RESERVE 不會在 RAM 或分頁檔配置實體的保存區域。1
64 位元處理程序可以用的虛擬空間很大,因此先 Reserve 一大段、之後只把需要的部分 Commit,這種設計在實務上是可行的。
3.2. Commit ── 承諾保得住
接著把已預留的範圍 Commit。
void* committed = VirtualAlloc(
base,
256ull * 1024 * 1024,
MEM_COMMIT,
PAGE_READWRITE);
成功之後,系統的 Commit Total、以及通常會反映到處理程序 Private Bytes 的承諾量都會增加。即使如此,也不代表 256MiB 份的實體頁會一次排好。一般的頁在第一次存取之前,仍然維持沒有實體配置的狀態。12
那麼 Commit 到底有什麼意義?意義在於:系統承擔不了這項承諾時,可以在 Commit 的當下就回傳失敗,而不是等到記憶體用到一半才失敗。
3.3. Touch ── 實體頁變得必要的時候
最後,用下面這行指派,第一次寫入開頭那一頁。
static_cast<unsigned char*>(base)[0] = 1;
CPU 想把虛擬位址轉成實體位址,但 PTE 裡還沒有指向有效實體頁的轉譯。分頁錯誤就發生在這裡。
接手控制權的記憶體管理員判定這是「對已 Commit 且可寫入的私有頁的第一次存取」,於是取得已歸零的實體頁綁到 PTE,並加入 Working Set。接著再讓失敗的那道寫入指令重新執行一次。
從應用程式看來只是一次指派,內部卻跑了在指派途中進入核心、配置實體頁,再回到同一道指令這一連串處理。
4. VAD ── 虛擬空間的範圍台帳
VAD 是 Virtual Address Descriptor 的縮寫,Windows 用 VAD 樹來管理處理程序正在使用的位址範圍。用 WinDbg 的 !vad 命令,可以查看起迄 VPN、Commit、保護屬性、Private/Mapped、Control Area 等資訊。3
VAD 代表性的記錄內容如下。
- 位址範圍的起點與終點
- Private、Mapped、Image 等種類
- Reserve/Commit 的狀態
- 讀取、寫入、執行、寫入時複製等保護
- 與檔案或區段的對應
- 防護頁面等特殊屬性
以範圍為單位保存,是為了減少管理上的浪費。 256MiB 換算成 4KiB 的頁,就是 65,536 頁。
與其一開始就替所有頁建立完整的管理結構,不如用 VAD 管成「這段連續範圍是一次預留」。在這個基礎上,再從真的需要的頁開始具體化。
4.1. 找到 VAD 不代表一定能恢復
「在 VAD 上就能解決錯誤,不在上面就是存取違規」這種說法當入門很方便,但過度簡化了。就算找得到 VAD,例如下列情況仍然無法繼續一般的存取。
- 只有 Reserve,目標頁尚未 Commit
- 保護屬性是
PAGE_NOACCESS - 對唯讀頁寫入
- 從不可執行的頁執行指令
- 第一次碰到防護頁面
- 碰到區段有效範圍以外的位置
反過來說,即使 PTE 無效,只要從 VAD 與 PTE 的軟體狀態看得出這是正當的存取,就能以 demand-zero、Transition 還原、從後備存放區讀回或 CoW 的形式解決。更精確地說,答案是把 VAD、PTE、保護屬性與存取種類合在一起判斷。
5. 分頁表與 TLB
應用程式手上的指標是虛擬位址。CPU 要存取 RAM,就必須把虛擬頁號轉成實體頁號。負責這件事的階層式轉譯表就是分頁表,末端的項目則是 PTE(Page Table Entry)。
有效的 PTE 在概念上持有 PFN、讀寫執行的保護、能否由使用者模式存取、Accessed/Dirty 等資訊。實際的位元配置取決於 CPU 與 Windows 的版本。
不過每次都走一遍分頁表太慢,所以 CPU 會把最近的轉譯結果快取在 TLB(Translation Lookaside Buffer)。位址轉譯照下面的順序進行。
- TLB 裡有轉譯,而且這次存取符合它的保護,就直接用那個結果。
- TLB 裡沒有轉譯,就由 CPU 走一遍分頁表。
- 有有效的 PTE,而且也符合保護,就登錄到 TLB 並繼續執行。
- 沒有有效的轉譯,或發生保護違規,就走進分頁錯誤的入口。保護檢查在轉譯來自 TLB 時同樣會做。
這裡容易混淆的是,TLB 未命中與分頁錯誤是兩回事。
| 狀況 | 接下來會發生的事 |
|---|---|
| TLB 裡有轉譯,也符合保護 | 用快取起來的轉譯繼續執行 |
| TLB 裡沒有轉譯,但有有效的 PTE,也符合保護 | 走一次分頁表取得轉譯後繼續執行 |
| 沒有有效的轉譯,或違反保護 | 走進分頁錯誤的入口 |
保護檢查在 TLB 命中時同樣會作用。對唯讀頁寫入、在不可執行的頁上執行指令,即使轉譯已經快取起來也照樣會錯誤。對 CoW 頁寫入之所以能引發錯誤,也是因為這個緣故。
flowchart TB
accTitle: 位址轉譯的流程與分頁錯誤的入口
accDescr: 即使 TLB 裡有轉譯,只要不符合保護就走進分頁錯誤的入口。TLB 裡沒有轉譯就走分頁表,有有效的 PTE 且符合保護就登錄到 TLB 並繼續執行,無效或保護違規時則走進分頁錯誤的入口
access["記憶體存取"] --> tlb{"TLB 裡有轉譯嗎?"}
tlb -->|有| perm{"符合保護嗎?"}
perm -->|符合| go["用那個轉譯繼續執行"]
perm -->|保護違規| entry["走進分頁錯誤的入口"]
tlb -->|沒有| walk["走一遍分頁表"]
walk --> valid{"有效的 PTE 且符合保護?"}
valid -->|是| register["登錄到 TLB 並繼續執行(不會錯誤)"]
valid -->|無效或保護違規| entry
圖 3: TLB 未命中可以靠走分頁表解決。會走進分頁錯誤的,是轉譯無效或保護違規的時候,而保護違規在 TLB 命中時也會發生。
5.1. 無效的 PTE 不只是一格空白
雖說是無效的 PTE,內容也不是空的。Windows 會從無效 PTE 的軟體狀態,區分出例如下列幾種情況。
- 從來沒有實體化過的 demand-zero 頁
- 還留在 RAM 裡的 Transition 頁
- 參照 Prototype PTE 的共用頁
- 已經存到分頁檔的私有頁
- 保護違規或無效的區域
CPU 的工作只到「這不是一般的有效轉譯」這個判斷為止,然後交給核心;再往下的意義判定由記憶體管理員負責。
6. 分頁錯誤的來龍去脈
我們用六個階段,跟著對已 Commit 私有頁的第一次寫入走一遍。
- CPU 準備寫入。
它查了 TLB 與分頁表,但目標 PTE 上沒有有效的 PFN。 - CPU 發出分頁錯誤。
它把發生錯誤的虛擬位址、讀寫執行的種類、是使用者模式還是核心模式、問題是缺少轉譯還是保護違規,一起交給核心。 - 記憶體管理員檢查 VAD 與 PTE。
判斷是否已 Commit、是否符合保護,以及該歸到 demand-zero、Transition、共用、從後備存放區讀回、CoW、例外的哪一種。 - 若是 demand-zero,就取得已歸零的實體頁。
為了不洩漏其他處理程序的資料,新交出去的頁必須是零。 - 更新 PTE 與 PFN 管理資訊。
在 PTE 設定 PFN 與保護,把實體頁設為 Active,並加入該處理程序的 Working Set。 - 重新執行失敗的那道指令。
因為已經正常解決,使用者模式收不到例外,應用程式就當成一次普通的指派繼續跑下去。
ETW 的分頁錯誤事件,同樣把 Transition、Demand Zero、Copy-on-Write、Guard Page、Hard Page Fault、Access Violation 記成不同的種類。4
也就是說,分頁錯誤從一開始就不是代表「異常」的詞。它是 CPU 走一般路徑轉譯不了時,請作業系統下判斷的共用入口。
flowchart LR
accTitle: 分頁錯誤解決去向的分支
accDescr: 記憶體管理員判斷 VAD、PTE、保護屬性與存取種類,分派到 demand-zero、重新接上仍在 RAM 的頁、從後備存放區讀取的硬性錯誤、寫入時複製、防護頁面通知或例外
faultIn["發生分頁錯誤"] --> judge["判斷 VAD、PTE、保護、種類"]
judge -->|第一次存取| dz["demand-zero(軟性)"]
judge -->|仍留在 RAM| soft["從 Standby 等處重新接上(軟性)"]
judge -->|需要讀磁碟| hard["硬性錯誤(磁碟 I/O)"]
judge -->|CoW 寫入| cow["複製後換掉 PTE"]
judge -->|防護頁面| guard["解除防護並通知"]
judge -->|無法解決| av["例外(0xC0000005 等)"]
圖 4: 從同一個入口進來的錯誤,會依判斷結果分成六種結局。防護頁面的細節在第 9 節處理。
7. Demand-zero ── 不讀磁碟的軟性錯誤
Demand-zero 是第一次碰到已 Commit 私有頁時會發生的代表性軟性錯誤。Microsoft 的 Working Set 說明文件,也把「處理程序第一次參照已配置的虛擬頁」列為軟性錯誤的例子。5
Demand-zero 有下列特徵。
- 不需要從磁碟讀取原始資料
- 初始內容為零
- 綁上可用的實體頁
- Working Set 與累計的 Page Fault Count 會增加
- 光是這項處理不會讓
Memory\\Pages Input/sec增加
因此,剛啟動時 Page Faults/sec 就算飆高,也不能光憑這點斷定儲存體塞住了。
7.1. 延遲配置是拿 RAM 去換第一次存取的成本
就算 Commit 了 256MiB,實際只用 8MiB 的話,能讓剩下 248MiB 不必放進 RAM 的延遲配置就很合理。代價是第一次存取要多付一筆錯誤處理的成本。
對延遲要求嚴格的處理,有時會選擇在開始之前先碰過每一頁的「預先錯誤(prefault)」。不過這不是免費的最佳化。它是把第一次存取的處理提前做完,同時也把 RAM 常駐量提前推高的一種取捨。
8. 軟性錯誤與硬性錯誤
分界點在於需不需要對後備存放區做讀取 I/O。光看分頁錯誤的次數,看不出這個差別。
8.1. 軟性錯誤
軟性錯誤是不必對後備存放區做讀取 I/O 就能解決的錯誤。舉幾個代表性的例子。
- Demand-zero
- 重新接上還留在 Standby/Transition 的頁
- 接上已經在其他處理程序 Working Set 裡的共用頁
- 接上已經預先讀取好的頁
- 原始頁仍然常駐時的寫入時複製
切換進核心模式、鎖、PTE/PFN 更新、TLB 一致性處理等 CPU 成本還是會有,但不會有儲存體等待。5
8.2. 硬性錯誤
另一方面,需要的頁在 RAM 裡哪裡都找不到、必須從後備存放區讀進來時,就是硬性錯誤。這時的讀取來源不只有分頁檔。
- 被寫出到分頁檔的私有頁
- 記憶體對應檔
- EXE 或 DLL 的映像
- 檔案快取所參照的資料檔
ETW 的 HardFault 事件裡含有 FileObject、ReadOffset、ByteCount,可以追出實際的讀取來源。6
因此,Hard Fault = 讀了 pagefile.sys 這個等號並不成立。
需要讀取後備存放區時,要求就會進入 Windows 的 I/O 堆疊。IRP 以及發出與完成的流程在「Windows I/O 的深層(第 1 回)」,與檔案快取的匯流處則在「Windows I/O 的深層(第 4 回)」處理。頁在 RAM 裡的話,記憶體管理員自己就能回來;不在的話就得發出 I/O,並讓發生錯誤的執行緒等到完成為止。
9. 無法解決的錯誤會變成例外
查過 VAD 與 PTE 之後,仍然無法當成正當的配置、從後備存放區讀回或 CoW 來解決的錯誤,會以例外的形式送到使用者模式。
9.1. 會變成存取違規的情況
代表就是 STATUS_ACCESS_VIOLATION,例外碼 0xC0000005。它發生在讀取、寫入或執行無效位址時,第一個例外參數表示存取種類,第二個參數表示違規的位址。7
典型的發生模式如下。
- 讀取 NULL、已釋放或陣列外的位址
- 對唯讀頁寫入
- 從 DEP/NX 標成不可執行的頁執行指令
- 碰到尚未 Commit 的 Reserve 範圍
9.2. 防護頁面當成只通知一次的機制來用
另外,PAGE_GUARD 的意思稍有不同。它是只通知一次存取的機制,會引發 STATUS_GUARD_PAGE_VIOLATION,用在堆疊擴張等場合。8
正常的延遲配置、從後備存放區讀回、CoW、防護頁面的通知,以至於最後的存取違規,從 CPU 看來全都匯集到同一個分頁錯誤入口。決定結局的,是 VAD、PTE、保護屬性與存取種類的組合。
10. 自己動手確認
實驗的目的,是把 Commit 增加的階段與 Working Set 增加的階段分開觀察。
下面這支 C++ 程式會 Reserve 256MiB、Commit,對每一頁寫入 1 個位元組,最後 Release。每個階段都會等 Enter,所以可以在那裡確認 VMMap 與 PerfMon 的數值。
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <psapi.h>
#include <cstdio>
#include <cstdlib>
#pragma comment(lib, "Psapi.lib")
constexpr SIZE_T kSize = 256ull * 1024 * 1024;
void PrintMemory(const char* stage)
{
PROCESS_MEMORY_COUNTERS_EX c{};
c.cb = sizeof(c);
if (!GetProcessMemoryInfo(
GetCurrentProcess(),
reinterpret_cast<PROCESS_MEMORY_COUNTERS*>(&c),
sizeof(c))) {
std::printf("GetProcessMemoryInfo failed: %lu\n", GetLastError());
return;
}
std::printf(
"%-10s WS=%zu MiB Private=%zu MiB Faults=%lu\n",
stage,
c.WorkingSetSize / 1024 / 1024,
c.PrivateUsage / 1024 / 1024,
c.PageFaultCount);
}
void Pause(const char* message)
{
PrintMemory(message);
std::puts("Press Enter...");
(void)std::getchar();
}
int main()
{
SYSTEM_INFO si{};
GetSystemInfo(&si);
std::printf("PID=%lu, page=%lu bytes\n",
GetCurrentProcessId(), si.dwPageSize);
void* base = VirtualAlloc(nullptr, kSize, MEM_RESERVE, PAGE_NOACCESS);
if (!base) {
std::fprintf(stderr, "Reserve failed: %lu\n", GetLastError());
return EXIT_FAILURE;
}
Pause("reserved");
if (!VirtualAlloc(base, kSize, MEM_COMMIT, PAGE_READWRITE)) {
std::fprintf(stderr, "Commit failed: %lu\n", GetLastError());
VirtualFree(base, 0, MEM_RELEASE);
return EXIT_FAILURE;
}
Pause("committed");
auto* bytes = static_cast<volatile unsigned char*>(base);
for (SIZE_T offset = 0; offset < kSize; offset += si.dwPageSize) {
bytes[offset] = 1;
}
Pause("touched");
if (!VirtualFree(base, 0, MEM_RELEASE)) {
std::fprintf(stderr, "Release failed: %lu\n", GetLastError());
return EXIT_FAILURE;
}
Pause("released");
}
在 Visual Studio 的 x64 Native Tools Command Prompt 上,可以用下列命令建置。
cl /std:c++20 /EHsc /W4 memory_fault_demo.cpp
10.1. 在 VMMap 要看什麼
VMMap 是把已預留的虛擬記憶體、Commit、Working Set、Private、Shareable 依種類顯示出來的工具。9 各階段預期的變化如下。
| 階段 | 預期的變化 |
|---|---|
| Reserve | Address Space 的 Size 會增加,但 Commit/WS 不會增加同樣多 |
| Commit | Private 的 Commit 增加約 256MiB |
| Touch | Working Set 與 Private WS 大幅增加,Fault Count 也增加 |
| Release | 目標範圍消失,Commit 與 WS 下降 |
要看的是階段之間的變化,不是數值有沒有對上
實際的數值會隨 runtime、資安產品、記憶體壓力與觀測時機而變。請不要看是否剛好等於 256MiB,而是看階段之間往哪個方向移動。
10.2. 在 PerfMon 分開軟性與硬性
在 PerfMon 裡,把下列計數器排在同一條時間軸上。
Process(<目標>)\\Page Faults/secMemory\\Pages Input/secMemory\\Page Reads/secMemory\\Available MBytesProcess(<目標>)\\Working Set - PrivateProcess(<目標>)\\Private Bytes
Process\\Page Faults/sec 裡軟性與硬性都包含在內。另一方面,Memory\\Pages Input/sec 是為了解決硬性錯誤而從磁碟讀進來的頁數。10
在這支程式的 Touch 階段,Page Faults/sec 就算飆高,Pages Input/sec 應該也不會大幅增加。因為新 Commit 的頁是用 demand-zero 實體化的,不需要從磁碟讀原始資料。
同名的處理程序還要用 PID 確認
另外,同名處理程序有多個時,PerfMon 的 process#1 這類編號可能在重新啟動後改變。請與顯示 PID 的計數器交叉比對,或是用 Process V2、ETW/WPA 以 PID 來識別。
11. 實務上想避開的三種誤讀
11.1. 「Commit 增加了,所以是 RAM 洩漏」
Commit 是承諾保住內容的量,還沒 Touch 的頁有可能並不常駐在 RAM。判斷洩漏時,要看 Private Bytes 的時間變化、配置的明細,以及處理結束後是否回到基準值。
11.2. 「Page Faults/sec 很高,所以磁碟很慢」
軟性錯誤不伴隨磁碟 I/O。請把 Page Faults/sec、Pages Input/sec 與儲存體等待分開看,必要時再用 ETW 的 HardFault 事件追到讀取來源檔案與堆疊。
11.3. 「把 Working Set 清空就能修好洩漏」
就算把頁移出 Working Set,Commit 與擁有權也不會被釋放。頁只是移到 Standby 或 Modified,之後再錯誤回來而已。要修好洩漏,必須由配置的一方執行 VirtualFree、堆積釋放、物件解構等動作。
被移出去的那個實體頁去了哪裡,第 2 回會接著追。
12. 摘要
MEM_RESERVE會佔住虛擬位址範圍,但不配置 RAM 或分頁檔的實體區域。1MEM_COMMIT會消耗 Commit,保證將來保得住內容,但一般的實體頁要到第一次存取才配置。12- VAD 是範圍的台帳,PTE 是虛擬頁的台帳,PFN 資料庫是實體頁的台帳。
- TLB 未命中不是分頁錯誤。只要有有效的 PTE,走一次分頁表就能解決。
- Demand-zero、Transition 還原、接上共用頁,都是不必磁碟 I/O 就能解決的軟性錯誤。5
- 必須從分頁檔、DLL、EXE、對應檔讀取的話,就是硬性錯誤。6
- 檢查 VAD、PTE 與保護屬性後仍然無法解決,就會變成
0xC0000005這類例外。7 - 判斷效能時不要只看
Page Faults/sec,而要把Pages Input/sec、Available、Working Set、Private Bytes 與儲存體等待放在同一條時間軸上看。
接下來是第 2 回「實體頁的一生:五份清單與分頁檔的真相」。
把 Commit 這項承諾換成實體頁之後,那一頁一旦離開 Working Set 會去哪裡,我們從 PFN 資料庫與各種頁清單來追。
相關文章
- Windows 的「記憶體使用量」代表什麼 ── 正確解讀 Working Set、Private Bytes、Commit 與分頁檔
- Windows I/O 的深層(第 1 回) ── Windows 的 I/O 架構與 IRP
- Windows I/O 的深層(第 4 回) ── 快取管理員與 WriteFile
- 用 WinDbg + SOS 解析當機傾印檔
- Windows 應用程式的當機傾印收集入門
相關諮詢領域
小村軟體有限公司承接 Windows 應用程式的記憶體使用量調查、存取違規、啟動延遲、分頁,以及原生程式碼的缺陷分析。
參考連結
-
Microsoft Learn, VirtualAlloc function. 關於
MEM_RESERVE只預留虛擬位址範圍而不配置實體儲存區;MEM_COMMIT對系統整體記憶體與分頁檔計入 Commit Charge;已 Commit 的頁初始內容為零;以及實際的實體頁要到被存取時才配置。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 -
Microsoft Learn, PERFORMANCE_INFORMATION structure. 關於
CommitTotal是目前系統的 Commit 頁數,以及CommitLimit是不擴充分頁檔也能 Commit 的上限。 ↩ ↩2 ↩3 -
Microsoft Learn, !vad (WinDbg). 關於
!vad會顯示 VAD 樹,並可查看起迄 VPN、Commit、Mapped/Private、保護屬性、Control Area 等資訊。 ↩ -
Microsoft Learn, PageFault_TypeGroup1 class. 關於 ETW 會區分並記錄 Transition Fault、Demand Zero Fault、Copy-on-Write、Guard Page Fault、Hard Page Fault 與 Access Violation。 ↩
-
Microsoft Learn, Working Set. 關於軟性錯誤可以不存取後備存放區就解決,並且會因其他處理程序的 Working Set、Transition、第一次參照的 demand-zero 等而發生。 ↩ ↩2 ↩3
-
Microsoft Learn, PageFault_HardFault class. 關於 HardFault 事件含有 FileObject、ReadOffset、ByteCount、VirtualAddress 與執行緒 ID,因此可以追蹤讀取來源。 ↩ ↩2
-
Microsoft Learn, Access Violation C0000005. 關於
0xC0000005發生於讀取、寫入或執行無效的記憶體位址,以及例外參數會表示存取種類與違規位址。 ↩ ↩2 -
Microsoft Learn, Creating Guard Pages. 關於
PAGE_GUARD提供頁面存取的一次性通知,並會引發STATUS_GUARD_PAGE_VIOLATION。 ↩ -
Microsoft Learn, VMMap - Sysinternals. 關於 VMMap 會依種類拆解已 Commit 的虛擬記憶體,並顯示各種類的 Working Set 與詳細位址圖。 ↩
-
Microsoft Learn, Performance Analysis of Logs (PAL) Tool. 關於
Memory\\Pages Input/sec是為了解決硬性分頁錯誤而從磁碟讀進來的頁數。 ↩
相關文章
共用相同標籤的最新文章。能以相近的主題延伸理解。
Windows 記憶體的深層(第2回) ── 實體分頁的一生:五份清單與分頁檔的真相
串起 PFN 資料庫、Standby、Modified、記憶體壓縮與分頁檔,說明離開 Working Set 的實體分頁會去哪裡。
Windows 記憶體的深層(第3回)── 區段物件與寫入時複製:DLL 與檔案對應的真面目
串起區段物件、映像/資料對應、共用快取與寫入時複製,說明 DLL 與共用記憶體如何共用實體分頁。
Windows 的「記憶體使用量」代表什麼 ── 正確解讀 Working Set、Private Bytes、Commit 與分頁檔
工作管理員的記憶體、Working Set、Private Bytes、Commit 並非同一個數值。本文解說 Windows 虛擬記憶體與實體記憶體的關係、分頁檔的角色,以及在記憶體不足或洩漏調查時應該檢視的指標。
為什麼「剩餘1秒」遲遲不結束?── 進度列與剩餘時間的運作原理
剩餘1秒持續很久、停在99%、一直顯示準備中,分別是怎麼回事?從進度的分母、速度預測、最後的處理步驟與畫面更新逐一說明,並提供同一工作不同進度顯示的互動示範。
Windows 共用資料夾為何時而能連線、時而失敗——釐清 Kerberos、NTLM 與認證資訊問題
從症狀與記錄釐清 Windows 共用資料夾連線不穩定的原因。說明 IP 與名稱差異、只有應用程式失敗、空白密碼、1219、重新啟動及 SMB 簽章的檢查步驟,以及各項結果能證明什麼。
相關主題
與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。
Windows 技術主題
彙整 KomuraSoft LLC 關於 Windows 開發、故障調查與既有資產活用文章的主題中心。
常見問題
整理諮詢這個主題時常見的問題。
- 對 VirtualAlloc 指定 MEM_COMMIT 的當下就會取得 RAM 嗎?
- 一般的私有記憶體中,Commit 會消耗系統的認可餘裕,但對應的實體頁要到第一次被存取時才會指派。第一次以寫入碰到的頁,是在 demand-zero 錯誤的處理過程中取得實體頁。
- 分頁錯誤代表異常或效能問題嗎?
- 不是。demand-zero 或從 Standby 回來這類不伴隨磁碟 I/O 的軟性錯誤都是正常運作。判斷效能時不能只看 Page Faults/sec,還要一併觀察 Pages Input/sec、儲存體等待與 Available MBytes。
- TLB 未命中和分頁錯誤是同一件事嗎?
- 是兩回事。即使 TLB 裡沒有轉譯結果,只要分頁表的 PTE 有效,CPU 也只是走一次表並重新登錄轉譯而已。PTE 無效,或是發生保護違規時,才會走進分頁錯誤的入口。
- 只要位址範圍在 VAD 裡就不會發生存取違規嗎?
- 不一定。除了 VAD 存不存在,記憶體管理員還會評估是 Reserve 還是 Commit、讀寫執行的保護屬性、防護頁面、PTE 的狀態等。無法解決時就會變成 0xC0000005 這類例外。
- Page Faults/sec 偏高就代表 RAM 不足嗎?
- 光憑這一項無法判斷。Page Faults/sec 裡也包含大量軟性錯誤。必須把 Memory\Pages Input/sec、Memory\Page Reads/sec、Available MBytes 與磁碟等待時間放在同一條時間軸上看相關性。