Windows Memory Internals (חלק 3) —‏ Section objects ו-Copy-on-Write: מה באמת קורה ב-DLL וב-file mapping

· עודכן בתאריך: · · Windows, ניהול זיכרון, shared memory, file mapping, Copy-on-Write, DLL, Cache Manager

היסטוריית עדכונים (גרסה ראשונה, פורסמה בתאריך 20 Aug 2026)
פרסום ראשון
לצטט את המאמר הזה(DOI: 10.5281/zenodo.22176180)

מאמר זה מאוחסן בארכיון Zenodo. להלן גם ה-DOI שתמיד מפנה לגרסה האחרונה וגם ה-DOI המקובע לגרסה שאתם קוראים.

Go Komura (2026). Windows Memory Internals (חלק 3) —‏ Section objects ו-Copy-on-Write: מה באמת קורה ב-DLL וב-file mapping. KomuraSoft LLC. https://doi.org/10.5281/zenodo.22176180 https://comcomponent.com/he/blog/windows-memory-internals-section-copy-on-write/

DOI (הגרסה האחרונה)
10.5281/zenodo.22176180
DOI (הגרסה הזו)
10.5281/zenodo.22176181

במאמר הקודם, “Windows Memory Internals (חלק 2) — מחזור החיים של physical page”, עקבנו אחרי physical page שיוצא מ-Working Set ועובר בין Modified, Standby, Free ו-Zeroed. באותו Standby נשארים גם pages של DLL, EXE, mapped files ושל file cache.

ומכאן השאלה. כשמאה processes משתמשים באותו kernel32.dll, האם Windows שם ב-RAM מאה סטים של code pages? וכששני processes ממפים את אותו קובץ לזיכרון, האם השני יכול להשתמש ב-page שהראשון כבר קרא?

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

המאמר עוקב אחרי EXE ו-DLL, קובצי data, shared memory עם page-file backing, ו-file cache: איפה הם מתחברים לאותו file stream, ואיפה נתיבי העיבוד מתפצלים. את קריאת המספרים עצמם מניחים לפי מאמר הפתיחה What Does Windows’ “Memory Usage” Actually Mean?.

Windows Memory Internals — שלושת החלקים

  1. חלק 1: virtual addresses ו-Page Fault
    עוקבים אחרי הרגע שבו virtual page שעבר Commit מקבל RAM פיזי.
  2. חלק 2: מחזור החיים של physical page
    עוקבים אחרי מעברי המצב של page שיוצא מ-Working Set.
  3. חלק 3 (המאמר הזה): section objects ו-Copy-on-Write
    עוקבים אחרי המנגנון שבו DLL, file mapping ו-shared memory חולקים physical pages.

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

למה כמה processes יכולים להשתמש באותו DLL או קובץ כסט אחד של physical pages?

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

1. קודם כל, המסקנה

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

  • Section object מייצג טווח זיכרון שניתן לשתף.
    כל process ממפה חלק מאותו section למרחב הווירטואלי שלו כ-view.1
  • Views של אותו section לא חייבים לשבת באותה virtual address.
    ה-0x000001... של process A וה-0x000002... של process B יכולים להצביע לאותו section offset ולאותו physical page.2
  • יש file-backed sections ויש page-file-backed sections.
    הראשונים נשענים על קובץ אמיתי; האחרונים משמשים ל-shared memory בלי קובץ מפורש וכדומה.2
  • טעינת EXE/DLL היא image section; file mapping רגיל הוא data section.
    עם SEC_IMAGE, תכונות ה-section בתוך ה-PE קובעות את ה-page protection.3
  • בזמן קריאה אפשר לשתף את אותו physical page.
    כשאחד הצדדים כותב ל-CoW page, רק ה-page הזה מועתק וה-PTE של ה-process הכותב מוחלף.4
  • גם על אותו file stream, נתיבי ה-cache, ה-data וה-image מתפצלים.
    cached I/O של Cache Manager משתמש ב-SharedCacheMap, data mapping ב-DataSectionObject, ו-EXE/DLL ב-ImageSectionObject. שלושתם מתחברים לאותו stream דרך SECTION_OBJECT_POINTERS, אבל image fault לא עובר דרך Cache Manager.5
  • Private Bytes לבדם לא מוכיחים שקרה CoW.
    FILE_MAP_COPY מחייב Commit charge על כל ה-view מראש, למקרה שכל page יהפוך מאוחר יותר ל-Private. לבדיקה לפי page משתמשים בסיבית Shared של QueryWorkingSetEx.67

