ความลึกของหน่วยความจำ Windows (ตอนที่ 1) — ช่วงที่ที่อยู่เสมือนกลายเป็น RAM กายภาพ: page fault จากต้นจนจบ
· Go Komura · Windows, การจัดการหน่วยความจำ, VirtualAlloc, Page fault, VAD, การเฝ้าดูประสิทธิภาพ
การส่ง MEM_COMMIT ให้ VirtualAlloc ทำให้ Commit เพิ่มขึ้นทันที อย่างไรก็ตาม Working Set ไม่จำเป็นต้องเพิ่มเท่ากัน แล้วหน่วยความจำที่คุณคิดว่าจัดสรรไปอยู่ที่ไหน?
คำตอบคือ เพจส่วนใหญ่ยังไม่มี RAM กายภาพที่สอดคล้อง Windows เลื่อนการกำหนดเพจกายภาพจนกว่าแอปพลิเคชันจะแตะเพจจริง เมื่อการเข้าถึงครั้งแรกทำให้ CPU ยก page fault ตัวจัดการหน่วยความจำตรวจ VAD, PTE, แอตทริบิวต์การป้องกัน และ backing store แล้วผูก RAM ทีละเพจหากจำเป็น1
บทความนี้ตามเส้นทางที่ «ไบต์แรกที่คุณแตะ» เดินไปจนถึง RAM กายภาพ หากต้องการจัดความหมายของตัวเลขอย่าง Working Set และ Commit ก่อน ดูบทนำ «What Does Windows’ “Memory Usage” Actually Mean? — Correctly Reading Working Set, Private Bytes, Commit, and the Page File» ซีรีส์นี้ไม่นิยามคำที่ใช้ที่นั่นใหม่ แต่ขุดจากฝั่งกลไกว่า «ทำไมตัวเลขจึงออกมาแบบนั้น»
«ความลึกของหน่วยความจำ Windows» — ทั้ง 3 ตอน
- ตอนที่ 1 (บทความนี้): ที่อยู่เสมือนและ page fault
เราตามว่าเมื่อไรบริเวณที่จัดสรรด้วยVirtualAllocได้ RAM กายภาพ - ตอนที่ 2: ชีวิตของเพจกายภาพ
เราตามว่าเพจที่ออกจาก Working Set เคลื่อนผ่าน Modified, Standby, Free และ Zeroed อย่างไร - ตอนที่ 3: ออบเจ็กต์เซกชันและ copy-on-write
เราตามว่าทำไม DLL การแมปไฟล์ และหน่วยความจำร่วมจึงแชร์เพจกายภาพได้
คำถามที่ตอนที่ 1 ตอบมีเพียงข้อเดียว
ที่อยู่เสมือนที่ Commit แล้วกลายเป็น RAM กายภาพในชั่วขณะใด?
ผู้อ่านที่ตั้งใจ คือนักพัฒนาและผู้ปฏิบัติการที่ต้องการเข้าใจจากกลไก การใช้หน่วยความจำของแอป Windows, page fault ทันทีหลังสตาร์ต, 0xC0000005 และตัวเลขใน VMMap กับ PerfMon ข้อกำหนดเบื้องต้น คือ Windows 10/11 หรือ Windows Server ปัจจุบัน และพื้นหลังที่ต้องมี คือพอยน์เตอร์กับพื้นฐาน VirtualAlloc ไม่ต้องมีประสบการณ์เลย์เอาต์บิตของตารางเพจหรือดีบักเกอร์เคอร์เนล ความยากระดับกลาง เราใช้ชื่อโครงสร้างภายใน แต่ไม่สมมติเลย์เอาต์ที่ไม่ได้เอกสารซึ่งขึ้นกับบิลด์ Windows เฉพาะ
1. สรุปก่อน
กระแสหน่วยความจำส่วนตัวทั่วไป ในหนึ่งบรรทัด คือแบบนี้
Reserve สำรองช่วงที่อยู่เสมือน Commit บันทึกภาระ commit (commit charge) กับขีดจำกัด commit เพื่อรับประกันที่เก็บเนื้อหาในอนาคต และ page fault ตอนเข้าถึงครั้งแรกกำหนดเพจกายภาพ
กล่าวอีกอย่าง MEM_COMMIT ไม่ใช่คำสั่งที่ว่า «จัดสรร RAM เดี๋ยวนี้» เอกสาร VirtualAlloc ของ Microsoft ก็รับประกันว่าเนื้อหาเริ่มต้นของเพจที่ Commit เป็นศูนย์ พร้อมอธิบายว่าเพจกายภาพจริงยังไม่ถูกกำหนดจนกว่าจะเข้าถึงที่อยู่เสมือน1
ทั้งนี้ การอ้างว่า «Reserve/Commit เขียนลง VAD เท่านั้น» ก็ไม่แม่น ในทางปฏิบัติ Reserve สร้าง VAD ที่แทนช่วงที่อยู่เสมือนและแอตทริบิวต์เป็นหลัก และ Commit เพิ่ม Commit Total ของระบบแล้วบันทึกสถานะที่ commit ของช่วง ชั้นตารางเพจกลางและ PTE แต่ละตัวถูกสร้างแบบขี้เกียจเมื่อจำเป็น และการผูกสุดท้ายกับ RAM กายภาพมักเกิดตอนเข้าถึงครั้งแรก
Commit ไม่ใช่คำสัญญาเปล่า แต่เป็น คำสัญญาทั้งระบบว่าเนื้อหาจะเก็บในอนาคตใน RAM หรือ backing store ที่เหมาะสมได้ จุดเข้าที่ทำให้คำสัญญานั้นเป็นจริงทีละเพจคือ page fault
flowchart TB
accTitle: สิ่งที่เกิดตอน Reserve, Commit และการเข้าถึงครั้งแรก
accDescr: MEM_RESERVE บันทึกช่วงและแอตทริบิวต์ใน VAD, MEM_COMMIT ใช้ Commit Total เพื่อสัญญาที่เก็บ และ page fault ตอนเข้าถึงครั้งแรกกำหนดเพจกายภาพแล้วเพิ่มเข้า Working Set
reserve["1. MEM_RESERVE"] --> commit["2. MEM_COMMIT"]
commit --> touch["3. เข้าถึงครั้งแรก(Touch)"]
reserve -.-> vad["บันทึกช่วงและแอตทริบิวต์ใน VAD"]
commit -.-> charge["ใช้ Commit Total(ยังไม่มีเพจกายภาพ)"]
touch --> fault["Page fault"]
fault --> zero["ผูกเพจกายภาพที่ล้างศูนย์กับ PTE"]
zero --> ws["เพิ่มเข้า Working Set แล้วรันคำสั่งซ้ำ"]
ภาพ 1: Reserve, Commit และ Touch เป็นเหตุการณ์แยกกัน RAM กายภาพถูกผูกเฉพาะขั้นสุดท้าย คือการเข้าถึงครั้งแรก
2. สามบัญชีที่ตามเพจเสมือน
เพื่อเข้าใจเส้นทางจากที่อยู่เสมือนสู่ RAM กายภาพ คุณต้องแยกสามชนิดบัญชีที่ Windows ถือ
| บัญชี | หน่วย | บทบาท |
|---|---|---|
| VAD | ช่วงที่อยู่เสมือน | จัดการว่าบริเวณคืออะไร Reserve/Commit การป้องกัน และการคู่กับเซกชัน |
| ตารางเพจ / PTE | เพจเสมือน | แทนคำแปลปัจจุบันสู่เพจกายภาพ หรือสถานะที่ยังไม่เกิดขึ้นจริง |
| ฐานข้อมูล PFN | เพจกายภาพ | ตามความเป็นเจ้าของ การอ้างอิง และสถานะของแต่ละเพจ RAM |
VAD ถือข้อมูลเกี่ยวกับ ช่วง PTE เกี่ยวกับ เพจเสมือน และฐานข้อมูล PFN เกี่ยวกับ เพจกายภาพ ตัวจัดการ page fault ตรวจไขว้สิ่งเหล่านี้เพื่อตัดสินว่าการเข้าถึงจะต่อได้หรือไม่
flowchart TB
accTitle: สามบัญชีจากที่อยู่เสมือนสู่ RAM กายภาพ
accDescr: ที่อยู่เสมือนถูกจัดการโดย VAD ที่ความละเอียดช่วงและโดย PTE ที่ความละเอียดเพจเสมือน และฐานข้อมูล PFN ตามเพจกายภาพซึ่งเป็นเป้าหมายคำแปลของ PTE ที่ความละเอียดเพจกายภาพ
va["ที่อยู่เสมือน"] --> vad["VAD(บัญชีช่วง)"]
va --> pte["PTE(บัญชีเพจเสมือน)"]
vad -.->|ตัดสิน Reserve/Commit และการป้องกัน| pte
pte -->|คำแปลที่ใช้ได้| pfn["ฐานข้อมูล PFN(บัญชีเพจกายภาพ)"]
pfn --> ram["เพจ RAM กายภาพ"]
ภาพ 2: สามบัญชีที่ความละเอียดต่างกัน การจัดการ fault ตรวจไขว้ 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 ยังไม่มีคำแปลที่ใช้ได้สู่เพจกายภาพ page fault เกิดที่นี่
ตัวจัดการหน่วยความจำที่รับการควบคุมตัดสินว่านี่คือ «การเข้าถึงครั้งแรกสู่เพจส่วนตัวที่ Commit แล้วและเขียนได้» ได้เพจกายภาพที่ล้างศูนย์ ผูกกับ PTE และเพิ่มเข้า Working Set จากนั้นรันคำสั่งเขียนที่ล้มเหลวซ้ำ
จากแอปดูเป็นการกำหนดค่าธรรมดา แต่ภายใน การควบคุมเข้าเคอร์เนลกลางการกำหนดค่า เพจกายภาพถูกกำหนด และการทำงานกลับไปคำสั่งเดิม
4. VAD — บัญชีช่วงของปริภูมิเสมือน
VAD ย่อจาก Virtual Address Descriptor และ Windows จัดการช่วงที่อยู่ที่โพรเซสใช้อยู่เป็นต้นไม้ของ VAD ด้วยคำสั่ง !vad ของ WinDbg คุณตรวจ VPN ต้นและปลาย, Commit, แอตทริบิวต์การป้องกัน, Private/Mapped, Control Area และอื่น ๆ ได้3
ข้อมูลตัวแทนที่ VAD บันทึกมีดังนี้
- จุดเริ่มและจุดจบของช่วงที่อยู่
- ชนิด เช่น Private, Mapped หรือ Image
- สถานะ Reserve/Commit
- การป้องกันอย่างอ่าน เขียน รัน และ copy-on-write
- การคู่กับไฟล์หรือเซกชัน
- แอตทริบิวต์พิเศษอย่างเพจเฝ้า
เหตุผลที่จัดการตามช่วงคือประสิทธิภาพ 256MiB คือ 65,536 เพจที่ 4KiB แทนที่จะสร้างโครงสร้างจัดการครบสำหรับทุกเพจล่วงหน้า สิ้นเปลืองน้อยกว่าที่จะถือ «ช่วงต่อเนื่องนี้คือการจองหนึ่งครั้ง» ใน VAD แล้วทำให้เพจเป็นจริงเมื่อจำเป็น
4.1. พบ VAD ไม่ได้รับประกันการกู้คืน
«ถ้าอยู่ใน VAD fault ถูกแก้ ถ้าไม่อยู่คุณได้การละเมิดการเข้าถึง» เป็นคำอธิบายระดับต้นที่สะดวก แต่ทำให้เรียบเกินไป แม้พบ VAD การเข้าถึงทั่วไปก็ต่อไม่ได้ในกรณีอย่างต่อไปนี้
- Reserve อย่างเดียว และเพจเป้าหมายยังไม่ Commit
PAGE_NOACCESS- เขียนลงเพจอ่านอย่างเดียว
- รันคำสั่งจากเพจที่รันไม่ได้
- แตะเพจเฝ้าครั้งแรก
- แตะนอกช่วงที่ใช้ได้ของเซกชัน
ในทางกลับกัน แม้ PTE จะไม่ถูกต้อง หากสถานะซอฟต์แวร์ของ VAD และ PTE แสดงการเข้าถึงที่ชอบธรรม ก็แก้เป็น demand-zero การคืน Transition, page-in หรือ 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 แล้วทำงานต่อ
- หากไม่มีคำแปลที่ใช้ได้ หรือมีการละเมิดการป้องกัน การควบคุมไปที่จุดเข้า page fault การตรวจการป้องกันทำทั้งเมื่อคำแปลมาจาก TLB
ดังที่กระแสนี้แสดง TLB miss กับ page fault เป็นคนละเรื่อง หากปัญหาเดียวคือ TLB ไม่มีคำแปลและ PTE ใช้ได้ สิ่งที่เกิดคือการเดินตารางเพจเท่านั้น ในทางกลับกัน แม้ TLB จะมีคำแปล การละเมิดการป้องกันอย่างเขียนลงเพจอ่านอย่างเดียวหรือรันคำสั่งบนเพจที่รันไม่ได้ก็ไปที่จุดเข้า page fault นั่นคือเหตุผลที่การเขียนลงเพจ CoW อาจ fault ได้แม้คำแปลจะถูกแคชแล้ว
flowchart TB
accTitle: กระแสการแปลที่อยู่และจุดเข้า page fault
accDescr: แม้ TLB จะมีคำแปล การไม่ตรงการป้องกันก็ไปที่จุดเข้า page fault หาก TLB ไม่มีคำแปลจะเดินตารางเพจ PTE ที่ใช้ได้และตรงการป้องกันด้วยจะถูกลงทะเบียนใน TLB แล้วทำงานต่อ และคำแปลที่ใช้ไม่ได้หรือการละเมิดการป้องกันไปที่จุดเข้า page fault
access["การเข้าถึงหน่วยความจำ"] --> tlb{"TLB มีคำแปลหรือไม่?"}
tlb -->|มี| perm{"การเข้าถึงตรงกับการป้องกันหรือไม่?"}
perm -->|ตรง| go["ทำงานต่อด้วยคำแปลนั้น"]
perm -->|ละเมิดการป้องกัน| entry["ไปจุดเข้า page fault"]
tlb -->|ไม่มี| walk["เดินตารางเพจ"]
walk --> valid{"PTE ใช้ได้และการป้องกันตรงด้วย?"}
valid -->|ใช่| register["ลงทะเบียนใน TLB แล้วทำงานต่อ(ไม่มี fault)"]
valid -->|ใช้ไม่ได้หรือละเมิดการป้องกัน| entry
ภาพ 3: TLB miss แก้ได้ด้วยการเดินตารางเพจ การควบคุมไปที่ page fault เมื่อคำแปลใช้ไม่ได้หรือมีการละเมิดการป้องกัน และการละเมิดการป้องกันเกิดแม้ตอน TLB hit
5.1. PTE ที่ใช้ไม่ได้ไม่ใช่แค่ช่องว่าง
แม้ PTE ที่ใช้ไม่ได้ก็ไม่ว่าง จากสถานะซอฟต์แวร์ของ PTE ที่ใช้ไม่ได้ Windows แยกกรณีอย่างต่อไปนี้
- เพจ demand-zero ที่ยังไม่เคยเกิดจริง
- เพจ Transition ที่ยังอยู่ใน RAM
- เพจร่วมที่อ้าง Prototype PTE
- เพจส่วนตัวที่เก็บในไฟล์เพจ
- การละเมิดการป้องกันหรือบริเวณที่ใช้ไม่ได้
งานของ CPU คือตัดสินว่า «นี่ไม่ใช่คำแปลที่ใช้ได้ธรรมดา» แล้วส่งให้เคอร์เนลเท่านั้น ตัวจัดการหน่วยความจำเป็นผู้ให้ความหมายต่อจากนั้น
6. page fault จากต้นจนจบ
ให้ตามการเขียนครั้งแรกสู่เพจส่วนตัวที่ Commit แล้วในหกขั้น
- CPU พยายามเขียน
มันตรวจ TLB และตารางเพจ แต่ PTE เป้าหมายไม่มี PFN ที่ใช้ได้ - CPU ยก page fault
มันส่งที่อยู่เสมือนที่ fault ชนิดอ่าน/เขียน/รัน ผู้ใช้/เคอร์เนล และว่าปัญหาคือคำแปลขาดหรือการละเมิดการป้องกันให้เคอร์เนล - ตัวจัดการหน่วยความจำตรวจ VAD และ PTE
มันตัดสินว่าเพจ Commit แล้วหรือไม่ การป้องกันตรงหรือไม่ และข้อใดใน demand-zero, Transition, ร่วม, page-in, CoW หรือข้อยกเว้นใช้ได้ - หากเป็น demand-zero จะได้เพจกายภาพที่ล้างศูนย์
เพจที่เพิ่งส่งต้องเป็นศูนย์เพื่อไม่ให้ข้อมูลของโพรเซสอื่นรั่ว - ข้อมูลจัดการ PTE และ PFN ถูกอัปเดต
PFN และการป้องกันถูกตั้งใน PTE เพจกายภาพถูกทำให้ Active และถูกเพิ่มเข้า Working Set ของโพรเซส - คำสั่งที่ล้มเหลวถูกรันซ้ำ
เพราะ fault แก้ได้ตามปกติ จึงไม่มีข้อยกเว้นโหมดผู้ใช้ถูกส่ง และแอปทำงานต่อด้วยการกำหนดค่าตามปกติ
เหตุการณ์ ETW ของ page fault ยังบันทึก Transition, Demand Zero, Copy-on-Write, Guard Page, Hard Page Fault และ Access Violation เป็นชนิดแยก4
ดังนั้น page fault จึงไม่ใช่คำที่หมาย «ผิดปกติ» ตั้งแต่ต้น มันคือจุดเข้าร่วมเพื่อขอให้ระบบปฏิบัติการตัดสินเมื่อ CPU แปลบนเส้นทางธรรมดาไม่ได้
flowchart LR
accTitle: การแยกทางของการแก้ page fault
accDescr: ตัวจัดการหน่วยความจำตัดสิน VAD, PTE, แอตทริบิวต์การป้องกัน และชนิดการเข้าถึง แล้วส่งไป demand-zero การเชื่อมเพจที่ยังอยู่ใน RAM อีกครั้ง hard fault จาก backing store copy-on-write การแจ้งเพจเฝ้า หรือข้อยกเว้น
faultIn["เกิด page fault"] --> judge["ตัดสิน VAD, PTE, การป้องกัน, ชนิด"]
judge -->|เข้าถึงครั้งแรก| dz["Demand-zero(อ่อน)"]
judge -->|ยังอยู่ใน RAM| soft["เชื่อมใหม่จาก Standby(อ่อน)"]
judge -->|ต้องอ่านดิสก์| hard["Hard fault(I/O ดิสก์)"]
judge -->|เขียน CoW| cow["คัดลอกแล้วสลับ PTE"]
judge -->|เพจเฝ้า| guard["ล้างเฝ้าแล้วแจ้ง"]
judge -->|แก้ไม่ได้| av["ข้อยกเว้น(0xC0000005 ฯลฯ)"]
ภาพ 4: fault ที่เข้าจุดเข้าเดียวกันแยกเป็นผลลัพธ์หกชนิดตามการตัดสิน รายละเอียดเพจเฝ้าอยู่ในข้อ 9
7. Demand-zero — soft fault ที่ไม่อ่านดิสก์
Demand-zero คือ soft fault ตัวแทนที่เกิดเมื่อเพจส่วนตัวที่ Commit แล้วถูกแตะครั้งแรก เอกสาร Working Set ของ Microsoft ก็ยก «โพรเซสอ้างเพจเสมือนที่จัดสรรแล้วเป็นครั้งแรก» เป็นตัวอย่าง soft fault5
Demand-zero มีลักษณะดังนี้
- ไม่ต้องอ่านข้อมูลต้นฉบับจากดิสก์
- เนื้อหาเริ่มต้นเป็นศูนย์
- เพจกายภาพที่ใช้ได้ถูกผูก
- Working Set และ Page Fault Count สะสมเพิ่มขึ้น
- การจัดการนี้เพียงอย่างเดียวไม่เพิ่ม
Memory\\Pages Input/sec
นั่นคือเหตุผลที่การพุ่งของ Page Faults/sec ทันทีหลังสตาร์ตเองไม่ได้หมายความว่าที่เก็บคือคอขวด
การแลกของการจัดสรรแบบขี้เกียจก็ควรจัดให้ชัด หากคุณ Commit 256MiB แล้วใช้จริงเพียง 8MiB การปล่อย 248MiB ที่เหลืออยู่นอก RAM ก็สมเหตุสมผล แลกกับการเข้าถึงครั้งแรกมีต้นทุนการจัดการ fault สำหรับงานที่ไวต่อความหน่วง มีการออกแบบที่แตะทุกเพจก่อนเริ่มเพื่อ prefault แต่เป็นการแลกที่เพิ่มการพักใน RAM ล่วงหน้า
8. Soft fault และ hard fault
8.1. Soft fault
Soft fault คือ fault ที่แก้ได้โดยไม่มี I/O อ่านสู่ backing store ตัวอย่างตัวแทนมีดังนี้
- Demand-zero
- เชื่อมเพจที่ยังอยู่บน Standby/Transition อีกครั้ง
- เชื่อมเพจร่วมที่อยู่ใน Working Set ของโพรเซสอื่น
- เชื่อมเพจที่ prefetch แล้ว
- Copy-on-Write ที่เพจต้นยังพักอยู่
ยังมีต้นทุน CPU สำหรับการเปลี่ยนเคอร์เนล ล็อก การอัปเดต PTE/PFN ความสอดคล้องของ TLB และอื่น ๆ แต่ไม่มีการรอที่เก็บ5
8.2. Hard fault
ในทางกลับกัน เมื่อเพจที่ต้องการไม่อยู่ที่ใดใน RAM และต้องอ่านจาก backing store นั่นคือ hard fault แหล่งอ่านไม่ใช่แค่ไฟล์เพจ
- เพจส่วนตัวที่ถูกเขียนออกไปไฟล์เพจ
- ไฟล์ที่แมปหน่วยความจำ
- อิมเมจ EXE หรือ DLL
- ไฟล์ข้อมูลที่แคชไฟล์อ้าง
เหตุการณ์ ETW HardFault มี FileObject, ReadOffset และ ByteCount จึงตามแหล่งอ่านจริงได้6
ดังนั้น Hard Fault = การอ่าน pagefile.sys จึงไม่จริง
เมื่อต้องอ่าน backing store คำขอเข้าสู่สแต็ก I/O ของ Windows กระแส IRP และการออก/เสร็จถูกกล่าวใน «The Depths of Windows I/O (Part 1)» และจุดต่อกับแคชไฟล์ใน «The Depths of Windows I/O (Part 4)» หากเพจอยู่ใน RAM ตัวจัดการหน่วยความจำกลับได้เอง หากไม่อยู่ มันออก I/O แล้วรอเธรดที่ fault จนเสร็จ
9. Fault ที่แก้ไม่ได้กลายเป็นข้อยกเว้น
Fault ที่หลังตรวจ VAD และ PTE แล้วแก้เป็นการจัดสรรที่ชอบธรรม page-in หรือ CoW ไม่ได้ จะถูกส่งสู่โหมดผู้ใช้เป็นข้อยกเว้น
กรณีตัวแทนคือ STATUS_ACCESS_VIOLATION รหัสข้อยกเว้น 0xC0000005 มันเกิดตอนอ่าน เขียน หรือรันที่อยู่ที่ใช้ไม่ได้ พารามิเตอร์ข้อยกเว้นแรกบอกชนิดการเข้าถึง และที่สองบอกที่อยู่ที่ละเมิด7
รูปแบบทั่วไปมีดังนี้
- อ่าน NULL ที่อยู่ที่ปล่อยแล้ว หรือที่อยู่นอกอาร์เรย์
- เขียนลงเพจอ่านอย่างเดียว
- รันคำสั่งจากเพจที่ DEP/NX ทำให้รันไม่ได้
- แตะช่วงที่จองแต่ยังไม่ Commit
PAGE_GUARD มีความหมายต่างเล็กน้อย มันคือการแจ้งครั้งเดียวของการเข้าถึง: มันยก STATUS_GUARD_PAGE_VIOLATION และใช้กับสิ่งอย่างการโตของสแต็ก8
การจัดสรรแบบขี้เกียจปกติ, page-in, CoW, การแจ้งเฝ้า และการละเมิดการเข้าถึงสุดท้าย จากมุม CPU รวมที่จุดเข้า page fault เดียวกัน สิ่งที่ตัดสินผลคือการรวมของ VAD, PTE, แอตทริบิวต์การป้องกัน และชนิดการเข้าถึง
10. ดูด้วยตัวเอง
คุณสังเกตกระแสจนถึงจุดนี้บนเครื่องตัวเองได้ โปรแกรม C++ ต่อไปนี้ Reserve 256MiB, Commit เขียนหนึ่งไบต์ต่อเพจ และสุดท้าย 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");
}
จาก x64 Native Tools Command Prompt ของ Visual Studio คุณสร้างด้วยคำสั่งต่อไปนี้ได้
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 ลด |
ตัวเลขจริงแปรตามรันไทม์ ผลิตภัณฑ์ความปลอดภัย ความกดดันหน่วยความจำ และเวลาที่คุณสังเกต ดู ตัวเลขขยับไปทางไหนระหว่างขั้น ไม่ใช่ว่าออกมาพอดี 256MiB หรือไม่
10.2. แยกอ่อนและแข็งใน PerfMon
ใน PerfMon วางเคาน์เตอร์ต่อไปนี้บนแกนเวลาเดียวกัน
Process(<target>)\\Page Faults/secMemory\\Pages Input/secMemory\\Page Reads/secMemory\\Available MBytesProcess(<target>)\\Working Set - PrivateProcess(<target>)\\Private Bytes
Process\\Page Faults/sec รวมทั้ง soft fault และ hard fault ส่วน Memory\\Pages Input/sec คือจำนวนเพจที่อ่านจากดิสก์เพื่อแก้ hard fault10
ในขั้น Touch ของโปรแกรมนี้ Page Faults/sec ควรกระโดด ในขณะที่ Pages Input/sec ไม่ควรขึ้นมาก เพจที่ Commit ใหม่ถูกทำให้เป็นจริงด้วย demand-zero จึงไม่ต้องอ่านข้อมูลต้นฉบับจากดิสก์
เมื่อหลายโพรเซสใช้ชื่อเดียวกัน หมายเลข PerfMon อย่าง process#1 อาจเปลี่ยนข้ามการรีสตาร์ต ตรวจไขว้กับเคาน์เตอร์ที่แสดง PID หรือระบุด้วย PID ผ่าน Process V2 หรือ ETW/WPA
11. สามการอ่านผิดที่ควรเลี่ยงในทางปฏิบัติ
11.1. «Commit ขึ้น ดังนั้นเป็นการรั่ว RAM»
Commit คือปริมาณเนื้อหาที่สัญญาจะเก็บ เพจที่ยังไม่แตะอาจไม่พักใน RAM ในการตัดสินการรั่ว ดูอนุกรมเวลาของ Private Bytes การแยกการจัดสรร และว่าตัวเลขกลับสู่เส้นฐานหลังประมวลผลจบหรือไม่
11.2. «Page Faults/sec สูง ดังนั้นดิสก์ช้า»
Soft fault ไม่มี I/O ดิสก์ แยก Page Faults/sec, Pages Input/sec และการรอที่เก็บ และหากจำเป็นตามไฟล์ต้นทางกับสแต็กด้วยเหตุการณ์ ETW HardFault
11.3. «การเท Working Set จะแก้การรั่ว»
การเอาเพจออกจาก Working Set ไม่ปล่อย Commit หรือความเป็นเจ้าของ เพจไป Standby หรือ Modified แล้ว fault กลับเข้ามาทีหลัง การแก้การรั่วต้องให้ตัวจัดสรรทำ VirtualFree การปล่อยฮีป การทำลายออบเจ็กต์ และอื่น ๆ
เพจกายภาพที่ถูกเอาออกไปไหนคือสิ่งที่เราตามในตอนที่ 2
12. สรุป
MEM_RESERVEสำรองช่วงที่อยู่เสมือนแต่ไม่กำหนดบริเวณกายภาพใน RAM หรือไฟล์เพจ1MEM_COMMITใช้ Commit และรับประกันว่าเนื้อหาเก็บในอนาคตได้ แต่เพจกายภาพทั่วไปยังไม่ถูกกำหนดจนกว่าจะเข้าถึงครั้งแรก12- VAD คือบัญชีช่วง PTE คือบัญชีเพจเสมือน และฐานข้อมูล PFN คือบัญชีเพจกายภาพ
- TLB miss ไม่ใช่ page fault หาก PTE ใช้ได้ การเดินตารางเพจอย่างเดียวก็แก้ได้
- Demand-zero การคืน Transition และการเชื่อมเพจร่วมคือ soft fault ที่แก้ได้โดยไม่มี I/O ดิสก์5
- หากต้องอ่านจากไฟล์เพจ, DLL, EXE หรือไฟล์ที่แมป นั่นคือ hard fault6
- หากการตรวจ VAD, PTE และแอตทริบิวต์การป้องกันแก้ fault ไม่ได้ คุณจะได้ข้อยกเว้นอย่าง
0xC00000057 - สำหรับการตัดสินประสิทธิภาพ อย่าดูแค่
Page Faults/secดูPages Input/sec, Available, Working Set, Private Bytes และการรอที่เก็บบนแกนเวลาเดียวกัน
ต่อในตอนที่ 2 «ชีวิตของเพจกายภาพ: ห้าบัญชีและความจริงของไฟล์เพจ»
หลังจากคำสัญญา Commit กลายเป็นเพจกายภาพ เราตามว่าเพจนั้นไปไหนเมื่อออกจาก Working Set จากฐานข้อมูล PFN และรายการเพจ
บทความที่เกี่ยวข้อง
- What Does Windows’ “Memory Usage” Actually Mean? — Correctly Reading Working Set, Private Bytes, Commit, and the Page File
- The Depths of Windows I/O (Part 1) — Every Read and Write Becomes an IRP: The Big Picture of the I/O System
- The Depths of Windows I/O (Part 4) — Cache Manager: When Does Your WriteFile Actually Reach the Disk?
- Reading Crash Dumps with WinDbg + SOS — A Practical Guide to Analysis After Collection
- An Introduction to Collecting Windows Crash Dumps - WER/ProcDump/WinDbg
ด้านให้คำปรึกษาที่เกี่ยวข้อง
KomuraSoft LLC จัดการการสอบสวนการใช้หน่วยความจำของแอปพลิเคชัน Windows การละเมิดการเข้าถึง ความล่าช้าตอนสตาร์ต การเพจ และข้อบกพร่องของโค้ดเนทีฟ
- การพัฒนาแอปพลิเคชัน Windows
- การตรวจสอบบั๊กและวิเคราะห์หาสาเหตุที่แท้จริง
- การใช้ประโยชน์และโยกย้ายสินทรัพย์ซอฟต์แวร์เดิม
- ติดต่อเรา
ลิงก์อ้างอิง
-
Microsoft Learn, VirtualAlloc function. เกี่ยวกับ
MEM_RESERVEที่จองช่วงที่อยู่เสมือนโดยไม่กำหนดที่เก็บกายภาพ;MEM_COMMITที่บันทึกภาระ commit (commit charge) กับหน่วยความจำและไฟล์เพจของระบบ; เนื้อหาเริ่มต้นของเพจที่ Commit เป็นศูนย์; และเพจกายภาพจริงยังไม่ถูกกำหนดจนกว่าจะถูกเข้าถึง ↩ ↩2 ↩3 ↩4 ↩5 ↩6 -
Microsoft Learn, PERFORMANCE_INFORMATION structure. เกี่ยวกับ
CommitTotalที่เป็นจำนวนเพจ Commit ปัจจุบันของระบบ และCommitLimitที่เป็นขีดบนที่ Commit ได้โดยไม่ขยายไฟล์เพจ ↩ ↩2 -
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. เกี่ยวกับ soft fault ที่แก้ได้โดยไม่เข้าถึง backing store และเกิดจาก Working Set ของโพรเซสอื่น, Transition, demand-zero ตอนอ้างครั้งแรก และอื่น ๆ ↩ ↩2 ↩3
-
Microsoft Learn, PageFault_HardFault class. เกี่ยวกับเหตุการณ์ HardFault ที่มี FileObject, ReadOffset, ByteCount, VirtualAddress และรหัสเธรด จึงตามแหล่งอ่านได้ ↩ ↩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ที่เป็นจำนวนเพจที่อ่านจากดิสก์เพื่อแก้ hard page fault ↩
บทความที่เกี่ยวข้อง
บทความล่าสุดที่มีแท็กเดียวกัน เพื่อเจาะลึกหัวข้อใกล้เคียง
ความลึกของหน่วยความจำ Windows (ตอนที่ 2) — ชีวิตของเพจทางกายภาพ: ห้าบัญชีและเรื่องจริงของไฟล์เพจ
บทความนี้เชื่อมฐานข้อมูล PFN, Standby, Modified, การบีบอัดหน่วยความจำ และไฟล์เพจ เพื่ออธิบายว่าเพจทางกายภาพไปไหนหลังจากออกจาก Working Set.
ความลึกของหน่วยความจำ Windows (ตอนที่ 3) — ออบเจ็กต์เซกชันและคัดลอกเมื่อเขียน: DLL และการแมปไฟล์คืออะไรจริง ๆ
บทความนี้เชื่อมออบเจ็กต์เซกชัน การแมปอิมเมจและข้อมูล แคชร่วม และคัดลอกเมื่อเขียน เพื่ออธิบายว่า DLL กับหน่วยความจำร่วมใช้เพจกายภาพร่วมกัน...
เชิงลึกของการจำลองเสมือนบน Windows (ตอนที่ 3) — เครื่องเสมือนที่บูตในไม่กี่วินาที: ทำไม WSL2, Windows Sandbox และคอนเทนเนอร์จึงเบา
ทำไม WSL2 และ Windows Sandbox จึงเริ่มในไม่กี่วินาทีและรู้สึกเบา? บทความนี้อธิบายกลไก ตั้งแต่ dynamic base image และ direct map ไปจนถึงกา...
เชิงลึกของการจำลองเสมือนบน Windows (ตอนที่ 2) — หน่วยความจำที่แม้เคอร์เนลก็มองไม่เห็น: กลไกของ VBS, HVCI และ Credential Guard
เมื่อติดตั้งใหม่บนฮาร์ดแวร์ที่รองรับ VBS จะเปิดตามค่าเริ่มต้น และใช้ hypervisor กับ SLAT สร้างการแยกที่แข็งกว่าเคอร์เนล บทความนี้อธิบายโค...
เชิงลึกของการจำลองเสมือนบน Windows (ตอนที่ 1) — Windows ของคุณรันอยู่ที่ไหนจริง ๆ? Hypervisor และพาร์ติชัน
เมื่อเปิด Hyper-V แล้ว Windows ฝั่งโฮสต์เองก็รันบน hypervisor ในฐานะ root partition บทความนี้อธิบายรากฐานของการจำลองเสมือนผ่านบทบาทของ VT...
หัวข้อที่เกี่ยวข้อง
หน้าเหล่านี้วางหัวข้อไว้ในบริบทที่กว้างขึ้นของบริการและการตัดสินใจ
หัวข้อเทคนิคของ Windows
ประตูสู่การพัฒนา Windows การตรวจสอบบั๊ก และการใช้ประโยชน์จากสินทรัพย์เดิม
บริการที่เกี่ยวข้องกับหัวข้อนี้
บทความนี้เกี่ยวข้องโดยตรงกับบริการต่อไปนี้
พัฒนาแอป Windows
แอปธุรกิจ การเชื่อมต่ออุปกรณ์ และเครื่องมือสื่อสาร ตั้งแต่ความต้องการจนถึงการพัฒนา
คำถามที่พบบ่อย
คำถามที่มักพบในการปรึกษาเกี่ยวกับหัวข้อของบทความนี้
- การส่ง MEM_COMMIT ให้ VirtualAlloc จัดสรร RAM ทันทีหรือไม่?
- ในหน่วยความจำส่วนตัวทั่วไป Commit ใช้หัวห้อง commit ของระบบ แต่เพจกายภาพที่สอดคล้องยังไม่ถูกกำหนดจนกว่าจะเข้าถึงครั้งแรก เพจที่ถูกแตะครั้งแรกด้วยการเขียนได้รับเพจกายภาพระหว่างการจัดการ demand-zero fault
- page fault หมายความว่ามีสิ่งผิดปกติหรือปัญหาประสิทธิภาพหรือไม่?
- ไม่ Soft fault ที่ไม่มี I/O ดิสก์ เช่น demand-zero หรือการคืนเพจจาก Standby เป็นการทำงานปกติ สำหรับการตัดสินประสิทธิภาพ อย่าดูแค่ Page Faults/sec แต่ดู Pages Input/sec การรอที่เก็บ และ Available MBytes ด้วย
- TLB miss กับ page fault เป็นสิ่งเดียวกันหรือไม่?
- ต่างกัน แม้ TLB จะไม่มีคำแปล ถ้า PTE ของตารางเพจถูกต้อง CPU ก็แค่เดินตารางแล้วลงทะเบียนคำแปลใหม่ มันไปที่จุดเข้า page fault เมื่อ PTE ไม่ถูกต้องหรือมีการละเมิดการป้องกัน
- ถ้าช่วงที่อยู่ใน VAD จะไม่เกิดการละเมิดการเข้าถึงใช่ไหม?
- ไม่จำเป็น นอกจากมี VAD หรือไม่ ตัวจัดการหน่วยความจำยังประเมิน Reserve กับ Commit การป้องกันอ่าน/เขียน/รัน เพจเฝ้า สถานะ PTE และอื่น ๆ หากแก้ fault ไม่ได้ คุณจะได้ข้อยกเว้นอย่าง 0xC0000005
- Page Faults/sec สูงหมายความว่าระบบขาด RAM หรือไม่?
- จากอย่างเดียวนั้นบอกไม่ได้ Page Faults/sec รวม soft fault จำนวนมากด้วย ต้องเทียบบนแกนเวลาเดียวกันกับ Memory\Pages Input/sec, Memory\Page Reads/sec, Available MBytes และเวลารอดิสก์