מעמקי הזיכרון של Windows (חלק 3) — אובייקטי מקטע והעתקה-בכתיבה: מה באמת DLL ומיפוי קבצים

· · Windows, ניהול זיכרון, זיכרון משותף, מיפוי קבצים, העתקה-בכתיבה, DLL, Cache Manager

במאמר הקודם, «מעמקי הזיכרון של Windows (חלק 2) — חייו של דף פיזי», עקבנו אחרי דף פיזי שיוצא מ-Working Set ועובר ב-Modified, Standby, Free ו-Zeroed. אותו Standby מחזיק גם דפים של DLL, EXE, קבצים ממופים ומטמון הקבצים.

כאן עולה שאלה. כשמאה תהליכים משתמשים באותו kernel32.dll, האם Windows שם מאה סטים של דפי קוד ב-RAM? כששני תהליכים ממפים את אותו קובץ לזיכרון, האם השני יכול להשתמש בדף שאחד מהם כבר קרא?

התשובה היא למפות כמה כתובות וירטואליות לאותו דף פיזי. המרכז שמייצג את יחידת השיתוף הזו הוא אובייקט המקטע, והמנגנון שמפצל את השיתוף רק בזמן כתיבה הוא העתקה-בכתיבה (Copy-on-Write, CoW).

המאמר עוקב אחרי האופן שבו EXE ו-DLL, קובצי נתונים, זיכרון משותף הנתמך ב-page file ומטמון הקבצים נצמדים לאותו זרם קובץ, ואיפה מסלולי העיבוד מתפצלים. קריאת המספרים עצמם נשענת על מאמר הפתיחה «What Does Windows’ “Memory Usage” Actually Mean?».

«מעמקי הזיכרון של Windows» — שלושת החלקים

  1. חלק 1: כתובות וירטואליות ותקלות דף
    עוקבים אחרי הרגע שבו דף וירטואלי מחויב מקבל RAM פיזי.
  2. חלק 2: חייו של דף פיזי
    עוקבים אחרי מעברי המצב של דף שיוצא מ-Working Set.
  3. חלק 3 (מאמר זה): אובייקטי מקטע והעתקה-בכתיבה
    עוקבים אחרי המנגנון שבו DLL, מיפוי קבצים וזיכרון משותף חולקים דפים פיזיים.

השאלה שחלק 3 עונה עליה היא אחת.

מדוע כמה תהליכים יכולים להשתמש באותו DLL או קובץ כסט אחד של דפים פיזיים?

קהל היעד הוא מפתחים ומפעילים שרוצים להבין שיתוף DLL, CreateFileMapping, MapViewOfFile, זיכרון משותף, CoW והקשר למטמון הקבצים לא רק כשימוש ב-API אלא מהמבנה הפנימי. הסביבה המוקדמת היא Windows 10/11 או Windows Server עדכני, והרקע הנדרש הוא יסודות כתובות וירטואליות, תקלות דף ו-Working Set. רמת הקושי בינונית; אנו משתמשים גם במונחים פנימיים כמו Control Area ו-Prototype PTE, אבל אפשר לשחזר את התצפיות עם VMMap, Process Explorer ו-QueryWorkingSetEx.

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

לפתיחה, התמונה הכללית.

  • אובייקט מקטע מייצג טווח זיכרון שניתן לשתף.
    כל תהליך ממפה חלק מאותו מקטע למרחב הווירטואלי שלו כ«תצוגה».1
  • תצוגות של אותו מקטע אינן חייבות להיות באותה כתובת וירטואלית.
    ה-0x000001... של תהליך A וה-0x000002... של תהליך B יכולים להצביע לאותו היסט מקטע ולאותו דף פיזי.2
  • יש מקטעים נתמכי קובץ ומקטעים נתמכי page file.
    הראשונים משתמשים בקובץ אמיתי; האחרונים משמשים לזיכרון משותף בלי קובץ מפורש וכדומה.2
  • טעינת EXE/DLL היא מקטע תמונה; מיפוי קובץ רגיל הוא מקטע נתונים.
    עם SEC_IMAGE תכונות המקטע בתוך ה-PE קובעות את הגנת הדף.3
  • במהלך קריאה אפשר לשתף את אותו דף פיזי.
    כשאחד הצדדים כותב לדף CoW, רק הדף הזה מועתק ו-PTE של התהליך הכותב מוחלף.4
  • גם על אותו זרם קובץ מסלולי המטמון, הנתונים והתמונה מתפצלים.
    קלט/פלט מטמון של Cache Manager משתמש ב-SharedCacheMap, מיפוי נתונים ב-DataSectionObject, ו-EXE/DLL ב-ImageSectionObject. שלושתם נצמדים לאותו זרם דרך SECTION_OBJECT_POINTERS, אבל תקלת תמונה אינה עוברת ב-Cache Manager.5
  • Private Bytes לבדם אינם מאשרים שקרה CoW.
    FILE_MAP_COPY מחייב Commit לכל התצוגה מראש, למקרה שכל דף יופרט מאוחר יותר. לבדיקה לפי דף השתמשו בסיבית Shared של QueryWorkingSetEx.67