במשפט אחד: מה שמשותף אינו ה-virtual address, אלא התוכן בתוך ה-section וה-physical page שמתאים לו באותו רגע.

2. Section objects ו-views

לפי ההגדרה של Microsoft, section object מייצג אזור זיכרון שניתן לשתף, והוא גם המנגנון שממפה קובץ למרחב הכתובות של process.1

הדרך להבין את זה היא להפריד את ה-section עצמו מה-view שכל process רואה.

מונח תפקיד
section object מייצג את התוכן לשיתוף, הגודל, ה-backing store ואת תקרת ה-protection
view מציג חלק מה-section כטווח virtual addresses ב-process כלשהו
PTE קושר כל virtual page בתוך ה-view ל-physical page הנוכחי, או למצב שעדיין לא מומש
PFN מייצג physical page שקיים בפועל ב-RAM

ב-Win32, CreateFileMapping מחזיר handle ל-file-mapping object, ו-MapViewOfFile יוצר view במרחב הווירטואלי של ה-process.3

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

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

קריאה ל-CreateFileMapping לבדה עדיין לא נותנת כתובת שה-process יכול לקרוא ממנה. מעבר לזה, יצירת view לא שמה מיד כל page ב-RAM. מה-page הראשון שנוגעים בו, Page Fault קורא את תוכן הקובץ וקושר physical page ל-PTE.8 כלומר, ה-demand paging מחלק 1 חל גם על section views.

2.1. אותו section, virtual addresses שונות

גם כש-process A ו-process B ממפים את אותו offset של אותו section, כתובת ההתחלה של ה-view יכולה להיות שונה.

מיפוי virtual addresses שונות לאותו physical pageל-Process A ול-Process B יש view כל אחד ב-virtual address שונה, אבל הם מגיעים לאותו physical page PFN X דרך אותו section offsetProcess A: 0x000001A00000 + 0x3000section offset 0x3000Process B: 0x000002700000 + 0x3000אותו physical page, PFN X

איור 1: מה שמשותף הוא התוכן בתוך ה-section וה-physical page, לא ה-virtual address.

לכן אסור לשמור raw pointer ב-shared memory. ערך ה-pointer של process A יכול להיות כתובת לא קשורה ב-process B.

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

3. File-backed ו-page-file-backed

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

איך file-backed ו-page-file-backed מתפצליםהעברת קובץ אמיתי ל-CreateFileMapping יוצרת file-backed section, ו-clean page אפשר לקרוא מחדש מהקובץ המקורי. העברת INVALID_HANDLE_VALUE יוצרת page-file-backed section; ה-page file תומך בתוכן שנעלם בהריסת ה-objectמעבירים handle של קובץ אמיתימעבירים INVALID_HANDLE_VALUECreateFileMappingfile-backed sectionpage-file-backed sectionclean page אפשר לקרוא מחדש מהקובץ המקוריה-page file תומך בתוכן; בהריסה הוא נעלם

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

3.1. File-backed sections

העברת קובץ אמיתי ל-CreateFileMapping יוצרת file-backed section.

  • view לקריאה בלבד קורא את ה-pages הנדרשים מהקובץ.
  • שינויים ב-view לקריאה/כתיבה מטופלים כ-data של אותו קובץ.
  • שינויים ב-CoW view לא נכתבים לקובץ המקורי; הם הופכים ל-Private pages.

אם file-backed page הוא clean, אפשר לזרוק את ה-physical page ולקרוא מחדש מהקובץ המקורי. התכונה הזו תומכת ביעילות של Standby ושל file cache שראינו בחלק 2.

3.2. Page-file-backed sections

העברת INVALID_HANDLE_VALUE כ-hFile של CreateFileMapping וציון גודל יוצרת page-file-backed section.

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

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

שימו לב: shared memory לא מגיע אוטומטית עם mutual exclusion. mutex, semaphore, event, פרוטוקול lock-free או מנגנון דומה מתכננים בנפרד.9

4. Image mapping ו-data mapping

כשאומרים “גם EXE או DLL הוא file mapping”, עדיין צריך לשמור את ההבדלים מקובץ data רגיל.

פריט image mapping data mapping
שימוש עיקרי טעינת EXE או DLL קבצים רגילים, shared data
תכונת יצירה SEC_IMAGE PAGE_READONLY, PAGE_READWRITE וכדומה
page protection תכונות בתוך תמונת ה-PE מחליטות ה-mapping ומפרט ה-view מחליטים
כתיבות אפשר להפוך ל-Private דרך writable section או CoW אפשר לבחור shared write או CoW
Type של VirtualQuery MEM_IMAGE MEM_MAPPED

