Page Fault ב-Windows (חלק 1): מתי כתובת וירטואלית מקבלת RAM פיזי
· עודכן בתאריך: · Go Komura · Windows, ניהול זיכרון, VirtualAlloc, Page Fault, VAD, ניטור ביצועים
היסטוריית עדכונים (גרסה ראשונה, פורסמה בתאריך 20 Aug 2026)
- פרסום ראשון
לצטט את המאמר הזה(DOI: 10.5281/zenodo.22176076)
מאמר זה מאוחסן בארכיון Zenodo. להלן גם ה-DOI שתמיד מפנה לגרסה האחרונה וגם ה-DOI המקובע לגרסה שאתם קוראים.
Go Komura (2026). Page Fault ב-Windows (חלק 1): מתי כתובת וירטואלית מקבלת RAM פיזי. KomuraSoft LLC. https://doi.org/10.5281/zenodo.22176076 https://comcomponent.com/he/blog/windows-memory-internals-page-fault/
- DOI (הגרסה האחרונה)
- 10.5281/zenodo.22176076
- DOI (הגרסה הזו)
- 10.5281/zenodo.22176077
כשמעבירים MEM_COMMIT ל-VirtualAlloc, Commit עולה באותו רגע. Working Set לא בהכרח גדל באותו סכום. אז איפה הזיכרון שחשבתם שהקציתם?
התשובה: לרוב הדפים עדיין אין RAM פיזי מתאים. Windows דוחה הקצאת דף פיזי עד שהאפליקציה באמת נוגעת בדף. הגישה הראשונה גורמת למעבד להרים page fault; אז memory manager בודק VAD, PTE, protection ו-backing store, וקושר RAM דף-דף אם צריך.1
המאמר עוקב אחרי הדרך שעובר «הבית הראשון שנגעתם בו» עד שהוא מגיע ל-RAM. אם תרצו קודם את משמעות המספרים עצמם — Working Set, Commit — ראו את מאמר הפתיחה «מה באמת אומר “שימוש בזיכרון” של Windows — Working Set, Private Bytes, Commit ו-pagefile». הסדרה הזו לא מגדירה מחדש את המונחים משם; היא חופרת מהמנגנון ב«למה המספר יוצא ככה».
«מעמקי הזיכרון של Windows» — שלושת החלקים
- חלק 1 (המאמר הזה): כתובות וירטואליות ו-page fault
מתי אזור שהוקצה ב-VirtualAllocמקבל RAM פיזי. - חלק 2: מחזור החיים של דף פיזי
לאן הולך דף שיוצא מ-Working Set: Modified, Standby, Free, Zeroed. - חלק 3: section object ו-copy-on-write
למה DLL, file mapping ו-shared memory יכולים לחלוק דפים פיזיים.
השאלה שחלק 1 עונה עליה היא אחת.
באיזה רגע כתובת וירטואלית שכבר ב-Commit הופכת ל-RAM פיזי?
קהל היעד הוא מפתחים ומפעילים שרוצים להבין מהמנגנון את שימוש הזיכרון של אפליקציות Windows, page fault מיד אחרי ההפעלה, 0xC0000005, ואת המספרים ב-VMMap וב-PerfMon. הסביבה היא Windows 10/11 או Windows Server עדכני. הרקע הנדרש הוא pointers ויסודות VirtualAlloc; אין צורך בפריסת סיביות של page table או ב-kernel debugger. רמת הקושי בינונית. אנחנו משתמשים בשמות מבנים פנימיים, אבל לא מניחים layout לא מתועד שתלוי ב-build מסוים של Windows.
1. קודם המסקנה
זרימת הזיכרון הפרטי הרגילה, בשורה אחת:
Reserve תופס טווח כתובות וירטואליות, Commit גובה commit charge מול Commit Limit כדי להבטיח מקום עתידי לתוכן, וה-page fault בגישה הראשונה מקצה את הדף הפיזי.
כלומר MEM_COMMIT אינו פקודה «הקצה RAM עכשיו». גם תיעוד Microsoft של VirtualAlloc מבטיח שתוכן ההתחלה של דף ב-Commit הוא אפס, ומסביר שהדף הפיזי בפועל לא מוקצה עד שניגשים לכתובת הווירטואלית.1
גם לא מדויק לטעון ש-«Reserve/Commit כותבים רק ב-VAD». בפועל Reserve יוצר בעיקר VAD שמייצג את טווח הכתובות והמאפיינים, ו-Commit מגדיל את Commit Total של המערכת ורושם את מצב ה-commit של הטווח. רמות ביניים של page table ו-PTE בודדות נבנות lazy כשצריך, והקישור הסופי ל-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, protection והתאמת section |
| page table / PTE | דף וירטואלי | מייצג את התרגום הנוכחי לדף פיזי, או מצב שעדיין לא מומש |
| PFN database | דף פיזי | עוקב אחרי בעלות, הפניות ומצב של כל דף RAM |
ה-VAD מחזיק מידע על טווח, ה-PTE על דף וירטואלי, ו-PFN database על דף פיזי. מטפל ה-page fault מצליב אותם כדי להחליט אם הגישה יכולה להמשיך.
flowchart TB
accTitle: שלושה מבני ניהול מכתובת וירטואלית ל-RAM פיזי
accDescr: כתובת וירטואלית מנוהלת על ידי VAD ברזולוציית טווח ועל ידי PTE ברזולוציית דף וירטואלי, ו-PFN database עוקב אחרי הדף הפיזי שהוא יעד התרגום של ה-PTE
va["כתובת וירטואלית"] --> vad["VAD (ניהול טווחים)"]
va --> pte["PTE (ניהול דף וירטואלי)"]
vad -.->|שיפוט Reserve/Commit והגנה| pte
pte -->|תרגום תקף| pfn["PFN database (ניהול דף פיזי)"]
pfn --> ram["דף RAM פיזי"]
איור 2: שלושה מבנים ברזולוציות שונות. טיפול ב-fault מצליב VAD ו-PTE ואז מעדכן את צד ה-PFN.
כוכבי המאמר הזה הם ה-VAD וה-PTE. את PFN database נראה מצד הדף הפיזי בחלק 2.
3. Reserve, Commit ו-Touch הם אירועים נפרדים
3.1. Reserve — לתפוס כתובת
קודם כל, שומרים טווח רציף של 256MiB כתובות וירטואליות.
void* base = VirtualAlloc(
nullptr,
256ull * 1024 * 1024,
MEM_RESERVE,
PAGE_NOACCESS);
מה שקרה כאן הוא רק שכתובת נתפסה במרחב הווירטואלי של התהליך, כדי שהקצאות אחרות לא יוכלו להשתמש בטווח. MEM_RESERVE לא מקצה אחסון פיזי ב-RAM או ב-pagefile.1
לתהליך 64-bit יש מרחב וירטואלי עצום, ולכן זה מעשי לעשות קודם 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;
המעבד מנסה לתרגם את הכתובת הווירטואלית לכתובת פיזית, אבל ל-PTE עדיין אין תרגום תקף לדף פיזי. כאן מתרחש page fault.
memory manager שמקבל את השליטה קובע שזו «גישה ראשונה לדף פרטי ב-Commit שניתן לכתיבה», משיג דף פיזי מאופס, קושר אותו ל-PTE ומוסיף אותו ל-Working Set. אחר כך הוא מריץ שוב את הוראת הכתיבה שנכשלה.
מהאפליקציה זה נראה כמו השמה בלבד, אבל בפנים השליטה נכנסת ל-kernel באמצע ההשמה, מוקצה דף פיזי, וההרצה חוזרת לאותה הוראה.
4. ה-VAD — ניהול הטווחים של המרחב הווירטואלי
VAD הוא קיצור של Virtual Address Descriptor. Windows מנהל את טווחי הכתובות שבשימוש של תהליך כעץ של VAD. בפקודת !vad של WinDbg אפשר לבדוק VPN התחלה וסיום, Commit, מאפייני protection, Private/Mapped, את ה-Control Area ועוד.3
מידע מייצג ש-VAD רושם:
- התחלה וסיום של טווח הכתובות
- סוג כמו Private, Mapped או Image
- מצב Reserve/Commit
- protection כמו קריאה, כתיבה, הרצה ו-copy-on-write
- התאמה לקובץ או ל-section
- מאפיינים מיוחדים כמו Guard page
הסיבה לניהול לפי טווח היא יעילות. 256MiB הם 65,536 דפים ב-4KiB. במקום לבנות מראש מבנה ניהול מלא לכל דף, זול יותר להחזיק «הטווח הרציף הזה הוא שמירה אחת» ב-VAD ולממש דפים כשהם נעשים נחוצים.
4.1. מציאת VAD אינה מבטיחה התאוששות
«אם זה ב-VAD ה-fault נפתר; אם לא, מקבלים access violation» הוא הסבר כניסה נוח, אבל הוא מפשט יותר מדי. גם כשנמצא VAD, גישה רגילה לא יכולה להמשיך במקרים כמו:
- Reserve בלבד, והדף היעד אינו ב-Commit
PAGE_NOACCESS- כתיבה לדף לקריאה בלבד
- הרצת הוראה מדף שאינו ניתן להרצה
- נגיעה ראשונה ב-Guard page
- נגיעה מחוץ לטווח התקף של section
להפך, גם אם ה-PTE אינה תקפה, אם מצב התוכנה של ה-VAD וה-PTE מראה גישה לגיטימית, אפשר לפתור כ-demand-zero, שחזור Transition, page-in או CoW. מדויק יותר: לשפוט יחד VAD, PTE, protection וסוג גישה.
5. page table ו-TLB
ה-pointer שהאפליקציה מחזיקה הוא כתובת וירטואלית. כדי שהמעבד ייגש ל-RAM הוא חייב לתרגם מספר דף וירטואלי למספר דף פיזי. טבלת התרגום ההיררכית הזו היא page table, והרשומה בקצה היא ה-PTE (Page Table Entry).
PTE תקפה מחזיקה מושגית PFN, הגנת read/write/execute, הרשאת user mode, Accessed/Dirty ומידע דומה. פריסת הסיביות בפועל תלויה במעבד ובגרסת Windows.
ללכת ב-page table בכל פעם יהיה איטי מדי, לכן המעבד שומר תרגומים אחרונים ב-TLB (Translation Lookaside Buffer). תרגום כתובות מתקדם בסדר הזה.
- אם ב-TLB יש תרגום והגישה תואמת את ה-protection, משתמשים בתוצאה הזו.
- אם ב-TLB אין תרגום, המעבד הולך ב-page table.
- אם יש PTE תקפה וה-protection גם תואם, היא נרשמת ב-TLB וההרצה נמשכת.
- אם אין תרגום תקף, או שיש הפרת protection, השליטה ממשיכה לנקודת הכניסה של page fault. בדיקת ה-protection מתבצעת גם כשהתרגום הגיע מה-TLB.
כפי שמראה הזרימה הזו, TLB miss ו-page fault הם דברים שונים. אם הבעיה היחידה היא שב-TLB אין תרגום וה-PTE תקפה, קורה רק page-table walk. להפך, גם אם ב-TLB יש תרגום, הפרת protection כמו כתיבה לדף לקריאה בלבד או הרצת הוראה בדף שאינו ניתן להרצה ממשיכה לכניסת page fault. לכן כתיבה לדף CoW יכולה להיכשל גם כשהתרגום כבר ב-cache.
flowchart TB
accTitle: זרימת תרגום כתובות ונקודת הכניסה של page fault
accDescr: גם אם ב-TLB יש תרגום, אי-התאמת protection ממשיכה לנקודת הכניסה של page fault. אם ב-TLB אין תרגום הולכים ב-page table; PTE תקפה שגם תואמת protection נרשמת ב-TLB וההרצה נמשכת
access["גישה לזיכרון"] --> tlb{"האם ב-TLB יש תרגום?"}
tlb -->|כן| perm{"האם הגישה תואמת את ה-protection?"}
perm -->|תואם| go["להמשיך עם התרגום הזה"]
perm -->|הפרת protection| entry["לנקודת הכניסה של page fault"]
tlb -->|לא| walk["page-table walk"]
walk --> valid{"PTE תקפה וה-protection גם תואם?"}
valid -->|כן| register["רישום ב-TLB והמשך (בלי fault)"]
valid -->|לא תקף או הפרת protection| entry
איור 3: TLB miss אפשר לפתור ב-page-table walk. השליטה ממשיכה ל-page fault כשהתרגום אינו תקף או שיש הפרת protection, והפרת protection מתרחשת גם ב-TLB hit.
5.1. PTE לא תקפה אינה סתם ריק
גם PTE לא תקפה אינה ריקה. ממצב התוכנה של PTE לא תקפה Windows מבדיל מקרים כמו:
- דף demand-zero שמעולם לא מומש
- דף Transition שנשאר ב-RAM
- דף משותף שמפנה ל-Prototype PTE
- דף פרטי שנשמר ב-pagefile
- הפרת protection או אזור לא תקף
עבודת המעבד היא רק להחליט «זה אינו תרגום תקף רגיל» ולמסור ל-kernel; memory manager מספק את המשמעות משם.
6. Page fault מההתחלה ועד הסוף
נעקוב אחרי כתיבה ראשונה לדף פרטי ב-Commit בשישה שלבים.
- המעבד מנסה לכתוב.
הוא בודק את ה-TLB ואת ה-page table, אבל ל-PTE היעד אין PFN תקף. - המעבד מרים page fault.
הוא מעביר ל-kernel את הכתובת הווירטואלית שבה אירע ה-fault, סוג read/write/execute, user/kernel, והאם הבעיה היא תרגום חסר או הפרת protection. - memory manager בודק את ה-VAD ואת ה-PTE.
הוא מחליט אם הדף ב-Commit, אם ה-protection תואם, ואיזה מבין demand-zero, Transition, משותף, page-in, CoW או exception חל. - אם זה demand-zero, משיגים דף פיזי מאופס.
דף שנמסר זה עתה חייב להיות אפס כדי שנתונים של תהליך אחר לא ידלפו. - מידע הניהול של PTE ו-PFN מתעדכן.
PFN וה-protection נקבעים ב-PTE, הדף הפיזי נעשה Active, והוא מתווסף ל-Working Set של התהליך. - ההוראה שנכשלה רצה שוב.
כי ה-fault נפתר כרגיל, לא נמסרת exception במצב משתמש, והאפליקציה ממשיכה את ההשמה כרגיל.
אירועי ETW של page fault גם רושמים Transition, Demand Zero, Copy-on-Write, Guard Page, Hard Page Fault ו-Access Violation כסוגים נפרדים.4
אז page fault אינו מילה שפירושה «חריג» מההתחלה. זו נקודת הכניסה המשותפת לבקש ממערכת ההפעלה להחליט כשהמעבד לא יכול היה לתרגם בנתיב הרגיל.
flowchart LR
accTitle: התפצלות פתרונות page fault
accDescr: memory manager שופט VAD, PTE, protection וסוג גישה ומפנה ל-demand-zero, חיבור מחדש של דף שעדיין ב-RAM, hard fault מ-backing store, copy-on-write, הודעת Guard page או exception
faultIn["מתרחש page fault"] --> judge["שיפוט VAD, PTE, protection, סוג"]
judge -->|גישה ראשונה| dz["Demand-zero (soft)"]
judge -->|עדיין ב-RAM| soft["חיבור מחדש מ-Standby (soft)"]
judge -->|נדרשת קריאת דיסק| hard["Hard fault (I/O לדיסק)"]
judge -->|כתיבת CoW| cow["העתקה והחלפת ה-PTE"]
judge -->|Guard page| guard["ניקוי ה-guard והודעה"]
judge -->|לא ניתן לפתור| av["exception (0xC0000005 וכו')"]
איור 4: faults שנכנסים מאותה נקודת כניסה מתפצלים לשישה סוגי תוצאה לפי השיפוט. פרטי Guard page בפרק 9.
7. Demand-zero — soft fault שאינו קורא דיסק
Demand-zero הוא ה-soft fault המייצג שמתרחש כשנוגעים לראשונה בדף פרטי ב-Commit. גם תיעוד Working Set של Microsoft מפרט «התהליך מפנה בפעם הראשונה לדף וירטואלי שהוקצה» כדוגמה ל-soft fault.5
ל-demand-zero המאפיינים הבאים.
- אין צורך לקרוא נתוני מקור מהדיסק
- תוכן ההתחלה הוא אפס
- נקשר דף פיזי זמין
- Working Set ו-Page Fault Count המצטבר גדלים
- הטיפול הזה לבדו אינו מגדיל
Memory\\Pages Input/sec
לכן קפיצה ב-Page Faults/sec מיד אחרי ההפעלה אינה אומרת לבדה שה-storage הוא צוואר הבקבוק.
שווה גם לסדר את הפשרה של הקצאה lazy. אם עשיתם Commit ל-256MiB ובאמת משתמשים רק ב-8MiB, להשאיר את 248MiB הנותרים מחוץ ל-RAM סביר. בתמורה, הגישה הראשונה נושאת את עלות טיפול ה-fault. לעבודה רגישה ל-latency יש עיצוב שנוגע בכל דף לפני ההתחלה כדי לעשות prefault, אבל זו פשרה שמגדילה מראש את כמות ה-RAM resident.
8. Soft fault ו-hard fault
8.1. Soft fault
soft fault הוא fault שאפשר לפתור בלי read I/O ל-backing store. דוגמאות מייצגות:
- Demand-zero
- חיבור מחדש של דף שנשאר ב-Standby/Transition
- חיבור דף משותף שנמצא ב-Working Set של תהליך אחר
- חיבור דף שנטען מראש
- copy-on-write שהדף המקורי שלו resident
עדיין יש עלות מעבד למעבר ל-kernel, נעילות, עדכוני PTE/PFN, עקביות TLB וכדומה, אבל אין המתנת storage.5
8.2. Hard fault
מצד שני, כשהדף הנחוץ אינו בשום מקום ב-RAM וחייבים לקרוא אותו מ-backing store, זו hard fault. מקור הקריאה אינו רק pagefile.
- דף פרטי שנכתב ל-pagefile
- קובץ שממופה לזיכרון
- image של EXE או DLL
- קובץ נתונים שמטמון הקבצים מפנה אליו
אירועי ETW HardFault כוללים FileObject, ReadOffset ו-ByteCount, כך שאפשר לעקוב אחרי מקור הקריאה בפועל.6
לכן Hard Fault = קריאה של pagefile.sys אינו נכון.
כשנדרשת קריאה מ-backing store, הבקשה נכנסת ל-I/O stack של Windows. זרימת IRP והנפקה/השלמה מכוסה ב«מעמקי I/O של Windows (חלק 1)», והצומת עם Cache Manager ב«מעמקי I/O של Windows (חלק 4)». אם הדף ב-RAM, memory manager יכול לחזור לבד; אם לא, הוא מנפיק I/O וממתין ל-thread שבו אירע ה-page fault עד ההשלמה.
9. Fault שלא ניתן לפתור הופך ל-exception
Fault שלאחר בדיקת VAD ו-PTE אי אפשר לפתור כהקצאה לגיטימית, page-in או CoW נמסר למצב משתמש כ-exception.
המקרה המייצג הוא STATUS_ACCESS_VIOLATION, קוד exception 0xC0000005. הוא מתרחש בקריאה, כתיבה או הרצה של כתובת לא תקפה; פרמטר ה-exception הראשון מציין את סוג הגישה והשני את הכתובת המפרה.7
תבניות טיפוסיות:
- קריאת NULL, כתובת ששוחררה או כתובת מחוץ למערך
- כתיבה לדף לקריאה בלבד
- הרצת הוראה מדף ש-DEP/NX הפך ללא ניתן להרצה
- נגיעה בטווח שמור שאינו ב-Commit
ל-PAGE_GUARD משמעות קצת אחרת. זו הודעה חד-פעמית על גישה: היא מרים STATUS_GUARD_PAGE_VIOLATION ומשמשת לדברים כמו גידול stack.8
הקצאה lazy רגילה, page-in, CoW, הודעת guard ו-access violation סופית מתכנסות, מנקודת המבט של המעבד, באותה נקודת כניסה של page fault. מה שמחליט את התוצאה הוא השילוב של VAD, PTE, protection וסוג גישה.
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");
}
משורת הפקודה Native Tools x64 של 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. הפרדת soft ו-hard ב-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 וגם hard. Memory\\Pages Input/sec, לעומת זאת, הוא מספר הדפים שנקראו מהדיסק כדי לפתור hard fault.10
בשלב 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 הוא הכמות המובטחת של תוכן לשמור; דפים שלא נגעו בהם אולי אינם resident ב-RAM. כדי לשפוט דליפה, הסתכלו על סדרת הזמן של Private Bytes, פירוק ההקצאות, והאם המספר חוזר לקו בסיס אחרי סיום העיבוד.
11.2. «Page Faults/sec גבוה, אז הדיסק איטי»
soft fault אינו כולל I/O לדיסק. הפרידו Page Faults/sec, Pages Input/sec והמתנת storage, ואם צריך עקבו אחרי קובץ המקור וה-stack עם אירועי ETW HardFault.
11.3. «ריקון ה-Working Set יתקן את הדליפה»
הוצאת דף מ-Working Set אינה משחררת Commit או בעלות. הדף עובר ל-Standby או Modified ואחר כך חוזר ב-fault. תיקון דליפה דורש שהמקצה יבצע VirtualFree, שחרור heap, הרס אובייקט וכדומה.
לאן הולך הדף הפיזי שהוסר הוא מה שאנחנו עוקבים אחריו בחלק 2.
12. סיכום
MEM_RESERVEתופס טווח כתובות וירטואליות אבל אינו מקצה אזור פיזי ב-RAM או ב-pagefile.1MEM_COMMITצורך Commit ומבטיח שאפשר יהיה לשמור את התוכן בעתיד, אבל דף פיזי רגיל אינו מוקצה עד הגישה הראשונה.12- ה-VAD מנהל טווחים, ה-PTE מנהל דף וירטואלי, ו-PFN database מנהל דף פיזי.
- TLB miss אינו page fault. אם ה-PTE תקפה, page-table walk לבדו פותר אותה.
- Demand-zero, שחזור Transition וחיבור דף משותף הם soft fault שאפשר לפתור בלי I/O לדיסק.5
- אם נדרשת קריאה מ-pagefile, DLL, EXE או קובץ ממופה, זו hard fault.6
- אם בדיקת VAD, PTE ו-protection אינה יכולה לפתור את ה-fault, מקבלים exception כמו
0xC0000005.7 - לשיפוט ביצועים אל תסתכלו על
Page Faults/secלבד; הסתכלו עלPages Input/sec, Available, Working Set, Private Bytes והמתנת storage על אותו ציר זמן.
המשך בחלק 2, «מחזור החיים של דף פיזי: חמש רשימות והאמת על pagefile».
אחרי שהבטחת ה-Commit הפכה לדף פיזי, נעקוב לאן הולך הדף הזה כשהוא יוצא מ-Working Set, מ-PFN database ומרשימות הדפים.
מאמרים קשורים
- מה באמת אומר “שימוש בזיכרון” של Windows — Working Set, Private Bytes, Commit ו-pagefile
- מעמקי I/O של Windows (חלק 1) — ארכיטקטורת I/O ו-IRP
- מעמקי I/O של Windows (חלק 4) — Cache Manager ו-WriteFile
- קריאת crash dump עם WinDbg + SOS
- מבוא לאיסוף crash dump ב-Windows — WER, ProcDump, WinDbg
תחומי ייעוץ קשורים
KomuraSoft LLC מטפלת בחקירות שימוש בזיכרון של יישומי Windows, access violation, עיכובי הפעלה, paging ופגמים בקוד native. זה חלק מ-Windows Custom Software Development.
קישורים
-
Microsoft Learn, VirtualAlloc function. על כך ש-
MEM_RESERVEשומר טווח כתובות וירטואליות בלי להקצות אחסון פיזי; ש-MEM_COMMITגובה commit charge מול הזיכרון ו-pagefile של המערכת; שתוכן ההתחלה של דף ב-Commit הוא אפס; ושהדף הפיזי בפועל אינו מוקצה עד שניגשים אליו. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 -
Microsoft Learn, PERFORMANCE_INFORMATION structure. על כך ש-
CommitTotalהוא מספר דפי ה-Commit הנוכחי של המערכת, ו-CommitLimitהוא הגבול העליון שאפשר לעשות לו Commit בלי להרחיב את ה-pagefile. ↩ ↩2 -
Microsoft Learn, !vad (WinDbg). על כך ש-
!vadמציג את עץ ה-VAD ומאפשר לבדוק VPN התחלה וסיום, Commit, Mapped/Private, מאפייני protection, את ה-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 ומזהה thread, כך שאפשר לעקוב אחרי מקור הקריאה. ↩ ↩2
-
Microsoft Learn, Access Violation C0000005. על כך ש-
0xC0000005מתרחש בקריאה, כתיבה או הרצה של כתובת זיכרון לא תקפה, ופרמטרי ה-exception מציינים את סוג הגישה ואת הכתובת המפרה. ↩ ↩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 fault. ↩
מאמרים קשורים
מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.
מנגנון הזיכרון של Windows (חלק 2) — מחזור החיים של דף פיזי: חמש רשימות והאמת על ה-pagefile
המאמר מחבר את ה-PFN database, Standby, Modified, דחיסת זיכרון וה-pagefile, ומסביר לאן עובר דף פיזי אחרי שהוא יוצא מ-Working Set.
Windows Memory Internals (חלק 3) — Section objects ו-Copy-on-Write: מה באמת קורה ב-DLL וב-file mapping
המאמר מחבר section objects, image mapping ו-data mapping, shared cache ו-Copy-on-Write, ומסביר איך DLL ו-shared memory חולקים physical pa...
איך קיצור דרך ב-Windows מוצא קובץ שהעברתם? — המיקום של הקובץ והזהות שלו הם שני דברים שונים
העברתם את הקובץ המקורי, ובכל זאת הקיצור פותח אותו. Windows מחפש את היעד לא רק לפי הנתיב השמור, אלא גם לפי מזהי מעקב ומאפיינים של הקובץ, ו...
האם עדיין צריך "הסרה בטוחה" של כונן USB? — מבט מ"הסרה מהירה" ומטמון הכתיבה
אפשר לשלוף כונן USB ברגע שההעתקה הסתיימה? ההבדל בין מטמון הכתיבה, "הסרה מהירה" ו"ביצועים גבוהים" מסביר מה תפקידה של ההסרה הבטוחה. המאמר כ...
למה האודיו נקטע כשה-CPU לא עסוק? — מבט דרך buffer ו-deadline
האודיו נקטע בזמן שהשימוש ב-CPU נמוך. ההסבר יוצא מה-buffer של הניגון ומה-deadline של המילוי מחדש, ממשיך למה buffer גדול יותר מוסיף השהיה, ...
נושאים קשורים
העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.
נושאים טכניים ב-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, המתנת storage ו-Available MBytes.
- האם TLB miss ו-page fault הם אותו דבר?
- לא. גם אם ב-TLB אין תרגום, PTE תקפה ב-page table אומרת שהמעבד רק הולך בטבלה ורושם את התרגום מחדש. הכניסה ל-page fault היא כשה-PTE לא תקפה, או כשיש הפרת protection.
- אם טווח הכתובות נמצא ב-VAD, לא יכולה להיות access violation?
- לא בהכרח. מעבר לקיום VAD, memory manager בודק Reserve מול Commit, הגנת read/write/execute, Guard page, מצב ה-PTE ועוד. אם אי אפשר לפתור את ה-fault, מגיעה exception כמו 0xC0000005.
- האם Page Faults/sec גבוה אומר שחסר RAM?
- אי אפשר לדעת מזה לבד. Page Faults/sec כולל גם הרבה soft fault. צריך להצליב על אותו ציר זמן עם Memory\Pages Input/sec, Memory\Page Reads/sec, Available MBytes וזמן המתנת דיסק.