במשפט אחד: מה שמשותף אינו הכתובת הווירטואלית, אלא התוכן בתוך המקטע והדף הפיזי שמתאים לו באותו רגע.

2. אובייקטי מקטע ותצוגות

בהגדרת Microsoft אובייקט מקטע מייצג אזור זיכרון שניתן לשתף והוא גם המנגנון שממפה קובץ למרחב הכתובות של תהליך.1

הטריק להבנה הוא להפריד את המקטע עצמו מהתצוגה שכל תהליך רואה.

מושג תפקיד
אובייקט מקטע מייצג את התוכן לשיתוף, הגודל, ה-backing store ואת תקרת ההגנה
תצוגה מציגה חלק מהמקטע כטווח כתובות וירטואליות בתהליך כלשהו
PTE קושר כל דף וירטואלי בתצוגה לדף הפיזי הנוכחי או למצב שעדיין לא מומש
PFN מייצג דף פיזי שקיים בפועל ב-RAM

ב-Win32 CreateFileMapping מחזיר handle לאובייקט מיפוי קובץ, ו-MapViewOfFile יוצר תצוגה במרחב הווירטואלי של התהליך.3

HANDLE mapping = CreateFileMappingW(
    file,
    nullptr,
    PAGE_READONLY,
    0,
    0,
    nullptr);

void* view = MapViewOfFile(
    mapping,
    FILE_MAP_READ,
    0,
    0,
    0);

קריאה ל-CreateFileMapping לבדה עדיין אינה נותנת כתובת שהתהליך יכול לקרוא. יתרה מכך, יצירת תצוגה אינה שמה מיד כל דף ב-RAM. מהדף הראשון שנוגעים בו, תקלת דף קוראת את תוכן הקובץ וקושרת דף פיזי ל-PTE.8 כלומר, ה-demand paging מחלק 1 חל גם על תצוגות מקטע.

2.1. אותו מקטע, כתובות וירטואליות שונות

גם כשתהליך A ותהליך B ממפים את אותו היסט של אותו מקטע, כתובת ההתחלה של התצוגה יכולה להיות שונה.

מיפוי כתובות וירטואליות שונות לאותו דף פיזילתהליך A ולתהליך B יש תצוגה כל אחד בכתובת וירטואלית שונה, אבל הם מגיעים לאותו דף פיזי PFN X דרך אותו היסט מקטעתהליך A: 0x000001A00000 + 0x3000היסט מקטע 0x3000תהליך B: 0x000002700000 + 0x3000אותו דף פיזי PFN X

איור 1: מה שמשותף הוא התוכן בתוך המקטע והדף הפיזי, לא הכתובת הווירטואלית.

לכן אסור לאחסן מצביע גולמי בזיכרון משותף. ערך המצביע של תהליך A יכול להיות כתובת לא קשורה בתהליך B.

במבנה משותף השתמשו בהיסט מתחילת התצוגה, במספרים שלמים ברוחב קבוע ובגרסה ויישור מפורשים. גם תיעוד MapViewOfFileEx של Microsoft ממליץ לאחסן היסט מהבסיס ולא מצביע, כי אין ערובה שאותה כתובת תהיה זמינה בעתיד.6

3. נתמך-קובץ ונתמך-page file

המקטעים מתפצלים לשני סוגים גדולים לפי המקום שממנו אפשר לשחזר את התוכן.

איך נתמך-קובץ ונתמך-page file מתפצליםהעברת קובץ אמיתי ל-CreateFileMapping יוצרת מקטע נתמך-קובץ, ודף נקי אפשר לקרוא מחדש מהקובץ המקורי. העברת INVALID_HANDLE_VALUE יוצרת מקטע נתמך-page file; קובץ ההחלפה תומך בתוכן שנעלם בהריסת האובייקטהעברת handle של קובץ אמיתיהעברת INVALID_HANDLE_VALUECreateFileMappingמקטע נתמך-קובץמקטע נתמך-page fileדף נקי אפשר לקרוא מחדש מהקובץ המקוריקובץ ההחלפה תומך בתוכן שנעלם בהריסה