עם SEC_IMAGE, תכונות ה-section של תמונת ההרצה עצמה קובעות את ה-page protection של ה-view יותר מערך ה-protection הרגיל שמועבר ל-CreateFileMapping.3

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

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

5. Copy-on-Write, צעד אחר צעד

נעקוב מהמצב שבו שני processes קוראים את אותו CoW page ועד ש-process A כותב byte אחד.

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

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

מצב משותף לפני Copy-on-Writeלפני שכתיבה מתרחשת, ה-PTE של Process A וה-PTE של Process B מגיעים שניהם לאותו shared page PFN X, וקריאות מצליחות כמו שהןProcess A PTEPFN X משותף (read / copy-on-write)Process B PTE

איור 3: לפני הכתיבה ה-PTE של שני ה-processes מצביעים לאותו physical page.

5.2. Protection fault בכתיבה

CoW page אינו מההתחלה shared page רגיל שניתן לכתיבה. כש-process A מנסה לכתוב, ה-CPU מעלה protection fault. Memory Manager שמקבל שליטה קובע שזו לא כתיבה לא חוקית, אלא כתיבה לתכונת CoW.

5.3. יצירת physical page חדש

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

  1. להשיג physical page אחד ל-process A.
  2. להעתיק את תוכן PFN X ל-page החדש PFN Y.
  3. להחליף את ה-PTE של process A ל-PFN Y.
  4. לשנות את ה-protection של process A לקריאה/כתיבה רגילה.
  5. לבצע מחדש את הוראת הכתיבה שנכשלה.
מצב מפוצל אחרי Copy-on-Writeאחרי כתיבת Process A רק ה-PTE של Process A מוחלף ל-Private page PFN Y שקיבל עותק של התוכן, בעוד שה-PTE של Process B ממשיך להצביע ל-shared page המקורי PFN Xcopy של התוכן בכתיבהProcess A PTEPrivate PFN Y (R/W, אחרי כתיבה)Process B PTEPFN X משותף (מקורי)

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

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

5.4. ההבדל מ-FILE_MAP_WRITE

Page שנכתב ב-shared write עם FILE_MAP_WRITE מתוכנן כך ששינוי של צד אחד יהיה גלוי גם מ-view אחר שמשתמש באותו file mapping. עם FILE_MAP_COPY, לעומת זאת, רק ה-pages שנכתבים הופכים ל-Private של ה-process; השינויים לא נכתבים בחזרה לקובץ המקורי, והם נעלמים כשמבטלים את המיפוי של ה-view.6

האם אתם רוצים להעביר עדכון דרך shared memory, או שכל process יעשה שינויים פרטיים מתוך data התחלתי משותף? לפי המטרה הבחירה הנכונה הפוכה.

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

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

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

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

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

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

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

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

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

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

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

לומר “טעינת EXE/DLL, file cache ו-shared memory כולם sections” נותן את התמונה הכללית, אבל אסור לכווץ את המימוש לאובייקט אחד.

ל-file stream יש SECTION_OBJECT_POINTERS ש-Memory Manager ו-Cache Manager משתמשים בהם.

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

התיעוד של Microsoft מסביר שהמבנה הזה קושר file object ל-sections של ה-file stream ועוקב אחרי תוכן בזיכרון ומידע cache.5

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

  • cached ReadFile / WriteFile משתמשים ב-SharedCacheMap וב-cache view של Cache Manager.
  • mapping fault על קובץ data מטופל בידי Memory Manager בצד DataSectionObject. הוא משתף פעולה עם cached I/O על אותו file stream כדי לשמור על עקביות התוכן.
  • image fault על EXE/DLL מטופל בידי Memory Manager עם ImageSectionObject ו-paging I/O. זה לא נתיב שעובר ב-SharedCacheMap של Cache Manager.
שלושה נתיבים שמתחברים לאותו file streamcached ReadFile/WriteFile משתמשים ב-SharedCacheMap, mapping fault של data משתמש ב-DataSectionObject, ו-image fault של EXE/DLL משתמש ב-ImageSectionObject; שלושתם מתחברים לאותו file stream דרך SECTION_OBJECT_POINTERScached ReadFile / WriteFileSharedCacheMapmapping fault על קובץ dataDataSectionObjectimage fault של EXE/DLLImageSectionObjectאותו file stream (SECTION_OBJECT_POINTERS)

