מעמקי הזיכרון של Windows (חלק 1) — הרגע שבו כתובת וירטואלית הופכת ל-RAM פיזי: page fault מההתחלה ועד הסוף

· · Windows, ניהול זיכרון, VirtualAlloc, Page fault, VAD, ניטור ביצועים

העברת MEM_COMMIT ל-VirtualAlloc מגדילה את ה-Commit באותו רגע. ה-Working Set, לעומת זאת, לא בהכרח גדל באותו סכום. אז איפה הזיכרון שחשבתם שהקציתם?

התשובה היא שלרוב הדפים עדיין אין RAM פיזי מתאים. Windows דוחה הקצאת דף פיזי עד שהיישום באמת נוגע בדף. כשהגישה הראשונה גורמת למעבד להרים 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. חלק 1 (המאמר הזה): כתובות וירטואליות ו-page fault
    אנחנו עוקבים מתי אזור שהוקצה ב-VirtualAlloc מקבל RAM פיזי.
  2. חלק 2: חייו של דף פיזי
    אנחנו עוקבים איך דף שיוצא מ-Working Set נע דרך Modified, Standby, Free ו-Zeroed.
  3. חלק 3: אובייקטי מקטע והעתקה-בכתיבה
    אנחנו עוקבים למה DLL, מיפויי קבצים וזיכרון משותף יכולים לחלוק דפים פיזיים.

השאלה שחלק 1 עונה עליה היא אחת בלבד.

באיזה רגע כתובת וירטואלית שכבר ב-Commit הופכת ל-RAM פיזי?

הקוראים המיועדים הם מפתחים ומפעילים שרוצים להבין מהמנגנון את שימוש הזיכרון של אפליקציות Windows, page fault מיד אחרי ההפעלה, 0xC0000005 ואת המספרים ב-VMMap וב-PerfMon. הדרישות המוקדמות הן Windows 10/11 או Windows Server עדכני, והרקע הנדרש הוא מצביעים ויסודות VirtualAlloc; אין צורך בניסיון בפריסת סיביות של טבלאות דפים או במנפה ליבה. רמת הקושי בינונית. אנחנו משתמשים בשמות מבנים פנימיים, אבל לא מניחים פריסות לא מתועדות שתלויות ב-build מסוים של Windows.

1. קודם המסקנה

הזרימה הרגילה של זיכרון פרטי, בשורה אחת, היא זו.

Reserve מניח בצד טווח כתובות וירטואליות, Commit מחשב חיוב commit (commit charge) מול מגבלת ה-commit כדי להבטיח מקום עתידי לשמור את התוכן, וה-page fault בגישה הראשונה מקצה את הדף הפיזי.

כלומר, MEM_COMMIT אינו פקודה שאומרת «הקצה RAM עכשיו». גם תיעוד Microsoft של VirtualAlloc מבטיח שתוכן ההתחלה של דף ב-Commit הוא אפס, ומסביר שהדף הפיזי בפועל אינו מוקצה עד שניגשים לכתובת הווירטואלית.1

עם זאת, גם לא מדויק לטעון ש«Reserve/Commit כותבים רק ב-VAD». בפועל Reserve יוצר בעיקר VAD שמייצג את טווח הכתובות הווירטואליות ואת התכונות שלו, ו-Commit מגדיל את Commit Total של המערכת ורושם את מצב ה-commit של הטווח. רמות ביניים של טבלת דפים ו-PTE בודדות נבנות בעצלתיים כשצריך, והקישור הסופי ל-RAM פיזי קורה בדרך כלל בגישה הראשונה.

Commit אינו הבטחה ריקה; זו הבטחה ברמת המערכת שאפשר יהיה לשמור את התוכן בעתיד ב-RAM או ב-backing store מתאים. נקודת הכניסה שמממשת את ההבטחה הזאת דף-דף היא ה-page fault.