איור 2: ההבדל ב-backing store מחליט מאיפה אפשר לשחזר את התוכן וכמה זמן הוא חי.

3.1. מקטעים נתמכי קובץ

העברת קובץ אמיתי ל-CreateFileMapping יוצרת מקטע נתמך-קובץ.

  • תצוגת קריאה בלבד קוראת את הדפים הנדרשים מהקובץ.
  • שינויים בתצוגת קריאה/כתיבה מטופלים כנתונים של אותו קובץ.
  • שינויים בתצוגת CoW אינם נכתבים לקובץ המקורי; הם הופכים לדפים פרטיים.

אם דף נתמך-קובץ נקי, אפשר לזרוק את הדף הפיזי ולקרוא מחדש מהקובץ המקורי. התכונה הזו תומכת ביעילות של Standby ומטמון הקבצים שראינו בחלק 2.

3.2. מקטעים נתמכי page file

העברת INVALID_HANDLE_VALUE כ-hFile של CreateFileMapping וציון גודל יוצרת מקטע נתמך-page file.

HANDLE mapping = CreateFileMappingW(
    INVALID_HANDLE_VALUE,
    nullptr,
    PAGE_READWRITE,
    0,
    64 * 1024,
    L"Local\\KomuraMemoryDemo");

זה מקטע בלי קובץ נתונים מפורש, הנתמך ב-page file. התוכן ההתחלתי אפס, וכמה תהליכים יכולים לפתוח את אותו אובייקט דרך שם, ירושת handle, DuplicateHandle וכדומה.93 שינויים נראים לתהליכים שממפים את אותו דף משותף. מצד שני, כשאובייקט המקטע נהרס התוכן אינו נשאר, ולכן זה אינו מתאים להשארת קובץ מתמשך.2

זהירות: זיכרון משותף אינו מגיע אוטומטית עם הדדיות. mutex, סמפור, אירוע, פרוטוקול lock-free או דומה מתכננים בנפרד.9

4. מיפוי תמונה ומיפוי נתונים

כשאומרים «גם EXE או DLL הוא מיפוי קובץ», עדיין צריך לשמור את ההבדלים מקובץ נתונים רגיל.

פריט מיפוי תמונה מיפוי נתונים
שימוש עיקרי טעינת EXE או DLL קבצים רגילים, נתונים משותפים
תכונת יצירה SEC_IMAGE PAGE_READONLY, PAGE_READWRITE וכדומה
הגנת דף תכונות בתוך תמונת ה-PE מחליטות המיפוי ומפרט התצוגה מחליטים
כתיבות אפשר להפריט דרך מקטע ניתן לכתיבה או CoW אפשר לבחור כתיבה משותפת או CoW
Type של VirtualQuery MEM_IMAGE MEM_MAPPED

עם SEC_IMAGE תכונות המקטע של תמונת הביצוע עצמה מחליטות את הגנת הדף של התצוגה יותר מערך ההגנה הרגיל שמועבר ל-CreateFileMapping.3

בזכות המנגנון הזה דפים שלא שונו כמו קוד יכולים לחלוק את אותו דף פיזי בין תהליכים רבים, ורק דפים שצריכים שינוי פרטי לתהליך מתפצלים דרך CoW. עם זאת לא כל דף DLL בהכרח משותף — בגלל relocation של ASLR, תיקוני loader, hotpatch, תכונות מקטע PE בפועל וכן הלאה.

העיצוב החשוב הוא לשתף קודם דפים שניתן לשתף, ולהעתיק בעצלות רק את הדפים שצריכים שינוי.

5. העתקה-בכתיבה מההתחלה ועד הסוף

נעקוב מהמצב שבו שני תהליכים קוראים את אותו דף CoW ועד שתהליך A כותב בית אחד.

5.1. לפני הכתיבה

לפני שכתיבה מתרחשת, ה-PTE של שני התהליכים מגיעים רעיונית לאותו דף משותף, וקריאות מצליחות כפי שהן.

מצב משותף לפני העתקה-בכתיבהלפני שכתיבה מתרחשת, PTE של תהליך A ו-PTE של תהליך B מגיעים שניהם לאותו דף משותף PFN X, וקריאות מצליחות כפי שהןPTE של תהליך APFN X משותף(קריאה / העתקה-בכתיבה)PTE של תהליך B

איור 3: לפני הכתיבה ה-PTE של שני התהליכים מצביעים לאותו דף פיזי.

5.2. תקלת הגנה בכתיבה