איור 5: שלושת הנתיבים מעובדים בנפרד, אבל מתחברים על אותו file stream.

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

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

8. ה-lifetime של ה-object ושל ה-view

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

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

הפרדת ה-lifetimes הזו היא סיבה לתופעה “סגרתי את הקובץ, אבל הוא עדיין בשימוש”. גם אחרי שסוגרים את handle הקובץ, אם image section או data view עדיין מתייחסים ל-file stream, ה-close הסופי של הקובץ מגיע מאוחר יותר.

הקשר ל-cleanup/close בצד ה-I/O מכוסה ב-The Depths of Windows I/O (Part 1), ומלכודות המימוש ב-מלכודות של shared memory ושיטות עבודה מומלצות בשטח.

9. לראות את זה בעצמכם

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

קודם מאשרים שיתוף DLL עם processes קיימים.

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

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

9.2. תצפית על FILE_MAP_COPY משני processes

התוכנית הבאה ממפה את אותו קובץ כ-CoW view ומציגה את סיבית Shared של QueryWorkingSetEx. ה-file-mapping object נוצר עם PAGE_READONLY, אבל ה-protection הזו תואמת view של FILE_MAP_COPY, והכתיבה הראשונה בצד ה-view גורמת ל-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 שוב בצד הכתיבה וה-page של ה-process הזה הופך ל-Shared 0. אחר כך לוחצים Enter בצד הקריאה ואפשר לאשר שהקורא ממשיך לקרוא את ה-page המקורי. Type של VirtualQuery נשאר MEM_MAPPED גם אחרי הכתיבה.

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

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

10.1. “Shared memory נגמר באותה virtual address”

מה שמשותף הוא ה-section וה-physical page. ה-virtual address של ה-view יכולה להשתנות לפי process, לכן שומרים offset ולא raw pointer.

10.2. “אם זה אותו DLL, כל page בהכרח משותף”

Clean code pages קלים לשיתוף, בעוד relocation, writable section, CoW והמצב ה-resident ברגע המדידה גם מייצרים Private pages.

10.3. “אחרי CoW זה הופך ל-MEM_PRIVATE”

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

10.4. “אם Private Bytes לא עלו, CoW לא קרה”

FILE_MAP_COPY מחייב Commit charge על כל ה-view מראש. מעדיפים Private WS וסיבית Shared.6

10.5. “אם ה-page משותף, אין צורך בסנכרון”

לראות את אותו physical page ולעדכן אותו בבטחה מכמה ליבות CPU הן בעיות שונות. מתכננים atomicity, memory ordering, mutual exclusion, מצב ביניים בקריסה ותאימות גרסאות.

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

11. סיכום

  • Section object מייצג טווח זיכרון שניתן לשתף, וכל process ממפה אותו למרחב הווירטואלי שלו כ-view.1
  • אותו offset של אותו section ממופה מ-virtual addresses שונות לאותו physical page.2
  • File-backed section נשען על קובץ אמיתי; page-file-backed section תומך ב-shared memory בשם וכדומה.9
  • EXE/DLL מטופל כ-image section וקובץ רגיל כ-data section; ה-protection ויעד הכתיבה-חזרה שונים.3
  • CoW משתף את ה-physical page בקריאה, ובכתיבה הראשונה מעתיק רק את ה-page הזה ומחליף את ה-PTE.4
  • אחרי CoW, VirtualQuery עדיין מחזיר MEM_MAPPED/MEM_IMAGE, לכן מאשרים עם סיבית Shared של QueryWorkingSetEx.7
  • עם FILE_MAP_COPY ה-Commit charge על כל ה-view מחויב קודם, ולכן אי אפשר לשפוט CoW לפי Private Bytes בלבד.6
  • Cached I/O של Cache Manager, data mapping ו-image mapping מתחברים לאותו file stream כנתיבים נפרדים שמשתמשים ב-SharedCacheMap, DataSectionObject ו-ImageSectionObject.5
  • Shared memory הופך לבטוח רק כשכוללים את ה-lifetime של views ו-handles, סנכרון, ACL ועיצוב offsets.

בכך מסתיימים שלושת חלקי Windows Memory Internals. עושים Reserve/Commit ל-virtual address, מקבלים physical page דרך Page Fault, מזיזים את ה-page מ-Working Set לרשימת pages, משתפים אותו דרך section ומפצלים דרך CoW רק את ה-pages שכתבתם — ניהול הזיכרון של Windows מחובר כזרימה אחת כזו.

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

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