מה קורה ב-Reserve, ב-Commit ובגישה הראשונהMEM_RESERVE רושם את הטווח והתכונות ב-VAD, MEM_COMMIT צורך Commit Total כדי להבטיח אחסון, ו-page fault בגישה הראשונה מקצה דף פיזי ומוסיף אותו ל-Working Set1. MEM_RESERVE2. MEM_COMMIT3. גישה ראשונה(Touch)רישום הטווח והתכונות ב-VADצריכת Commit Total(עדיין אין דף פיזי)Page faultקישור דף פיזי מאופס ל-PTEהוספה ל-Working Set והרצה חוזרת של ההוראה

איור 1: Reserve, Commit ו-Touch הם אירועים נפרדים. RAM פיזי נקשר רק בשלב האחרון, הגישה הראשונה.

2. שלושה פנקסים שעוקבים אחרי דף וירטואלי

כדי להבין את הדרך מכתובת וירטואלית ל-RAM פיזי, צריך להבחין בשלושת סוגי הפנקסים ש-Windows מחזיק.

פנקס יחידה תפקיד
VAD טווח כתובות וירטואליות מנהל מה האזור, Reserve/Commit, הגנה והתאמת מקטע
טבלת דפים / PTE דף וירטואלי מייצג את התרגום הנוכחי לדף פיזי, או מצב שעדיין לא מומש
מסד PFN דף פיזי עוקב אחרי בעלות, הפניות ומצב של כל דף RAM

ה-VAD מחזיק מידע על טווח, ה-PTE על דף וירטואלי, ומסד ה-PFN על דף פיזי. מטפל ה-page fault מצליב אותם כדי להחליט אם הגישה יכולה להמשיך.

שלושה פנקסים מכתובת וירטואלית ל-RAM פיזיכתובת וירטואלית מנוהלת על ידי VAD ברזולוציית טווח ועל ידי PTE ברזולוציית דף וירטואלי, ומסד PFN עוקב אחרי הדף הפיזי שהוא יעד התרגום של ה-PTE ברזולוציית דף פיזישיפוט 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;

המעבד מנסה לתרגם את הכתובת הווירטואלית לכתובת פיזית, אבל ל-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
  • הגנה כמו קריאה, כתיבה, הרצה והעתקה-בכתיבה
  • התאמה לקובץ או למקטע
  • תכונות מיוחדות כמו דפי שמירה

הסיבה לניהול לפי טווח היא יעילות. 256MiB הם 65,536 דפים ב-4KiB. במקום לבנות מראש מבנה ניהול מלא לכל דף, פחות בזבוז להחזיק «הטווח הרציף הזה הוא שמירה אחת» ב-VAD ולממש דפים כשהם נעשים נחוצים.

4.1. מציאת VAD אינה מבטיחה התאוששות

«אם זה ב-VAD השגיאה נפתרת; אם לא, מקבלים הפרת גישה» הוא הסבר כניסה נוח, אבל הוא מפשט יותר מדי. גם כשנמצא VAD, גישה רגילה לא יכולה להמשיך במקרים כמו הבאים.

  • Reserve בלבד, והדף היעד אינו ב-Commit
  • PAGE_NOACCESS
  • כתיבה לדף לקריאה בלבד
  • הרצת הוראה מדף שאינו ניתן להרצה
  • נגיעה ראשונה בדף שמירה
  • נגיעה מחוץ לטווח התקף של מקטע

להפך, גם אם ה-PTE אינה תקפה, אם מצב התוכנה של ה-VAD וה-PTE מראה גישה לגיטימית, אפשר לפתור כ-demand-zero, שחזור Transition, הכנסת דף או CoW. מדויק יותר שהתשובה היא לשפוט יחד VAD, PTE, תכונות הגנה וסוג גישה.

5. טבלאות דפים ו-TLB

המצביע שיישום מחזיק הוא כתובת וירטואלית. כדי שהמעבד ייגש ל-RAM הוא חייב לתרגם מספר דף וירטואלי למספר דף פיזי. טבלת התרגום ההיררכית הזאת היא טבלת הדפים, והרשומה העלה היא ה-PTE (Page Table Entry).

PTE תקפה מחזיקה מושגית PFN, הגנת קריאה/כתיבה/הרצה, הרשאת מצב משתמש, Accessed/Dirty ומידע דומה. פריסת הסיביות בפועל תלויה במעבד ובגרסת Windows.