דף CoW אינו מההתחלה דף משותף ניתן לכתיבה רגיל. כשתהליך A מנסה לכתוב, המעבד מעלה תקלת הגנה. מנהל הזיכרון שמקבל שליטה שופט שזו אינה כתיבה בלתי חוקית אלא כתיבה לתכונת CoW.

5.3. יצירת דף פיזי חדש

על פי השיפוט הזה Windows עושה את הדברים הבאים.

  1. להשיג דף פיזי אחד לתהליך A.
  2. להעתיק את תוכן PFN X לדף החדש PFN Y.
  3. להחליף את ה-PTE של תהליך A ל-PFN Y.
  4. לשנות את ההגנה של תהליך A לקריאה/כתיבה רגילה.
  5. לבצע מחדש את הוראת הכתיבה שנכשלה.
מצב מפוצל אחרי העתקה-בכתיבהאחרי כתיבת תהליך A רק ה-PTE של תהליך A מוחלף לדף הפרטי PFN Y שקיבל עותק של התוכן, בעוד שה-PTE של תהליך B ממשיך להצביע לדף המשותף המקורי PFN Xהועתק בכתיבהPTE של תהליך APFN Y פרטי(R/W, אחרי כתיבה)PTE של תהליך BPFN X משותף(מקורי)

איור 4: רק ה-PTE של התהליך הכותב מוחלף לדף פרטי חדש; הצד השני ממשיך לקרוא את התוכן המקורי.

תהליך B ממשיך לקרוא את התוכן המקורי ואינו רואה את השינוי של תהליך A. זו העתקה-בכתיבה. שיתוף DLL ו-FILE_MAP_COPY משתמשים באותו עיקרון: אל תעתיקו עד שאתם כותבים.46

5.4. ההבדל מ-FILE_MAP_WRITE

דף שנכתב בכתיבה משותפת עם FILE_MAP_WRITE מתוכנן כך ששינוי של צד אחד יהיה גלוי גם מתצוגה אחרת שמשתמשת באותו מיפוי קובץ. עם FILE_MAP_COPY, לעומת זאת, רק הדפים שנכתבים הופכים לפרטיים לתהליך; השינויים אינם נכתבים בחזרה לקובץ המקורי ואובדים כשמבטלים את מיפוי התצוגה.6

האם אתם רוצים «להעביר עדכון דרך זיכרון משותף», או «שכל תהליך יעשה שינויים פרטיים מנתונים התחלתיים משותפים»? הבחירה הנכונה הפוכה לפי המטרה.

6. אחרי CoW זה נשאר MEM_MAPPED / MEM_IMAGE

דף אחרי CoW הפך פיזית ל-Private. אפשר לצפות שגם Type של VirtualQuery ישתנה ל-MEM_PRIVATE, אבל בפועל תצוגת נתונים נשארת MEM_MAPPED ותמונה ניתנת להרצה נשארת MEM_IMAGE. VirtualQuery מדווח מאיזה הקצאה התחלתית האזור הגיע.7

כדי לראות לפי דף אם CoW כבר קרה, השתמשו בהליך הבא.

  1. גשו לדף היעד והפכו אותו לתושב.
  2. קבלו את מידע ה-Working Set של הדף עם QueryWorkingSetEx.
  3. הסתכלו בסיבית Shared.
  4. אם Shared == 0, הדף התושב הזה הוא Private.

גם באישור ב-VMMap הסתכלו לא רק ב-Type של האזור אלא בפירוק Private/Shareable של ה-Working Set.

6.1. Private Bytes עלולים לא לעלות

עם FILE_MAP_COPY התהליך עשוי מאוחר יותר לכתוב לכל דף בתצוגה. לכן Windows לוקח חיוב Commit השווה לכל התצוגה בזמן המיפוי.6 כתוצאה מכך כתיבת הדף הראשון אינה בהכרח מעלה את Private Bytes ב-4KiB באותו רגע.

המדדים להעדיף כשצופים ב-CoW הם אלה.

  • סיבית Shared של QueryWorkingSetEx
  • Private WS / Shareable WS של VMMap
  • מידע הדפים הפיזיים של RAMMap
  • Private Bytes כמידע משלים

אם תתייחסו ל«האם Private Bytes עלו ברגע הכתיבה» כמבחן עבר/נכשל, תפספסו CoW שעובד נכון.

7. נקודת המגע עם Cache Manager — להפריד שלושה מסלולים

לומר «טעינת EXE/DLL, מטמון הקבצים וזיכרון משותף כולם מקטעים» נותן את התמונה הכללית, אבל אסור לכווץ את המימוש לאובייקט אחד.

