Windows 記憶體的深層(第 1 回) ── 虛擬位址變成實體 RAM 的瞬間:分頁錯誤的來龍去脈

· · Windows, 記憶體管理, VirtualAlloc, 分頁錯誤, VAD, 效能監視

更新紀錄(僅初版,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 的差別。

  1. Reserve 是在虛擬空間裡佔住位址的階段。 它預留那段範圍,但不在 RAM 或分頁檔配置實體的保存區域。1
  2. Commit 是系統承諾將來保得住那些內容的階段。 它會計入 Commit Charge 並增加 Commit Total,但通常還不會把實體 RAM 綁到全部的頁上。12
  3. 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 節
Reserve、Commit 與第一次存取時發生的事MEM_RESERVE 把範圍與屬性記錄到 VAD,MEM_COMMIT 消耗 Commit Total 以承諾保存,第一次存取的分頁錯誤配置實體頁並加入 Working Set1. MEM_RESERVE2. MEM_COMMIT3. 第一次存取(Touch)把範圍與屬性記錄到 VAD消耗 Commit Total(尚無實體頁)分頁錯誤把已歸零的實體頁綁到 PTE加入 Working Set 並重新執行指令

圖 1: Reserve、Commit、Touch 是不同的事件。實體 RAM 被綁上去,是在最後那次第一次存取的時候。

2. 追蹤虛擬頁的三本台帳

Windows 持有的台帳,用不同的單位在看同一塊記憶體。首先把範圍、虛擬頁、實體頁這三者分開。

台帳 單位 角色
VAD 虛擬位址範圍 管理這是什麼區域、Reserve/Commit、保護,以及與區段的對應
分頁表/PTE 虛擬頁 表示目前對實體頁的轉譯,或尚未實體化的狀態
PFN 資料庫 實體頁 追蹤每一頁 RAM 的擁有者、參照與狀態

VAD 持有的是範圍的資訊,PTE 是虛擬頁,PFN 資料庫是實體頁。分頁錯誤處理常式會交叉比對這幾本台帳,判斷這次存取能不能繼續。

從虛擬位址到實體 RAM 的三本台帳虛擬位址由 VAD 以範圍為單位管理、由 PTE 以虛擬頁為單位管理,PTE 的轉譯目標實體頁則由 PFN 資料庫以實體頁為單位追蹤判斷 Reserve/Commit 與保護有效的轉譯虛擬位址VAD(範圍的台帳)PTE(虛擬頁的台帳)PFN 資料庫(實體頁的台帳)實體 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)。位址轉譯照下面的順序進行。

  1. TLB 裡有轉譯,而且這次存取符合它的保護,就直接用那個結果。
  2. TLB 裡沒有轉譯,就由 CPU 走一遍分頁表。
  3. 有有效的 PTE,而且也符合保護,就登錄到 TLB 並繼續執行。
  4. 沒有有效的轉譯,或發生保護違規,就走進分頁錯誤的入口。保護檢查在轉譯來自 TLB 時同樣會做。

這裡容易混淆的是,TLB 未命中與分頁錯誤是兩回事。

狀況 接下來會發生的事
TLB 裡有轉譯,也符合保護 用快取起來的轉譯繼續執行
TLB 裡沒有轉譯,但有有效的 PTE,也符合保護 走一次分頁表取得轉譯後繼續執行
沒有有效的轉譯,或違反保護 走進分頁錯誤的入口

保護檢查在 TLB 命中時同樣會作用。對唯讀頁寫入、在不可執行的頁上執行指令,即使轉譯已經快取起來也照樣會錯誤。對 CoW 頁寫入之所以能引發錯誤,也是因為這個緣故。

位址轉譯的流程與分頁錯誤的入口即使 TLB 裡有轉譯,只要不符合保護就走進分頁錯誤的入口。TLB 裡沒有轉譯就走分頁表,有有效的 PTE 且符合保護就登錄到 TLB 並繼續執行,無效或保護違規時則走進分頁錯誤的入口有符合保護違規沒有是無效或保護違規記憶體存取TLB 裡有轉譯嗎?符合保護嗎?用那個轉譯繼續執行走進分頁錯誤的入口走一遍分頁表有效的 PTE 且符合保護?登錄到 TLB 並繼續執行(不會錯誤)

圖 3: TLB 未命中可以靠走分頁表解決。會走進分頁錯誤的,是轉譯無效或保護違規的時候,而保護違規在 TLB 命中時也會發生。

5.1. 無效的 PTE 不只是一格空白

雖說是無效的 PTE,內容也不是空的。Windows 會從無效 PTE 的軟體狀態,區分出例如下列幾種情況。

  • 從來沒有實體化過的 demand-zero 頁
  • 還留在 RAM 裡的 Transition 頁
  • 參照 Prototype PTE 的共用頁
  • 已經存到分頁檔的私有頁
  • 保護違規或無效的區域

CPU 的工作只到「這不是一般的有效轉譯」這個判斷為止,然後交給核心;再往下的意義判定由記憶體管理員負責。

6. 分頁錯誤的來龍去脈