ללכת בטבלת הדפים בכל פעם יהיה איטי מדי, לכן המעבד שומר תרגומים אחרונים במטמון ב-TLB (Translation Lookaside Buffer). תרגום כתובות מתקדם בסדר הזה.

  1. אם ב-TLB יש תרגום והגישה תואמת את ההגנה הזאת, משתמשים בתוצאה הזאת.
  2. אם ב-TLB אין תרגום, המעבד הולך בטבלת הדפים.
  3. אם יש PTE תקפה וההגנה גם תואמת, היא נרשמת ב-TLB וההרצה נמשכת.
  4. אם אין תרגום תקף, או שיש הפרת הגנה, השליטה ממשיכה לנקודת הכניסה של page fault. בדיקת ההגנה מתבצעת גם כשהתרגום הגיע מה-TLB.

כפי שמראה הזרימה הזאת, החטאת TLB ו-page fault הם דברים שונים. אם הבעיה היחידה היא שב-TLB אין תרגום וה-PTE תקפה, קורה רק הליכה בטבלת דפים. להפך, גם אם ב-TLB יש תרגום, הפרת הגנה כמו כתיבה לדף לקריאה בלבד או הרצת הוראה בדף שאינו ניתן להרצה ממשיכה לנקודת הכניסה של page fault. לכן כתיבה לדף CoW יכולה להיכשל גם כשהתרגום כבר במטמון.

זרימת תרגום כתובות ונקודת הכניסה של page faultגם אם ב-TLB יש תרגום, אי-התאמת הגנה ממשיכה לנקודת הכניסה של page fault. אם ב-TLB אין תרגום הולכים בטבלת הדפים; PTE תקפה שגם תואמת הגנה נרשמת ב-TLB וההרצה נמשכת, ותרגום לא תקף או הפרת הגנה ממשיכים לנקודת הכניסה של page faultכןתואםהפרת הגנהלאכןלא תקף או הפרת הגנהגישה לזיכרוןהאם ב-TLB יש תרגום?האם הגישה תואמת את ההגנה?להמשיך עם התרגום הזהלנקודת הכניסה של page faultהליכה בטבלת דפיםPTE תקפה וההגנה גם תואמת?רישום ב-TLB והמשך(בלי שגיאה)

איור 3: החטאת TLB אפשר לפתור בהליכה בטבלת דפים. השליטה ממשיכה ל-page fault כשהתרגום אינו תקף או שיש הפרת הגנה, והפרת הגנה מתרחשת גם בפגיעת TLB.

5.1. PTE לא תקפה אינה סתם ריק

גם PTE לא תקפה אינה ריקה. ממצב התוכנה של PTE לא תקפה Windows מבדיל מקרים כמו הבאים.

  • דף demand-zero שמעולם לא מומש
  • דף Transition שנשאר ב-RAM
  • דף משותף שמפנה ל-Prototype PTE
  • דף פרטי שנשמר בקובץ הדפדוף
  • הפרת הגנה או אזור לא תקף

עבודת המעבד היא רק להחליט «זה אינו תרגום תקף רגיל» ולמסור לליבה; מנהל הזיכרון מספק את המשמעות משם.

6. Page fault מההתחלה ועד הסוף

נעקוב אחרי כתיבה ראשונה לדף פרטי ב-Commit בשישה שלבים.

  1. המעבד מנסה לכתוב.
    הוא בודק את ה-TLB ואת טבלת הדפים, אבל ל-PTE היעד אין PFN תקף.
  2. המעבד מרים page fault.
    הוא מעביר לליבה את הכתובת הווירטואלית שבה אירע ה-page fault, סוג קריאה/כתיבה/הרצה, משתמש/ליבה, והאם הבעיה היא תרגום חסר או הפרת הגנה.
  3. מנהל הזיכרון בודק את ה-VAD ואת ה-PTE.
    הוא מחליט אם הדף ב-Commit, אם ההגנה תואמת, ואיזה מבין demand-zero, Transition, משותף, הכנסת דף, CoW או חריגה חל.
  4. אם זה demand-zero, משיגים דף פיזי מאופס.
    דף שנמסר זה עתה חייב להיות אפס כדי שנתונים של תהליך אחר לא ידלפו.
  5. מידע הניהול של PTE ו-PFN מתעדכן.
    PFN וההגנה נקבעים ב-PTE, הדף הפיזי נעשה Active, והוא מתווסף ל-Working Set של התהליך.
  6. ההוראה שנכשלה רצה שוב.
    כי השגיאה נפתרה כרגיל, לא נמסרת חריגת מצב משתמש, והיישום ממשיך את ההשמה כרגיל.