לזרם קובץ יש SECTION_OBJECT_POINTERS שמנהל הזיכרון ו-Cache Manager משתמשים בהם.

typedef struct _SECTION_OBJECT_POINTERS {
    PVOID DataSectionObject;
    PVOID SharedCacheMap;
    PVOID ImageSectionObject;
} SECTION_OBJECT_POINTERS;
  • DataSectionObject: מצב מקטע לקובץ נתונים
  • SharedCacheMap: תצוגת המטמון ש-Cache Manager עוקב אחריה
  • ImageSectionObject: מצב מקטע לתמונה ניתנת להרצה

התיעוד של Microsoft מסביר שהמבנה הזה קושר אובייקט קובץ למקטעי זרם הקובץ ועוקב אחרי תוכן בזיכרון ומידע מטמון.5

כאן נפגשות סדרת ה-I/O וסדרת הזיכרון. הבינו את שלושת המסלולים בנפרד.

  • ReadFile / WriteFile במטמון משתמשים ב-SharedCacheMap ובתצוגת המטמון של Cache Manager.
  • תקלת מיפוי על קובץ נתונים מטופלת בידי מנהל הזיכרון בצד DataSectionObject. הוא משתף פעולה עם קלט/פלט במטמון על אותו זרם קובץ כדי לשמור על עקביות התוכן.
  • תקלת תמונה על EXE/DLL מטופלת בידי מנהל הזיכרון עם ImageSectionObject וקלט/פלט דפדוף. זה אינו מסלול שעובר ב-SharedCacheMap של Cache Manager.
שלושה מסלולים שנצמדים לאותו זרם קובץReadFile/WriteFile במטמון משתמשים ב-SharedCacheMap, תקלת מיפוי נתונים משתמשת ב-DataSectionObject, ותקלת תמונת EXE/DLL משתמשת ב-ImageSectionObject; שלושתם נצמדים לאותו זרם קובץ דרך SECTION_OBJECT_POINTERSReadFile / WriteFile במטמוןSharedCacheMapתקלת מיפוי נתוניםDataSectionObjectתקלת תמונת EXE/DLLImageSectionObjectאותו זרם קובץ(SECTION_OBJECT_POINTERS)

איור 5: שלושת המסלולים מעובדים בנפרד, אבל נצמדים על אותו זרם קובץ.

המשותף לשלושה אינו ש«הכול נכנס ל-Cache Manager», אלא שאותו זרם קובץ קושר מצבים נפרדים — מטמון, מקטע נתונים ומקטע תמונה — דרך SECTION_OBJECT_POINTERS.5 קריאות וכתיבות מטמון, Lazy Writer והקשר בין Cc ל-Mm מכוסים ב«The Depths of Windows I/O (Part 4) — Cache Manager: When Does Your WriteFile Actually Reach the Disk?».

שימו לב שכשמערבבים תצוגה ממופת-זיכרון עם ReadFile/WriteFile אין ערובה שתראו תמיד את התוכן של אותו רגע. התכנון צריך לכלול סנכרון, flush ומצב שיתוף הקובץ.36

8. אורך החיים של האובייקט והתצוגה

סגירת ה-handle של CreateFileMapping לבדה אינה הורסת תצוגה קיימת. תצוגה מחזיקה הפניה פנימית למקטע, ורק אחרי שכל תצוגה עברה UnmapViewOfFile וכל handle עבר CloseHandle האובייקט הופך להריס.3

UnmapViewOfFile(view);
CloseHandle(mapping);
CloseHandle(file);

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

הקשר ל-cleanup/close בצד ה-I/O מכוסה ב«The Depths of Windows I/O (Part 1)», ומלכודות המימוש ב«המלכודות של הזיכרון המשותף ושיטות העבודה המומלצות בפועל».

9. ראו בעצמכם

9.1. הסתכלות על אותו DLL משני תהליכים

קודם אשרו שיתוף DLL עם תהליכים קיימים.

  1. הפעילו Process Explorer כמנהל.
  2. הפעילו שני תהליכי cmd.exe.
  3. בחרו View > Lower Pane View > DLLs.
  4. אשרו את הנתיב והמיפוי של אותו DLL בשני התהליכים.
  5. פתחו כל cmd.exe ב-VMMap והשוו Images Working Set, Private ו-Shareable.