我們用六個階段,跟著對已 Commit 私有頁的第一次寫入走一遍。

  1. CPU 準備寫入。
    它查了 TLB 與分頁表,但目標 PTE 上沒有有效的 PFN。
  2. CPU 發出分頁錯誤。
    它把發生錯誤的虛擬位址、讀寫執行的種類、是使用者模式還是核心模式、問題是缺少轉譯還是保護違規,一起交給核心。
  3. 記憶體管理員檢查 VAD 與 PTE。
    判斷是否已 Commit、是否符合保護,以及該歸到 demand-zero、Transition、共用、從後備存放區讀回、CoW、例外的哪一種。
  4. 若是 demand-zero,就取得已歸零的實體頁。
    為了不洩漏其他處理程序的資料,新交出去的頁必須是零。
  5. 更新 PTE 與 PFN 管理資訊。
    在 PTE 設定 PFN 與保護,把實體頁設為 Active,並加入該處理程序的 Working Set。
  6. 重新執行失敗的那道指令。
    因為已經正常解決,使用者模式收不到例外,應用程式就當成一次普通的指派繼續跑下去。

ETW 的分頁錯誤事件,同樣把 Transition、Demand Zero、Copy-on-Write、Guard Page、Hard Page Fault、Access Violation 記成不同的種類。4

也就是說,分頁錯誤從一開始就不是代表「異常」的詞。它是 CPU 走一般路徑轉譯不了時,請作業系統下判斷的共用入口。

分頁錯誤解決去向的分支記憶體管理員判斷 VAD、PTE、保護屬性與存取種類,分派到 demand-zero、重新接上仍在 RAM 的頁、從後備存放區讀取的硬性錯誤、寫入時複製、防護頁面通知或例外第一次存取仍留在 RAM需要讀磁碟CoW 寫入防護頁面無法解決發生分頁錯誤判斷 VAD、PTE、保護、種類demand-zero(軟性)從 Standby 等處重新接上(軟性)硬性錯誤(磁碟 I/O)複製後換掉 PTE解除防護並通知例外(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/sec
  • Memory\\Pages Input/sec
  • Memory\\Page Reads/sec
  • Memory\\Available MBytes
  • Process(<目標>)\\Working Set - Private
  • Process(<目標>)\\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 或分頁檔的實體區域。1
  • MEM_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 應用程式的記憶體使用量調查、存取違規、啟動延遲、分頁,以及原生程式碼的缺陷分析。

參考連結

  1. Microsoft Learn, VirtualAlloc function. 關於 MEM_RESERVE 只預留虛擬位址範圍而不配置實體儲存區;MEM_COMMIT 對系統整體記憶體與分頁檔計入 Commit Charge;已 Commit 的頁初始內容為零;以及實際的實體頁要到被存取時才配置。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8

  2. Microsoft Learn, PERFORMANCE_INFORMATION structure. 關於 CommitTotal 是目前系統的 Commit 頁數,以及 CommitLimit 是不擴充分頁檔也能 Commit 的上限。 ↩ ↩2 ↩3

  3. Microsoft Learn, !vad (WinDbg). 關於 !vad 會顯示 VAD 樹,並可查看起迄 VPN、Commit、Mapped/Private、保護屬性、Control Area 等資訊。 ↩

  4. Microsoft Learn, PageFault_TypeGroup1 class. 關於 ETW 會區分並記錄 Transition Fault、Demand Zero Fault、Copy-on-Write、Guard Page Fault、Hard Page Fault 與 Access Violation。 ↩

  5. Microsoft Learn, Working Set. 關於軟性錯誤可以不存取後備存放區就解決,並且會因其他處理程序的 Working Set、Transition、第一次參照的 demand-zero 等而發生。 ↩ ↩2 ↩3

  6. Microsoft Learn, PageFault_HardFault class. 關於 HardFault 事件含有 FileObject、ReadOffset、ByteCount、VirtualAddress 與執行緒 ID,因此可以追蹤讀取來源。 ↩ ↩2

  7. Microsoft Learn, Access Violation C0000005. 關於 0xC0000005 發生於讀取、寫入或執行無效的記憶體位址,以及例外參數會表示存取種類與違規位址。 ↩ ↩2

  8. Microsoft Learn, Creating Guard Pages. 關於 PAGE_GUARD 提供頁面存取的一次性通知,並會引發 STATUS_GUARD_PAGE_VIOLATION。 ↩

  9. Microsoft Learn, VMMap - Sysinternals. 關於 VMMap 會依種類拆解已 Commit 的虛擬記憶體,並顯示各種類的 Working Set 與詳細位址圖。 ↩

  10. Microsoft Learn, Performance Analysis of Logs (PAL) Tool. 關於 Memory\\Pages Input/sec 是為了解決硬性分頁錯誤而從磁碟讀進來的頁數。 ↩

共用相同標籤的最新文章。能以相近的主題延伸理解。

與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。

本文連結到以下服務頁面,歡迎從最接近的入口查看。

常見問題

整理諮詢這個主題時常見的問題。

對 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 與磁碟等待時間放在同一條時間軸上看相關性。

作者檔案

本文作者的個人檔案頁面。

Go Komura

小村軟體有限公司 代表

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

回到部落格一覽