Windows Memory Internals (חלק 3) — Section objects ו-Copy-on-Write: מה באמת קורה ב-DLL וב-file mapping
· עודכן בתאריך: · Go Komura · 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: virtual addresses ו-Page Fault
עוקבים אחרי הרגע שבו virtual page שעבר Commit מקבל RAM פיזי. - חלק 2: מחזור החיים של physical page
עוקבים אחרי מעברי המצב של page שיוצא מ-Working Set. - חלק 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 יכולה להיות שונה.
flowchart LR
accTitle: מיפוי virtual addresses שונות לאותו physical page
accDescr: ל-Process A ול-Process B יש view כל אחד ב-virtual address שונה, אבל הם מגיעים לאותו physical page PFN X דרך אותו section offset
viewA["Process A: 0x000001A00000 + 0x3000"] --> offset["section offset 0x3000"]
viewB["Process B: 0x000002700000 + 0x3000"] --> offset
offset --> pfnX["אותו 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 מתפצלים לשני סוגים גדולים, לפי המקום שממנו אפשר לשחזר את התוכן.
flowchart TB
accTitle: איך file-backed ו-page-file-backed מתפצלים
accDescr: העברת קובץ אמיתי ל-CreateFileMapping יוצרת file-backed section, ו-clean page אפשר לקרוא מחדש מהקובץ המקורי. העברת INVALID_HANDLE_VALUE יוצרת page-file-backed section; ה-page file תומך בתוכן שנעלם בהריסת ה-object
create["CreateFileMapping"] -->|מעבירים handle של קובץ אמיתי| fileBacked["file-backed section"]
create -->|מעבירים INVALID_HANDLE_VALUE| pfBacked["page-file-backed section"]
fileBacked --> restore1["clean page אפשר לקרוא מחדש מהקובץ המקורי"]
pfBacked --> restore2["ה-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, וקריאות מצליחות כמו שהן.
flowchart LR
accTitle: מצב משותף לפני Copy-on-Write
accDescr: לפני שכתיבה מתרחשת, ה-PTE של Process A וה-PTE של Process B מגיעים שניהם לאותו shared page PFN X, וקריאות מצליחות כמו שהן
pteA["Process A PTE"] --> pfnX["PFN X משותף (read / copy-on-write)"]
pteB["Process B PTE"] --> pfnX
איור 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 עושה את הדברים הבאים.
- להשיג physical page אחד ל-process A.
- להעתיק את תוכן PFN X ל-page החדש PFN Y.
- להחליף את ה-PTE של process A ל-PFN Y.
- לשנות את ה-protection של process A לקריאה/כתיבה רגילה.
- לבצע מחדש את הוראת הכתיבה שנכשלה.
flowchart LR
accTitle: מצב מפוצל אחרי Copy-on-Write
accDescr: אחרי כתיבת Process A רק ה-PTE של Process A מוחלף ל-Private page PFN Y שקיבל עותק של התוכן, בעוד שה-PTE של Process B ממשיך להצביע ל-shared page המקורי PFN X
pteA2["Process A PTE"] --> pfnY["Private PFN Y (R/W, אחרי כתיבה)"]
pteB2["Process B PTE"] --> pfnX2["PFN X משותף (מקורי)"]
pfnX2 -.->|copy של התוכן בכתיבה| pfnY
איור 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 כבר קרה, משתמשים בהליך הבא.
- ניגשים ל-page היעד וגורמים לו להיות resident.
- מקבלים את מידע ה-Working Set של ה-page עם
QueryWorkingSetEx. - מסתכלים בסיבית
Shared. - אם
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 לקובץ dataSharedCacheMap: 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.
flowchart TB
accTitle: שלושה נתיבים שמתחברים לאותו file stream
accDescr: cached ReadFile/WriteFile משתמשים ב-SharedCacheMap, mapping fault של data משתמש ב-DataSectionObject, ו-image fault של EXE/DLL משתמש ב-ImageSectionObject; שלושתם מתחברים לאותו file stream דרך SECTION_OBJECT_POINTERS
cached["cached ReadFile / WriteFile"] --> scm["SharedCacheMap"]
dataFault["mapping fault על קובץ data"] --> dso["DataSectionObject"]
imageFault["image fault של EXE/DLL"] --> iso["ImageSectionObject"]
scm --> stream["אותו file stream (SECTION_OBJECT_POINTERS)"]
dso --> stream
iso --> stream
איור 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 קיימים.
- מפעילים Process Explorer כ-Administrator.
- מפעילים שני processes של
cmd.exe. - בוחרים View > Lower Pane View > DLLs.
- מאשרים את הנתיב וה-mapping של אותו DLL בשני ה-processes.
- פותחים כל
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 מחובר כזרימה אחת כזו.
מאמרים קשורים
- Windows Memory Internals (חלק 1) — הרגע שבו virtual address הופך ל-RAM פיזי: Page Fault מקצה לקצה
- Windows Memory Internals (חלק 2) — מחזור החיים של physical page: חמש רשימות והאמת על ה-page file
- The Depths of Windows I/O (Part 4) — Cache Manager: When Does Your WriteFile Actually Reach the Disk?
- מלכודות של shared memory ושיטות עבודה מומלצות בשטח
- Why EXCEL.EXE Processes Remain After C# Excel COM Automation — Reference Release Patterns and the Replacement Decision
- Process Explorer / Handle / VMMap in Practice — Chasing Hangs, Leaks, and “File in Use” from the State Right Now
תחומי ייעוץ קשורים
KomuraSoft LLC מטפלת בחקירת תקלות סביב shared memory, file mapping, טעינת DLL, נעילות קבצים, תקשורת בין-תהליכים ושימוש בזיכרון באפליקציות Windows.
קישורים
-
Microsoft Learn, Section Objects and Views. על section object שמייצג טווח זיכרון שניתן לשתף, ועל כל process שממפה חלק מה-section כ-view. ↩ ↩2 ↩3
-
Microsoft Learn, File-Backed and Page-File-Backed Sections. על file-backed sections ו-page-file-backed sections, CoW, והיכולת לשתף את אותו זיכרון פיזי מ-virtual addresses של processes שונים. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, CreateFileMappingW function. על ה-file-mapping object, page-file-backed sections,
SEC_IMAGE, ה-lifetime של views ו-handles, והעקביות בין views שתומכים באותו קובץ. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 -
Microsoft Learn, Memory Protection. על כמה processes שחולקים את ה-physical pages של אותו DLL, ועל CoW שמעתיק ל-physical page חדש ומעדכן את ה-PTE כשאחד הצדדים כותב. ↩ ↩2 ↩3
-
Microsoft Learn, SECTION_OBJECT_POINTERS structure. על DataSectionObject, SharedCacheMap ו-ImageSectionObject שקושרים mapping של file stream ומידע cache ל-Memory Manager / Cache Manager. ↩ ↩2 ↩3 ↩4
-
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 -
Microsoft Learn, VirtualQuery function. על כך ש-Type נשאר
MEM_MAPPED/MEM_IMAGEאחרי CoW, ושאפשר לאשר הפיכה ל-Private עם סיבית Shared שלQueryWorkingSetEx. ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, Managing Memory Sections. על כך שזיכרון פיזי לא מוקצה עד שניגשים ל-view, ועל ה-Page Fault של הגישה הראשונה שקורא את תוכן הקובץ. ↩
-
Microsoft Learn, Sharing Files and Memory. על שיתוף אותו file-mapping object בשם או ב-handle; יצירת page-file-backed shared memory עם
INVALID_HANDLE_VALUE; ועל כך שסנכרון נדרש בנפרד. ↩ ↩2 ↩3 -
Microsoft Learn, Process Explorer - Sysinternals. על כך ש-Process Explorer יכול להציג handles של process ו-DLL / memory-mapped files שנטענו. ↩
-
Microsoft Learn, VMMap - Sysinternals. על כך ש-VMMap מפרק את הזיכרון הווירטואלי של process ל-Image, Mapped File, Private וכדומה, ומציג את פירוק Private/Shareable של ה-Working Set. ↩
מאמרים קשורים
מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.
DllMain ו-loader lock — הסיבה האמיתית שאומרים לא לעשות כלום ב-initialization של DLL
למה אסור לקרוא ל-LoadLibrary או להסתנכרן עם threads מתוך DllMain. המאמר מסביר ממקורות ראשוניים איך loader lock מסדר כל DLL notification, ...
מנגנון הזיכרון של Windows (חלק 2) — מחזור החיים של דף פיזי: חמש רשימות והאמת על ה-pagefile
המאמר מחבר את ה-PFN database, Standby, Modified, דחיסת זיכרון וה-pagefile, ומסביר לאן עובר דף פיזי אחרי שהוא יוצא מ-Working Set.
Page Fault ב-Windows (חלק 1): מתי כתובת וירטואלית מקבלת RAM פיזי
VirtualAlloc, VAD, page table, TLB, demand-zero ו-hard fault מתחברים לרגע שבו כתובת וירטואלית מקבלת דף פיזי. המאמר עוקב אחרי הגישה הראשונ...
איך קיצור דרך ב-Windows מוצא קובץ שהעברתם? — המיקום של הקובץ והזהות שלו הם שני דברים שונים
העברתם את הקובץ המקורי, ובכל זאת הקיצור פותח אותו. Windows מחפש את היעד לא רק לפי הנתיב השמור, אלא גם לפי מזהי מעקב ומאפיינים של הקובץ, ו...
האם עדיין צריך "הסרה בטוחה" של כונן USB? — מבט מ"הסרה מהירה" ומטמון הכתיבה
אפשר לשלוף כונן USB ברגע שההעתקה הסתיימה? ההבדל בין מטמון הכתיבה, "הסרה מהירה" ו"ביצועים גבוהים" מסביר מה תפקידה של ההסרה הבטוחה. המאמר כ...
נושאים קשורים
העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.
נושאים טכניים ב-Windows
שער לנושאי פיתוח Windows, חקירת תקלות וניצול נכסים קיימים.
שירותים הקשורים לנושא הזה
המאמר קשור ישירות לשירותים הבאים.
פיתוח יישומי Windows
יישומים עסקיים, חיבור התקנים וכלי תקשורת, מהגדרת הדרישות ועד הפיתוח.
שאלות נפוצות
שאלות נפוצות בפניות בנושא המאמר.
- האם כל 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 ברוחב קבוע, ובפריסה ובסכימת סנכרון מפורשות.