אירועי ETW של page fault גם רושמים Transition, Demand Zero, Copy-on-Write, Guard Page, Hard Page Fault ו-Access Violation כסוגים נפרדים.4

אז page fault אינו מילה שפירושה «חריג» מההתחלה. זו נקודת הכניסה המשותפת לבקש ממערכת ההפעלה להחליט כשהמעבד לא יכול היה לתרגם בנתיב הרגיל.

התפצלות פתרונות page faultמנהל הזיכרון שופט VAD, PTE, תכונות הגנה וסוג גישה ומפנה ל-demand-zero, חיבור מחדש של דף שעדיין ב-RAM, שגיאה קשה מ-backing store, העתקה-בכתיבה, הודעת דף שמירה או חריגהגישה ראשונהעדיין ב-RAMנדרשת קריאת דיסקכתיבת CoWדף שמירהלא ניתן לפתורמתרחש page faultשיפוט VAD, PTE, הגנה, סוגDemand-zero(רך)חיבור מחדש מ-Standby(רך)שגיאה קשה(קלט/פלט דיסק)העתקה והחלפת ה-PTEניקוי השמירה והודעהחריגה(0xC0000005 וכו׳)

איור 4: שגיאות שנכנסות מאותה נקודת כניסה מתפצלות לשישה סוגי תוצאה לפי השיפוט. פרטי דפי שמירה בפרק 9.

7. Demand-zero — שגיאה רכה שאינה קוראת דיסק

Demand-zero היא השגיאה הרכה המייצגת שמתרחשת כשנוגעים לראשונה בדף פרטי ב-Commit. גם תיעוד Working Set של Microsoft מפרט «התהליך מפנה בפעם הראשונה לדף וירטואלי שהוקצה» כדוגמה לשגיאה רכה.5

ל-demand-zero המאפיינים הבאים.

  • אין צורך לקרוא נתוני מקור מהדיסק
  • תוכן ההתחלה הוא אפס
  • נקשר דף פיזי זמין
  • Working Set ו-Page Fault Count המצטבר גדלים
  • הטיפול הזה לבדו אינו מגדיל Memory\\Pages Input/sec

לכן קפיצה ב-Page Faults/sec מיד אחרי ההפעלה אינה אומרת לבדה שהאחסון הוא צוואר הבקבוק.

שווה גם לסדר את הפשרה של הקצאה עצלה. אם עשיתם Commit ל-256MiB ובאמת משתמשים רק ב-8MiB, להשאיר את 248MiB הנותרים מחוץ ל-RAM סביר. בתמורה, הגישה הראשונה נושאת את עלות טיפול השגיאה. לעבודה רגישה להשהיה יש עיצוב שנוגע בכל דף לפני ההתחלה כדי לעשות prefault, אבל זו פשרה שמגדילה מראש תושבות RAM.

8. שגיאות רכות ושגיאות קשות

8.1. שגיאות רכות

שגיאה רכה היא שגיאה שאפשר לפתור בלי קלט/פלט קריאה ל-backing store. דוגמאות מייצגות כוללות את הבאות.

  • Demand-zero
  • חיבור מחדש של דף שנשאר ב-Standby/Transition
  • חיבור דף משותף שנמצא ב-Working Set של תהליך אחר
  • חיבור דף שנטען מראש
  • העתקה-בכתיבה שהדף המקורי שלה תושב

עדיין יש עלות מעבד למעבר לליבה, נעילות, עדכוני PTE/PFN, עקביות TLB וכדומה, אבל אין המתנת אחסון.5