KomuraSoft LLC מטפלת בחקירת תקלות סביב shared memory, file mapping, טעינת DLL, נעילות קבצים, תקשורת בין-תהליכים ושימוש בזיכרון באפליקציות Windows.

קישורים

  1. Microsoft Learn, Section Objects and Views. על section object שמייצג טווח זיכרון שניתן לשתף, ועל כל process שממפה חלק מה-section כ-view. ↩ ↩2 ↩3

  2. Microsoft Learn, File-Backed and Page-File-Backed Sections. על file-backed sections ו-page-file-backed sections, CoW, והיכולת לשתף את אותו זיכרון פיזי מ-virtual addresses של processes שונים. ↩ ↩2 ↩3 ↩4

  3. Microsoft Learn, CreateFileMappingW function. על ה-file-mapping object, page-file-backed sections, SEC_IMAGE, ה-lifetime של views ו-handles, והעקביות בין views שתומכים באותו קובץ. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8

  4. Microsoft Learn, Memory Protection. על כמה processes שחולקים את ה-physical pages של אותו DLL, ועל CoW שמעתיק ל-physical page חדש ומעדכן את ה-PTE כשאחד הצדדים כותב. ↩ ↩2 ↩3

  5. Microsoft Learn, SECTION_OBJECT_POINTERS structure. על DataSectionObject, SharedCacheMap ו-ImageSectionObject שקושרים mapping של file stream ומידע cache ל-Memory Manager / Cache Manager. ↩ ↩2 ↩3 ↩4

  6. Microsoft Learn, MapViewOfFileEx function. על CoW עם FILE_MAP_COPY; Private pages עם backing של page file; Commit charge על כל ה-view; ושמירת offset במקום virtual address. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8

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

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

  9. Microsoft Learn, Sharing Files and Memory. על שיתוף אותו file-mapping object בשם או ב-handle; יצירת page-file-backed shared memory עם INVALID_HANDLE_VALUE; ועל כך שסנכרון נדרש בנפרד. ↩ ↩2 ↩3

  10. Microsoft Learn, Process Explorer - Sysinternals. על כך ש-Process Explorer יכול להציג handles של process ו-DLL / memory-mapped files שנטענו. ↩

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

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

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

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

שאלות נפוצות

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

האם כל process שמשתמש באותו DLL מקבל עותק מלא של אותו DLL ב-RAM?
בדרך כלל לא. pages שלא שונו מאותו image ממופים מ-virtual addresses שונות של כל process לאותו physical page. רק pages שצריך לכתוב אליהם הופכים ל-physical pages פרטיים ל-process, דרך Copy-on-Write וכדומה.
האם CreateFileMapping מקצה זיכרון ל-process באותו רגע?
CreateFileMapping יוצר file-mapping object, אבל MapViewOfFile הוא מה שממפה אותו למרחב הווירטואלי של ה-process. מעבר לזה, ה-physical pages של ה-view בדרך כלל נכנסים ל-RAM ב-Page Fault, מה-page הראשון שניגשים אליו.
מה ההבדל בין FILE_MAP_WRITE ל-FILE_MAP_COPY?
שינוי דרך FILE_MAP_WRITE הוא כתיבה שמתעדכנת בצד ה-file data המשותף. FILE_MAP_COPY משתף את ה-pages ההתחלתיים, אבל רק pages שנכתבים הופכים לעותק Private של ה-process; השינויים לא נכתבים בחזרה לקובץ המקורי, והם נעלמים כשמבטלים את המיפוי של ה-view.
אחרי Copy-on-Write, האם VirtualQuery מחזיר MEM_PRIVATE?
לא. data view נשאר MEM_MAPPED ו-image view נשאר MEM_IMAGE. כדי לדעת אם page באמת הפך ל-Private, צריך קודם לגרום לו להיות resident ואז לבדוק את סיבית Shared של QueryWorkingSetEx.
מותר לשמור raw pointer ב-shared memory?
בדרך כלל לא. גם על אותו section אין הבטחה שה-view של כל process יישב באותה virtual address. במבנה משותף משתמשים ב-offsets מבסיס ה-view, ב-integers ברוחב קבוע, ובפריסה ובסכימת סנכרון מפורשות.

פרופיל הכותב

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

Go Komura

מנהל KomuraSoft LLC

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

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

חזרה לבלוג