לראות את אותו DLL ב-Process Explorer הוא ראיה ששניהם מיפו את אותה תמונה. זה לבדו, עם זאת, אינו מוכיח ש-PFN של כל דף תואם. שלבו את פירוק Shareable של VMMap, RAMMap ו-QueryWorkingSetEx כדי לאשר שיתוף לפי דף. Process Explorer ו-VMMap מסופקים בידי Sysinternals.1011

9.2. תצפית על FILE_MAP_COPY משני תהליכים

התוכנית הבאה ממפה את אותו קובץ כתצוגת CoW ומציגה את סיבית Shared של QueryWorkingSetEx. אובייקט מיפוי הקובץ נוצר עם PAGE_READONLY, אבל ההגנה הזו תואמת תצוגת FILE_MAP_COPY, והכתיבה הראשונה בצד התצוגה גורמת ל-CoW.3

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <psapi.h>

#include <cstdio>
#include <cwchar>

#pragma comment(lib, "Psapi.lib")

void PrintPage(const char* stage, void* address)
{
    MEMORY_BASIC_INFORMATION mbi{};
    if (!VirtualQuery(address, &mbi, sizeof(mbi))) {
        std::printf("VirtualQuery failed: %lu\n", GetLastError());
        return;
    }

    for (int attempt = 0; attempt < 3; ++attempt) {
        // The page may have been trimmed while the user was waiting.
        // Touch it immediately before querying the working-set attributes.
        volatile unsigned char resident =
            *static_cast<volatile unsigned char*>(address);
        (void)resident;

        PSAPI_WORKING_SET_EX_INFORMATION ws{};
        ws.VirtualAddress = address;
        if (!QueryWorkingSetEx(GetCurrentProcess(), &ws, sizeof(ws))) {
            std::printf("QueryWorkingSetEx failed: %lu\n", GetLastError());
            return;
        }

        if (!ws.VirtualAttributes.Valid) {
            Sleep(0);
            continue;
        }

        std::printf(
            "%s: Type=0x%lx Valid=1 Shared=%llu ShareCount=%llu\n",
            stage,
            static_cast<unsigned long>(mbi.Type),
            static_cast<unsigned long long>(ws.VirtualAttributes.Shared),
            static_cast<unsigned long long>(ws.VirtualAttributes.ShareCount));
        return;
    }

    std::printf(
        "%s: page is not resident; Shared/ShareCount were not interpreted\n",
        stage);
}