8.2. שגיאות קשות

מצד שני, כשהדף הנחוץ אינו בשום מקום ב-RAM וחייבים לקרוא אותו מ-backing store, זו שגיאה קשה. מקור הקריאה אינו רק קובץ הדפדוף.

  • דף פרטי שנכתב לקובץ הדפדוף
  • קובץ שממופה לזיכרון
  • תמונת EXE או DLL
  • קובץ נתונים שמטמון הקבצים מפנה אליו

אירועי ETW HardFault כוללים FileObject, ReadOffset ו-ByteCount, כך שאפשר לעקוב אחרי מקור הקריאה בפועל.6

לכן Hard Fault = קריאה של pagefile.sys אינו נכון.

כשנדרשת קריאה מ-backing store, הבקשה נכנסת למחסנית הקלט/פלט של Windows. זרימת IRP והנפקה/השלמה מכוסה ב«The Depths of Windows I/O (Part 1)», והצומת עם מטמון הקבצים ב«The Depths of Windows I/O (Part 4)». אם הדף ב-RAM מנהל הזיכרון יכול לחזור לבד; אם לא, הוא מנפיק קלט/פלט וממתין לשרשור שבו אירע ה-page fault עד ההשלמה.

9. שגיאה שלא ניתנת לפתרון הופכת לחריגה

שגיאה שלאחר בדיקת VAD ו-PTE אי אפשר לפתור כהקצאה לגיטימית, הכנסת דף או CoW נמסרת למצב משתמש כחריגה.

המקרה המייצג הוא STATUS_ACCESS_VIOLATION, קוד חריגה 0xC0000005. הוא מתרחש בקריאה, כתיבה או הרצה של כתובת לא תקפה; פרמטר החריגה הראשון מציין את סוג הגישה והשני את הכתובת המפרה.7

תבניות טיפוסיות כוללות את הבאות.

  • קריאת NULL, כתובת ששוחררה או כתובת מחוץ למערך
  • כתיבה לדף לקריאה בלבד
  • הרצת הוראה מדף ש-DEP/NX הפך ללא ניתן להרצה
  • נגיעה בטווח שמור שאינו ב-Commit

ל-PAGE_GUARD משמעות קצת אחרת. זו הודעה חד-פעמית על גישה: היא מרים STATUS_GUARD_PAGE_VIOLATION ומשמשת לדברים כמו גידול מחסנית.8

הקצאה עצלה רגילה, הכנסת דף, CoW, הודעת שמירה והפרת גישה סופית מתכנסות, מנקודת המבט של המעבד, באותה נקודת כניסה של 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");
}

משורת הפקודה 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. הפרדת רך וקשה ב-PerfMon

ב-PerfMon שימו את המונים הבאים על אותו ציר זמן.

  • Process(<target>)\\Page Faults/sec
  • Memory\\Pages Input/sec
  • Memory\\Page Reads/sec
  • Memory\\Available MBytes
  • Process(<target>)\\Working Set - Private
  • Process(<target>)\\Private Bytes

Process\\Page Faults/sec כולל גם שגיאות רכות וגם קשות. Memory\\Pages Input/sec, לעומת זאת, הוא מספר הדפים שנקראו מהדיסק כדי לפתור שגיאות קשות.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 הוא הכמות המובטחת של תוכן לשמור; דפים שלא נגעו בהם אולי אינם תושבים ב-RAM. כדי לשפוט דליפה, הסתכלו על סדרת הזמן של Private Bytes, פירוק ההקצאות, והאם המספר חוזר לקו בסיס אחרי סיום העיבוד.

11.2. «Page Faults/sec גבוה, אז הדיסק איטי»