int wmain(int argc, wchar_t** argv)
{
    if (argc != 3) {
        std::fwprintf(stderr, L"usage: cow_demo <file> <read|write>\n");
        return 2;
    }

    HANDLE file = CreateFileW(
        argv[1], GENERIC_READ, FILE_SHARE_READ,
        nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
    if (file == INVALID_HANDLE_VALUE) return 3;

    HANDLE mapping = CreateFileMappingW(
        file, nullptr, PAGE_READONLY, 0, 0, nullptr);
    if (!mapping) {
        CloseHandle(file);
        return 4;
    }

    auto* view = static_cast<unsigned char*>(
        MapViewOfFile(mapping, FILE_MAP_COPY, 0, 0, 0));
    if (!view) {
        CloseHandle(mapping);
        CloseHandle(file);
        return 5;
    }

    volatile unsigned char value = view[0];
    (void)value;

    std::puts("Start the other process. When both are waiting, press Enter...");
    (void)std::getchar();
    PrintPage("before", view);

    if (std::wcscmp(argv[2], L"write") == 0) {
        std::puts("Press Enter to trigger copy-on-write...");
        (void)std::getchar();
        view[0] ^= 0x5a;
        PrintPage("after write", view);
    } else {
        std::puts("After the writer changes its page, press Enter...");
        (void)std::getchar();
        PrintPage("reader after peer write", view);
    }

    std::puts("Press Enter to exit...");
    (void)std::getchar();

    UnmapViewOfFile(view);
    CloseHandle(mapping);
    CloseHandle(file);
}

בנו והכינו.

cl /std:c++20 /EHsc /W4 cow_demo.cpp

$path = "$env:TEMP\\cow-demo.bin"
[IO.File]::WriteAllBytes($path, [byte[]]::new(65536))

ואז פתחו את אותו קובץ משתי קונסולות.

.\\cow_demo.exe "$env:TEMP\\cow-demo.bin" read
.\\cow_demo.exe "$env:TEMP\\cow-demo.bin" write

אחרי שהפעלתם את שניהם לחצו Enter קודם בצד הקריאה ואז בצד הכתיבה, ואשרו ש-Shared מוגדר ב-before בשניהם. לחצו Enter שוב בצד הכתיבה והדף של התהליך הזה הופך ל-Shared 0. אחר כך לחצו Enter בצד הקריאה ותוכלו לאשר שהקורא ממשיך לקרוא את הדף המקורי. Type של VirtualQuery נשאר MEM_MAPPED גם אחרי הכתיבה.

שימו לב ש-PrintPage נוגע שוב בדף היעד מיד לפני השאילתה, ואם Valid == 0 הוא אינו מפרש Shared ו-ShareCount ומנסה שוב עד שלוש פעמים. אם הדף עדיין אינו תושב הוא אינו מפיק תוצאה ומדווח על כך. ShareCount יכול להשתנות עם תזמון ולחץ זיכרון, לכן הסתכלו בשינוי סיבית Shared שאושר ב-Valid == 1, לא בערך קבוע.

10. חמישה קריאות שגויות להימנע מהן בפועל

10.1. «זיכרון משותף נגמר באותה כתובת וירטואלית»

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

10.2. «אם זה אותו DLL, כל דף בהכרח משותף»

דפי קוד נקיים קלים לשיתוף, בעוד relocation, מקטע ניתן לכתיבה, CoW והתושבות ברגע המדידה גם מייצרים דפי Private.

10.3. «אחרי CoW זה הופך ל-MEM_PRIVATE»

Type של VirtualQuery נשאר MEM_MAPPED או MEM_IMAGE. אשרו את מצב השיתוף בפועל עם QueryWorkingSetEx.7

10.4. «אם Private Bytes לא עלו, CoW לא קרה»

FILE_MAP_COPY מחייב Commit לכל התצוגה מראש. העדיפו Private WS וסיבית Shared.6

10.5. «אם הדף משותף, אין צורך בסנכרון»

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

הרעיון לעקוב בנפרד אחרי הפניות ואורך חיים חל גם על תהליך שנשאר אחרי interop COM של Excel. ראו גם «Why EXCEL.EXE Processes Remain After C# Excel COM Automation — Reference Release Patterns and the Replacement Decision».

11. סיכום

  • אובייקט מקטע מייצג טווח זיכרון שניתן לשתף, וכל תהליך ממפה אותו למרחב הווירטואלי שלו כתצוגה.1
  • אותו היסט של אותו מקטע ממופה מכתובות וירטואליות שונות לאותו דף פיזי.2
  • מקטע נתמך-קובץ תומך בקובץ אמיתי; מקטע נתמך-page file תומך בזיכרון משותף בשם וכדומה.9
  • EXE/DLL מטופל כמקטע תמונה וקובץ רגיל כמקטע נתונים; ההגנה ויעד הכתיבה-חזרה שונים.3
  • CoW משתף את הדף הפיזי בקריאה ובכתיבה הראשונה מעתיק רק את הדף הזה ומחליף את ה-PTE.4
  • אחרי CoW VirtualQuery עדיין מחזיר MEM_MAPPED/MEM_IMAGE, לכן מאשרים עם סיבית Shared של QueryWorkingSetEx.7
  • עם FILE_MAP_COPY Commit לכל התצוגה מחויב קודם, ולכן אי אפשר לשפוט CoW לפי Private Bytes בלבד.6
  • קלט/פלט מטמון של Cache Manager, מיפוי נתונים ומיפוי תמונה נצמדים לאותו זרם קובץ כמסלולים נפרדים שמשתמשים ב-SharedCacheMap, DataSectionObject ו-ImageSectionObject.5
  • זיכרון משותף הופך לבטוח רק כשכוללים את אורך החיים של תצוגות ו-handles, סנכרון, ACL ועיצוב היסטים.

בכך מסתיימים שלושת חלקי «מעמקי הזיכרון של Windows». אתם Reserve/Commit כתובת וירטואלית, מקבלים דף פיזי דרך תקלת דף, מזיזים את הדף מ-Working Set לרשימת דפים, משתפים אותו דרך מקטע ומפצלים דרך CoW רק את הדפים שכתבתם — ניהול הזיכרון של Windows מחובר כזרימה אחת זו.

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

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

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

קישורים

  1. Microsoft Learn, Section Objects and Views. על אובייקט מקטע שמייצג טווח זיכרון שניתן לשתף, ועל כל תהליך שממפה חלק מהמקטע כתצוגה.  2 3

  2. Microsoft Learn, File-Backed and Page-File-Backed Sections. על מקטעים נתמכי קובץ ונתמכי page file, CoW, והיכולת לשתף את אותו זיכרון פיזי מכתובות וירטואליות של תהליכים שונים.  2 3 4

  3. Microsoft Learn, CreateFileMappingW function. על אובייקט מיפוי הקובץ, מקטעים נתמכי page file, SEC_IMAGE, אורך החיים של תצוגות ו-handles, והעקביות בין תצוגות שתומכות באותו קובץ.  2 3 4 5 6 7 8

  4. Microsoft Learn, Memory Protection. על כמה תהליכים שחולקים את הדפים הפיזיים של אותו DLL, ועל CoW שמעתיק לדף פיזי חדש ומעדכן את ה-PTE כשאחד הצדדים כותב.  2 3

  5. Microsoft Learn, SECTION_OBJECT_POINTERS structure. על DataSectionObject, SharedCacheMap ו-ImageSectionObject שקושרים מיפוי של זרם קובץ ומידע מטמון למנהל הזיכרון / Cache Manager.  2 3 4

  6. Microsoft Learn, MapViewOfFileEx function. על CoW עם FILE_MAP_COPY; דפים פרטיים הנתמכים ב-page file; חיוב Commit לכל התצוגה; ואחסון היסט במקום כתובת וירטואלית.  2 3 4 5 6 7 8

  7. Microsoft Learn, VirtualQuery function. על כך ש-Type נשאר MEM_MAPPED/MEM_IMAGE אחרי CoW, ושאפשר לאשר הפרטה עם סיבית Shared של QueryWorkingSetEx 2 3 4

  8. Microsoft Learn, Managing Memory Sections. על כך שזיכרון פיזי אינו מוקצה עד שניגשים לתצוגה, ועל תקלת הדף של הגישה הראשונה שקוראת את תוכן הקובץ. 

  9. Microsoft Learn, Sharing Files and Memory. על שיתוף אותו אובייקט מיפוי קובץ בשם או ב-handle; יצירת זיכרון משותף נתמך-page file עם INVALID_HANDLE_VALUE; ועל כך שסנכרון נדרש בנפרד.  2 3

  10. Microsoft Learn, Process Explorer - Sysinternals. על כך ש-Process Explorer יכול להציג handles של תהליך ו-DLL / קבצים ממופי-זיכרון שנטענו. 

  11. Microsoft Learn, VMMap - Sysinternals. על כך ש-VMMap מפרק את הזיכרון הווירטואלי של תהליך ל-Image, Mapped File, Private וכדומה, ומציג את פירוק Private/Shareable של ה-Working Set. 

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

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

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

שאלות נפוצות

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

האם כל תהליך שמשתמש באותו DLL מקבל עותק מלא של אותו DLL ב-RAM?
בדרך כלל לא. דפים שלא שונו מאותה תמונה ממופים מכתובות וירטואליות שונות של כל תהליך לאותו דף פיזי. רק דפים שצריך לכתוב הופכים לדפים פיזיים פרטיים לתהליך, דרך העתקה-בכתיבה וכדומה.
האם CreateFileMapping מקצה זיכרון לתהליך באותו רגע?
CreateFileMapping יוצר אובייקט מיפוי קובץ, אבל MapViewOfFile הוא שעושה אותו גלוי במרחב הווירטואלי של התהליך. יתרה מכך, הדפים הפיזיים של התצוגה מתממשים בדרך כלל בתקלת דף מהדף הראשון שניגשים אליו.
מה ההבדל בין FILE_MAP_WRITE ל-FILE_MAP_COPY?
שינוי דרך FILE_MAP_WRITE הוא כתיבה שמשתקפת בצד נתוני הקובץ המשותפים. FILE_MAP_COPY משתף את הדפים ההתחלתיים, אבל רק הדפים שנכתבים הופכים לעותקים פרטיים לתהליך; השינויים אינם נכתבים בחזרה לקובץ המקורי ואובדים כשמבטלים את מיפוי התצוגה.
אחרי העתקה-בכתיבה, האם VirtualQuery מחזיר MEM_PRIVATE?
לא. תצוגת נתונים נשארת MEM_MAPPED ותצוגת תמונה נשארת MEM_IMAGE. כדי לראות אם דף אכן הופרט, הפוך את הדף לתושב ובדוק את סיבית Shared של QueryWorkingSetEx.
מותר לאחסן מצביע גולמי בזיכרון משותף?
בדרך כלל לא. גם באותו מקטע אין ערובה שתצוגת כל תהליך תוצב באותה כתובת וירטואלית. במבנה משותף השתמשו בהיסטים מהבסיס, במספרים שלמים ברוחב קבוע ובפריסה וסכימת סנכרון מפורשות.

פרופיל הכותב

עמוד היכרות עם כותב המאמר.

Go Komura

מנהל KomuraSoft LLC

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

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

חזרה לבלוג