שגיאות רכות אינן כוללות קלט/פלט דיסק. הפרידו 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 אינה page fault. אם ה-PTE תקפה, הליכה בטבלת דפים לבדה פותרת אותה.
  • Demand-zero, שחזור Transition וחיבור דף משותף הם שגיאות רכות שאפשר לפתור בלי קלט/פלט דיסק.5
  • אם נדרשת קריאה מקובץ הדפדוף, DLL, EXE או קובץ ממופה, זו שגיאה קשה.6
  • אם בדיקת VAD, PTE ותכונות הגנה אינה יכולה לפתור את השגיאה, מקבלים חריגה כמו 0xC0000005.7
  • לשיפוט ביצועים אל תסתכלו על Page Faults/sec לבד; הסתכלו על Pages Input/sec, Available, Working Set, Private Bytes והמתנת אחסון על אותו ציר זמן.

המשך בחלק 2, «חייו של דף פיזי: חמש רשימות והאמת על קובץ הדפדוף».

אחרי שהבטחת ה-Commit הפכה לדף פיזי, נעקוב לאן הולך הדף הזה כשהוא יוצא מ-Working Set, ממסד ה-PFN ומרשימות הדפים.

מאמרים קשורים

תחומי ייעוץ קשורים

KomuraSoft LLC מטפלת בחקירות שימוש בזיכרון של יישומי Windows, הפרות גישה, עיכובי הפעלה, דפדוף ופגמים בקוד מקורי.

קישורים

  1. Microsoft Learn, VirtualAlloc function. על כך ש-MEM_RESERVE שומר טווח כתובות וירטואליות בלי להקצות אחסון פיזי; ש-MEM_COMMIT מחשב חיוב commit (commit charge) מול הזיכרון וקובץ הדפדוף של המערכת; שתוכן ההתחלה של דף ב-Commit הוא אפס; ושהדף הפיזי בפועל אינו מוקצה עד שניגשים אליו.  2 3 4 5 6

  2. Microsoft Learn, PERFORMANCE_INFORMATION structure. על כך ש-CommitTotal הוא מספר דפי ה-Commit הנוכחי של המערכת, ו-CommitLimit הוא הגבול העליון שאפשר לעשות לו Commit בלי להרחיב את קובץ הדפדוף.  2

  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. על כך ששגיאה רכה ניתנת לפתרון בלי לגשת ל-backing store, ומתרחשת מ-Working Set של תהליך אחר, מ-Transition, מ-demand-zero בהפניה ראשונה וכדומה.  2 3

  6. Microsoft Learn, PageFault_HardFault class. על כך שאירוע HardFault כולל FileObject, ReadOffset, ByteCount, VirtualAddress ומזהה שרשור, כך שאפשר לעקוב אחרי מקור הקריאה.  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 הוא מספר הדפים שנקראו מהדיסק כדי לפתור page fault קשים. 

מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.

העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.

המאמר קשור ישירות לשירותים הבאים.

שאלות נפוצות

שאלות נפוצות בפניות בנושא המאמר.

האם העברת MEM_COMMIT ל-VirtualAlloc מקצה RAM באותו רגע?
בזיכרון פרטי רגיל, Commit צורך את מרווח ה-commit של המערכת, אך הדף הפיזי המתאים אינו מוקצה עד הגישה הראשונה. דף שנגעו בו לראשונה בכתיבה מקבל את הדף הפיזי במהלך טיפול ב-demand-zero fault.
האם page fault אומר שמשהו לא תקין או שיש בעיית ביצועים?
לא. שגיאות רכות בלי קלט/פלט דיסק — כמו demand-zero או החזרת דף מ-Standby — הן פעולה רגילה. לשיפוט ביצועים אל תסתכלו רק על Page Faults/sec אלא גם על Pages Input/sec, המתנת אחסון ו-Available MBytes.
האם החטאת TLB ו-page fault הם אותו דבר?
הם שונים. גם אם ב-TLB אין תרגום, אם ה-PTE של טבלת הדפים תקפה המעבד רק הולך בטבלה ורושם מחדש את התרגום. הוא ממשיך לנקודת הכניסה של page fault כשה-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

מנהל KomuraSoft LLC

מתמחה בפיתוח תוכנה עבור Windows, ייעוץ טכני וחקירת תקלות, בעיקר בפרויקטים עם מערכות קיימות ובאגים שקשה לשחזר.

קישורים ציבוריים

חזרה לבלוג