היסטוריית עדכונים (גרסה ראשונה, פורסמה בתאריך 29 Jul 2026)
- פרסום ראשון
לצטט את המאמר הזה(DOI: 10.5281/zenodo.22175314)
מאמר זה מאוחסן בארכיון Zenodo. להלן גם ה-DOI שתמיד מפנה לגרסה האחרונה וגם ה-DOI המקובע לגרסה שאתם קוראים.
Go Komura (2026). מעמקי ה-I/O ב-Windows (פרק 4) — Cache Manager: מתי WriteFile באמת מגיע לדיסק. KomuraSoft LLC. https://doi.org/10.5281/zenodo.22175314 https://comcomponent.com/he/blog/windows-cache-manager-writefile-disk/
- DOI (הגרסה האחרונה)
- 10.5281/zenodo.22175314
- DOI (הגרסה הזו)
- 10.5281/zenodo.22175315
WriteFile החזיר הצלחה. אז איפה הנתונים עכשיו?
התשובה, כמעט בוודאות: עדיין לא בדיסק. הם רק הועתקו למטמון בזיכרון. לכן קורה “שמרתי, ואחרי ניתוק חשמל זה נעלם”; לכן מדדי העתקת קבצים מציגים מהירויות שאינן אפשריות פיזית; ולכן מסדי נתונים קוראים באדיקות ל-fsync.
פרק 4 בסדרה “מעמקי ה-I/O ב-Windows” עוסק במה שעומד באמצע: Cache Manager. בפרק 2 כתבנו ש”אם הנתונים כבר במטמון, גם I/O אסינכרוני מסתיים באופן סינכרוני”, ובפרק 1 השארנו כשיעורי בית את “הדרך הקצרה שאינה בונה IRP: Fast I/O”. הפעם סוגרים את שני החוטים האלה.
1. קודם המסקנה
- מטמון הקבצים של Windows הוא write-back. קריאה קודם ממטמון הקבצים של המערכת, וכתיבה קודם אל המטמון. השתקפות לדיסק נעשית אחר כך בידי ה-OS.1
- המטמון עצמו הוא מיפוי קובץ. Cache Manager ממפה מקטעים של 256KB מהקובץ למרחב הכתובות של המערכת, וקריאה וכתיבה הן “העתקת זיכרון מול ה-view הזו” (פרק 2).1
- כתיבה נרדפת בידי lazy writer שרץ פעם בשנייה. קריסת אפליקציה לא מאבדת נתונים, אבל בניתוק חשמל או קריסת OS, dirty cache אובד (פרק 4).1
- שלושה כלים ל”לכתוב בוודאות”.
FlushFileBuffers(=Flush(true)ב-.NET),FILE_FLAG_WRITE_THROUGH,FILE_FLAG_NO_BUFFERING. flush בכל כתיבה לא יעיל, והתיעוד הרשמי מצביע על שילוב NO_BUFFERING+WRITE_THROUGH (פרק 5).21 - ל-NO_BUFFERING יש דרישות יישור. גודל והיסט הם כפולה של גודל sector, וכתובת buffer מיושרת לגבול sector פיזי. גם עם NO_BUFFERING, metadata ממשיך להיות במטמון (סעיף 5.3).31
- mapped view והמטמון חולקים את אותם נתונים. memory-mapped file ו-I/O רגיל עם מטמון עקביים; persistence של map הוא שני שלבים:
FlushViewOfFileואזFlushFileBuffers(פרק 6).45 - קריאה וכתיבה סינכרוניות שעלו למטמון לפעמים לא בונות בכלל IRP. זו הדרך הקצרה שנקראת Fast I/O, ישר אל Cache Manager: התשובה לשיעורי הבית מפרק 1 (פרק 7).6
ב-diagram, solid line מציינת relation שתמיד מתקיים ו-dashed line מציינת relation מותנה (התנאים מופיעים בהסבר של כל relation ב-detail page). הרשימה המלאה של ה-relations (סה”כ 32, כולל evidence ו-certainty) וההגדרות של ה-concepts המרכזיים נמצאות ב-detail page של ה-knowledge map (ביפנית). Data: JSON-LD / Turtle
2. מה המטמון באמת: הקובץ ממופה לזיכרון
2.1. משבצות של 256KB והעתקת זיכרון
אם חושבים על מטמון הקבצים של Windows כ”מיכל של בלוקי דיסק”, הרבה מההתנהגות מפסיקה להיות ניתנת להסבר. התמונה הנכונה היא זו: Cache Manager ממפה מקטעים של 256KB מהקובץ ל”משבצות” במרחב הכתובות של המערכת, וקריאה וכתיבה עם מטמון דלוק רצות כהעתקת זיכרון בין המשבצת הזו לבין ה-buffer של האפליקציה.1
flowchart TB
subgraph U["אפליקציה (user mode)"]
BUF["buffer של האפליקציה<br/>(האזור שהועבר ל-ReadFile/WriteFile)"]
end
subgraph S["מרחב הכתובות של המערכת"]
SLOT["מטמון הקבצים של המערכת<br/>משבצת שממפה מקטע 256KB מהקובץ"]
end
DISK[("הקובץ בדיסק")]
BUF <-->|"ReadFile/WriteFile =<br/>העתקת זיכרון מול המשבצת"| SLOT
SLOT <-->|"קריאה בגישה ראשונה וכתיבה חזרה<br/>אחר כך, לפי דפים"| DISK
איור 1: I/O עם מטמון דלוק במציאות. “קריאה וכתיבה לקובץ” מצד האפליקציה הן לרוב רק העתקת זיכרון
נקודה אחת שקל לטעות בה. 256KB הוא גודל ה-view (המיפוי), לא אומר ש-I/O לדיסק תמיד רץ ביחידות של 256KB. דפים בתוך המשבצת נטענים לפי הצורך, וכמות ה-I/O שבאמת יוצאת לדיסק תלויה בגודל הבקשה ובדפוס הגישה. במקטע שנקרא בפעם הראשונה יש I/O לדיסק כדי למלא (כאן IRP מפרק 1 יורד למחסנית האחסון). אם כבר במטמון, הקריאה מסתיימת בהעתקה בלבד. מה שראינו בפרק 2 פרק 5, “cache hit גורם גם להנפקה אסינכרונית להסתיים סינכרונית”, הוא הביטוי של “בקשה שאפשר לענות עליה במקום מסתיימת במקום”. ולהפך: אם המטמון דלוק אבל הדף לא בזיכרון, אין מנגנון אסינכרוני לטיפול ב-page fault, ולכן קריאה אסינכרונית לפעמים מעובדת באופן סינכרוני, כמו שראינו בפרק 2.7
2.2. מה באמת קורה כש”הזיכרון הפנוי ירד”
האם משתמשים במטמון ומצב ה-read-ahead מנוהלים לפי אופן הפתיחה (לפי file object),1 אבל גוף הנתונים שבמטמון משותף לפי קובץ (stream). כמה פעמים שתפתחו את אותו קובץ, לא נוצרים מטמונים נפרדים; מכל handle רואים את אותו תוכן מטמון (זה הבסיס לעקביות בפרק 6). המטמון רץ כל עוד Windows רצה, תחת Cache Manager.1 העתקת קובץ גדול או הרבה קריאה וכתיבה ממירות זיכרון פיזי פנוי למטמון. גם אם במנהל המשימות נראה שפחות זיכרון פנוי, הרבה מזה הוא “זיכרון בהמתנה בשימוש בעל ערך, שאפליקציה תקבל אותו די מהר אם תבקש”. כדי לא לטעות בהבחנה הזו באבחון מחסור בזיכרון, יש גם את “איך מבחינים ב-.NET בין המתנה ל-GC לבין memory leak”.
אפשר לראות את זה על המסך. מנהל המשימות > ביצועים > זיכרון, בסרגל “הרכב הזיכרון” למטה: בשימוש / שונה / standby / פנוי. רוב מטמון הקבצים נכנס ל-standby, וברשימה מימין זה מסוכם כ”במטמון”. לפירוט יותר דק, Monitor משאבים > לשונית זיכרון, עם אותן קטגוריות ועם נפחים. אחרי שמעתיקים קובץ של כמה GB וחוזרים להסתכל, standby עולה והפנוי יורד, אבל “בשימוש” כמעט לא זז. כלומר לא “הזיכרון נאכל”, אלא “זיכרון שהיה פנוי הלך למטמון”.
3. read-ahead: ספקולציה בקריאה
Cache Manager קורא מראש מקטעים שנראים כמו הצעד הבא, לפי דפוס הגישה (read-ahead). בקובץ שנקרא לפי הסדר, ההמשך כבר במטמון לפני שהאפליקציה ביקשה. זה מה שמסביר למה קריאה סדרתית מהירה. כמות ה-read-ahead לא קבועה; היא משתנה לפי הדפוס שזוהה ולפי גודל הבקשה.
flowchart LR
A["היסטוריית בקשות הקריאה של האפליקציה<br/>קוראים מההתחלה לפי הסדר"]
D{"Cache Manager מזהה דפוס"}
R["read-ahead: מקטע ההמשך נטען<br/>לפני שהתבקש<br/>(הכמות משתנה לפי דפוס וגודל בקשה)"]
H1["רמז FILE_FLAG_SEQUENTIAL_SCAN<br/>= read-ahead אגרסיבי יותר"]
H2["רמז FILE_FLAG_RANDOM_ACCESS<br/>= read-ahead מבוזבז, מדכאים אותו"]
A --> D
D --> R
H1 -.-> D
H2 -.-> D
איור 2: read-ahead. בנוסף לזיהוי דפוס גישה, אפשר לתת רמז ב-flags של CreateFile
FileOptions.SequentialScan / RandomAccess מטבלת המיפוי בפרק 1 הם רמזים למנוע ה-read-ahead הזה. לעיבוד אצווה ש”עובר על הכול” הראשון, לגישה שעוקבת אחרי אינדקס השני. אם חושבים על זה כ-flags שבהם האפליקציה מספרת ל-OS על עתיד שרק היא יודעת, ברור מתי להשתמש.
4. כתיבה מושהית: מה אומר “הצלחה” של WriteFile
4.1. lazy writer מגיע כל שנייה
בצד הכתיבה זה write-back cache. WriteFile מחזיר הצלחה ברגע שהעתיק נתונים למשבצת, והשתקפות לדיסק נדחית. המדיניות הזו נקראת lazy writing.1
מי שמבצע את ההשתקפות הוא lazy writer ש-Cache Manager מפעיל כל שנייה. הוא שם בתור שמינית מהדפים שלא עברו flush לאחרונה, ואם יש הרבה מה לכתוב הוא מוסיף עוד. קובץ זמני שנוצר עם FILE_ATTRIBUTE_TEMPORARY יוצא מיעד ה-flush של lazy writer: אין טעם לכתוב משהו שמניחים שיימחק מיד.1 אבל זה רמז לפי attribute, לא הבטחה. אם הזיכרון לוחץ, עדיין יכול להיות write-back, וזה לא חל על קובץ שרק “נשמע זמני” בשם.
sequenceDiagram
participant App as אפליקציה
participant C as מטמון המערכת
participant LW as lazy writer (כל שנייה)
participant D as דיסק
App->>C: WriteFile(נתונים)
Note over C: מעתיק למשבצת<br/>ומסמן את הדף dirty (עדיין לא נכתב)
C-->>App: TRUE חוזר מיד
Note over App,C: מכאן עד ה-write-back זה חלון מסוכן<br/>ניתוק חשמל או קריסת OS מוחקים את הנתונים האלה
LW->>C: בוחר 1/8 מה-dirty pages
LW->>D: כותב חזרה ביחד
Note over D: כאן לראשונה זה נהיה durable
איור 3: כתיבה מושהית. הצלחת WriteFile היא “נמסר ל-OS”, לא “נהיה durable”
4.2. מה קורה, ומה נעלם, לפי סוג הכשל
מחדדים מה “חלון מסוכן” אומר. סוג הכשל מפצל את הגורל.
flowchart TB
W["נתונים מיד אחרי הצלחת WriteFile<br/>(dirty page במטמון)"]
Q{"מה קרה"}
A1["תהליך האפליקציה קרס<br/>או נעצר בכוח"]
A2["כל ה-OS נעצר<br/>(ניתוק חשמל, מסך כחול)"]
S["הנתונים נשארים<br/>המטמון שייך ל-OS<br/>lazy writer כותב חזרה כמתוכנן"]
L["dirty pages אובדים<br/>נשאר רק מה שכבר הגיע לדיסק"]
W --> Q
Q --> A1
Q --> A2
A1 --> S
A2 --> L
איור 4: סוג הכשל וקו החיתוך של הישרדות. המטמון שייך ל-OS, לא לתהליך
- אם האפליקציה מתה, הנתונים לא נעלמים. ברגע שההעתקה למטמון הסתיימה, הבעלים הוא ה-OS. “שמרתי, האפליקציה נפלה מיד, והקובץ שלם” קורה בגלל זה.
- אם כל ה-OS מת, החלק ה-dirty נעלם. תדירות ה-flush מכוונת כ-trade-off בין ביצועים לאמינות, והתיעוד כותב במפורש ש”באיבוד חשמל פתאומי, נתונים שבמטמון אובדים”.1
כלומר השאלה בתכנון אפליקציה עסקית היא: “את הנתונים האלה מותר לאבד ברגע של ניתוק חשמל?” כמה שניות של לוג אולי בסדר. רשומת הזמנה סופית כנראה לא. רק על מה שאי אפשר להרשות להפסיד שמים את הכלים בפרק הבא.
5. ארגז כלים ל”נכתב בוודאות”
5.1. FlushFileBuffers: לכתוב עד הסוף עכשיו
FlushFileBuffers כותב עד הסוף אל ההתקן את הנתונים שנאגרו לקובץ שצוין. metadata של מערכת הקבצים תמיד במטמון, ולכן כדי להביא גם metadata בוודאות צריך flush (או WRITE_THROUGH).12 ב-.NET המקבילה היא FileStream.Flush(true) (Flush() לבד מעביר רק את ה-buffer הפנימי של .NET ל-OS, ומטמון ה-OS נשאר כמו שהוא).8
אבל התיעוד הרשמי נועץ מסמר במפורש: לקרוא לזה בכל כתיבה זה לא יעיל. אם צריך persistence בכל אחת מכתיבות רבות, כדאי NO_BUFFERING+WRITE_THROUGH שבהמשך.2
5.2. FILE_FLAG_WRITE_THROUGH: מורידים רק את ההשהיה
פתיחה עם FILE_FLAG_WRITE_THROUGH כותבת גם למטמון, וגם מיד לדיסק בלי לחכות ל-lazy writer.1 הנקודה היא שקריאות ממשיכות ליהנות מהמטמון. זו התשובה הישירה ל”רוצים קריאה מהירה, ובלי השהיית כתיבה”.
5.3. FILE_FLAG_NO_BUFFERING: בלי לעבור במטמון
FILE_FLAG_NO_BUFFERING מוציא את מטמון המערכת עצמו מקריאה וכתיבה. כל קריאה וכתיבה הופכות ל-I/O אל התקן הדיסק, בלי לעבור במטמון.1 אבל מה שאפשר לעקוף הוא רק עד מטמון Windows. כמו באיור 5, write cache בתוך ההתקן הוא שלב נפרד. אם צריך עמידות גם לניתוק חשמל, עדיין צריך WRITE_THROUGH ביחד או FlushFileBuffers. זה כלי להעברת נתונים גדולה בבת אחת, או למנוע מסד נתונים שמנהל buffers בעצמו, ויש לו התחייבויות קשיחות.3
- גודל והיסט הקובץ של קריאה וכתיבה הם כפולה שלמה של גודל ה-sector של ה-volume (ב-sector של 512 בתים: 512, 1024, 1536…).
- גם כתובת ה-buffer מיושרת לגודל sector פיזי (צריך להתחשב גם בדיסקי Advanced Format עם sector פיזי 4096 בתים).
- גם אז metadata ממשיך להיות במטמון, ולכן persistence מלא צריך WRITE_THROUGH ביחד או
FlushFileBuffers.12
ה”התחייבות” הזו היא המקום הראשון שבו נופל מי שרק מוסיף flag. קריאה וכתיבה בלי יישור נכשלות ב-ERROR_INVALID_PARAMETER (87). שלוש הנקודות שצריך ליישר:3
| מה מיישרים | התנאי | איך ממלאים |
|---|---|---|
| גודל הקריאה/כתיבה | כפולה של גודל ה-sector של ה-volume | לוקחים lpBytesPerSector מ-GetDiskFreeSpace ומעגלים לכפולה |
| היסט הקובץ | אותו דבר (גם כשמציינים ב-Offset של OVERLAPPED) |
מתקדמים בכפולות של גודל sector |
| כתובת ה-buffer | יישור לגודל sector פיזי | מקצים ב-VirtualAlloc (חוזר אזור מיושר לגבול דף, בדרך כלל 4096 בתים) |
את השלישית הכי קל לפספס. לכתובת ש-malloc, new או מערך C# מחזירים אין הבטחת יישור לגבול sector. VirtualAlloc שמקצה על גבול דף ממלא גם את דרישת דיסק Advanced Format עם sector פיזי 4096. הצורה המינימלית:
// C++ / Win32. טיפול בשגיאות מצומצם בכוונה
DWORD sectorsPerCluster = 0, bytesPerSector = 0, freeClusters = 0, totalClusters = 0;
if (!GetDiskFreeSpaceW(L"C:\\", §orsPerCluster, &bytesPerSector,
&freeClusters, &totalClusters))
{
return GetLastError();
}
// יחידת קריאה/כתיבה ככפולה של גודל sector (כאן בערך 1MiB)
const DWORD chunk = (1024 * 1024 / bytesPerSector) * bytesPerSector;
// buffer באזור מיושר לגבול דף (malloc/new לא מבטיחים את זה)
BYTE* buffer = static_cast<BYTE*>(
VirtualAlloc(nullptr, chunk, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE));
if (buffer == nullptr) { return GetLastError(); }
HANDLE h = CreateFileW(L"C:\\temp\\big.bin", GENERIC_READ, FILE_SHARE_READ, nullptr,
OPEN_EXISTING, FILE_FLAG_NO_BUFFERING, nullptr);
if (h == INVALID_HANDLE_VALUE)
{
// GetLastError מחזיר את תוצאת קריאת Win32 האחרונה. אם קוראים קודם
// ל-VirtualFree, סיבת הכשל של CreateFileW (גישה נדחתה, נתיב לא קיים)
// נדרסת בתוצאת הניקוי, וחוזר רק קוד בלי סיבה
const DWORD err = GetLastError();
VirtualFree(buffer, 0, MEM_RELEASE);
return err;
}
// מתקדמים כל פעם ב-chunk בתים, אז גם הגודל וגם ההיסט נשארים מיושרים
DWORD read = 0;
while (ReadFile(h, buffer, chunk, &read, nullptr) && read > 0)
{
// מעבדים את read הבתים הראשונים ב-buffer
// (בסוף הקובץ read < chunk. זה תקין)
}
CloseHandle(h);
VirtualFree(buffer, 0, MEM_RELEASE);
שימו לב: ב-FileOptions של .NET אין ערך שמקביל ל-FILE_FLAG_NO_BUFFERING. אם באמת צריך, קוראים ל-CreateFile ישירות, וגם אז את דרישות היישור ממלאים בעצמכם. סדר הבדיקה הוא “מנהלים buffers בעצמנו, לכן NO_BUFFERING”, לא “רוצים מהר, לכן NO_BUFFERING”.
5.4. איך בוחרים
flowchart TB
A["buffer של האפליקציה"]
B["מטמון הקבצים של המערכת<br/>(dirty pages)"]
C["cache בתוך התקן הדיסק"]
D[("מדיה לא נדיפה")]
A -->|"WriteFile כברירת מחדל: הצלחה חוזרת עד כאן"| B
B -->|"lazy writer (כל שנייה) / WRITE_THROUGH (מיידי)"| C
C -->|"תזמון ההתקן /<br/>FlushFileBuffers דורש כתיבה עד הסוף"| D
A -.->|"NO_BUFFERING מדלג על המטמון והולך ישר"| C
איור 5: שכבות הנתונים, ועד לאן כל כלי דוחף. שימו לב גם לשלב האחרון: cache בתוך התקן הדיסק
| שיטה | מה קורה | למה זה מתאים |
|---|---|---|
| ברירת מחדל (מטמון דלוק) | מסתיים בהעתקה למטמון. ההשתקפות אצל lazy writer | רוב ה-I/O לקבצים |
FlushFileBuffers / Flush(true) |
כותב עד הסוף את הנתונים וה-metadata של אותו רגע | נקודת אישור (commit של טרנזקציה וכו’) |
FILE_FLAG_WRITE_THROUGH |
בכל כתיבה ישר לדיסק (קריאה נשארת במטמון) | לוג / journal עם כתיבות שאי אפשר לאבד |
FILE_FLAG_NO_BUFFERING (+WRITE_THROUGH) |
בלי מטמון. יש דרישות יישור | ניהול buffers עצמי, I/O גדול בבת אחת |
סדר הבחירה הוא שני שלבים: קודם מחליטים כמה רשומות מותר לאבד בניתוק חשמל, ואז בודקים כמה האטה מותרת בשביל זה. לא מעיינים בטבלה מלמעלה למטה, אלא הולכים אחרי הפיצול הזה.
flowchart TB
S["עומדים לכתוב את הנתונים האלה"]
Q1{"מותר לאבד אותם ברגע של<br/>ניתוק חשמל או מסך כחול?"}
A0["ברירת מחדל (מטמון דלוק)<br/>הכי מהיר. רוב ה-I/O כאן"]
Q2{"מה שאי אפשר לאבד הוא<br/>נקודת ציון, או כל רשומה?"}
A1["FlushFileBuffers בנקודת ציון<br/>ב-.NET: Flush(true)<br/>העלות: המתנה רק בנקודת הציון"]
Q3{"מנהלים buffers בעצמכם<br/>וממלאים את דרישות היישור מ-5.3?"}
A2["FILE_FLAG_WRITE_THROUGH<br/>בכל כתיבה ישר לדיסק<br/>קריאה נשארת מהירה במטמון"]
A3["FILE_FLAG_NO_BUFFERING<br/>+ FILE_FLAG_WRITE_THROUGH<br/>הצורה שהתיעוד מציין ל-persistence תכוף"]
S --> Q1
Q1 -->|"מותר<br/>(לוג של כמה שניות אחרונות)"| A0
Q1 -->|"אסור"| Q2
Q2 -->|"נקודת ציון<br/>(אישור עסקה וכו')"| A1
Q2 -->|"כל רשומה"| Q3
Q3 -->|"לא (אפליקציה רגילה)"| A2
Q3 -->|"כן (מנוע DB וכו')"| A3
איור 6: איך בוחרים כלי. הפיצול הראשון הוא דרישת אמינות, השני הוא העלות שאפשר לשלם. אין מסלול של FlushFileBuffers בכל רשומה, כי כמו ב-5.1 התיעוד הרשמי רואה בזה חוסר יעילות
שלושה דפוסים מעשיים בלבד.
- “כותבים לקובץ זמני → flush → rename” הוא הכלל שלא משאיר קובץ חצי-שבור. כותבים את התוכן עד הסוף, ורק אז מקבעים בשם. המסירה האטומית הזו מפורטת ב”יסודות נעילה בשיתוף קבצים”.
- להשאיר את זה למסד נתונים זו גם תכנון לגיטימי. איך SQLite בונה durability עם WAL ו-flush מכוסה ב”SQLite באפליקציה עסקית ב-C#”. האפשרות “לא לכתוב בעצמכם אסטרטגיית flush” תמיד קיימת.
- ב-benchmark חושדים במטמון. מדידה ש”הקריאה מהירה מדי” בדרך כלל מודדת cache hit מהפעם השנייה ואילך. שיטת המדידה מרוכזת ב”איך משווים נכון מהירויות בין גרסאות תוכנית ב-Windows”.
ואל תשכחו את השלב האחרון באיור 5: cache בתוך התקן הדיסק. FlushFileBuffers דורש כתיבה עד הסוף כולל אותו שלב, אבל ב-USB ובדיסק חיצוני נכנסת גם מדיניות ה-write cache של ההתקן (“הסרה מהירה” מול “ביצועים גבוהים”). לטיפול בהתקנים נשלפים ראו גם “איך מטפלים בהתקן USB באפליקציית Windows”.
6. עקביות מול memory-mapped file
בפרק 1 שמעתם ש”המטמון עצמו הוא מיפוי קובץ”, ובטח עלתה השאלה: אז view שפתחתם ב-MapViewOfFile והמטמון של ReadFile/WriteFile לא רבים?
לא. כי הם יושבים על אותו מנגנון. file-mapping object נשען על הקובץ, ודחיקת דף מתבצעת ככתיבה חזרה לקובץ. גם אם כמה תהליכים יוצרים views לאותו קובץ מקומי, התוכן שנראה coherent.4
flowchart TB
subgraph P1["מרחב הכתובות של תהליך A"]
V1["view של MapViewOfFile"]
end
subgraph SYS["מרחב הכתובות של המערכת"]
SC["view של Cache Manager<br/>(המשבצת ש-ReadFile/WriteFile משתמשים בה)"]
end
PAGES["אותם דפים פיזיים<br/>(זיכרון שנשען על הקובץ)"]
DISK[("הקובץ בדיסק")]
V1 --> PAGES
SC --> PAGES
PAGES --> DISK
NB["I/O עם FILE_FLAG_NO_BUFFERING<br/>מחוץ לשיתוף הזה (ישר לדיסק)"]
NB -.-> DISK
איור 7: גם mapped view וגם המטמון רואים את אותם “דפים שנשענים על הקובץ”. מחוץ למסגרת נמצא רק NO_BUFFERING
שתי נקודות זהירות.
- I/O עם
FILE_FLAG_NO_BUFFERINGמחוץ למסגרת העקביות הזו. קריאה וכתיבה שלא עוברות במטמון לא מושוות לתוכן של mapped view / מטמון. אם מערבבים, אתם אחראים ליישור. - persistence של mapped view הוא שני שלבים.
FlushViewOfFileמתחיל כתיבה של dirty pages בטווח, אבל לא כותב metadata ולא ממתין לכתיבה פיזית מתוך cache החומרה של הדיסק. כדי להביא בוודאות קוראים ל-FlushFileBuffersאחריFlushViewOfFile.5
הפרקטיקה של file mapping כזיכרון משותף (שיתוף בשם, סנכרון, דפוסי תקלה) מכוסה ב”מלכודות של shared memory ופרקטיקה בשטח”.
7. Fast I/O: סוגרים את שיעורי הבית מפרק 1
קודם בשתי שורות. Fast I/O היא דרך קצרה לקריאה וכתיבה סינכרוניות לקובץ שכבר במטמון: מעבירים נתונים מול המטמון בלי לבנות IRP (I/O Request Packet, המיכל שה-kernel מוסר לנהג). בעמודת Operation ב-Procmon מופיעים יחד IRP_MJ_READ ו-FASTIO_READ כי אותה “קריאה” עברה בנתיב הרגיל או בדרך הקצרה.
בסעיף 5.2 בפרק 1 כתבנו ש”לא כל I/O הופך ל-IRP”. הנה התשובה.
לקריאה וכתיבה של קובץ שכבר במטמון לא חייבים לבנות IRP ולהזרים אותו במחסנית ההתקנים; מספיקה העתקת זיכרון מול המטמון. לכן Windows מכינה Fast I/O ל-I/O סינכרוני לקובץ שבמטמון: בלי IRP, קריאה ישירה ל-fast I/O entry point של מערכת הקבצים, והעתקה ישירה מ-Cache Manager.6 אם Fast I/O לא יכול לטפל (אין במטמון, יש נעילה, filter מתערב וכו’) חוזרים לנתיב IRP הרגיל. וזו דרך מהירה לבקשה סינכרונית, לא “cache hit = תמיד Fast I/O”. פעולה על handle אסינכרוני (FILE_FLAG_OVERLAPPED) יכולה לעבור בנתיב IRP גם כשהיא מסתיימת במקום מהמטמון (פרק 2 פרק 5).
flowchart TB
REQ["קריאה וכתיבה סינכרוניות ל-handle עם מטמון דלוק"]
Q{"אפשר לטפל ב-Fast I/O<br/>(הקובץ במטמון וכו')"}
FAST["Fast I/O<br/>העתקה ישירה מול המטמון בלי IRP<br/>ב-Procmon מופיע כ-FASTIO_"]
IRP["נתיב רגיל<br/>בונים IRP ושולחים למחסנית ההתקנים<br/>(עולם איור 6 בפרק 1)"]
REQ --> Q
Q -->|אפשר| FAST
Q -->|אי אפשר| IRP
איור 8: פיצול Fast I/O. לכן ב-Procmon מתערבבים FASTIO_READ ו-IRP_MJ_READ
זה מסביר למה בתצפית Procmon בפרק 1 פרק 7 היו שורות FASTIO_. בקריאת cache hit סינכרונית, אפילו IRP הוא מותרות. קיום הנתיב הזה משפיע גם על filter drivers בפרק 6 (minifilter יכול להתערב גם ב-Fast I/O).
8. סיכום
- מטמון הקבצים של Windows הוא write-back, והגוף הוא מיפוי מקטעי 256KB מהקובץ. קריאה וכתיבה עם מטמון דלוק הן העתקת זיכרון מול המשבצת.1
- בקריאה יש read-ahead ספקולטיבי, ו-
SequentialScan/RandomAccessהם הרמזים שלו.1 - בכתיבה lazy writer כל שנייה רודף אחריכם. אם האפליקציה מתה הנתונים נשארים; אם כל ה-OS מת, רק החלק ה-dirty נעלם. שאלת התכנון היא “מותר לאבד את הנתונים האלה ברגע של ניתוק חשמל?”.1
- כלים לכתיבה ודאית:
FlushFileBuffers(אישור בנקודת ציון) /WRITE_THROUGH(בכל כתיבה) /NO_BUFFERING(בלי מטמון + דרישות יישור). flush בכל כתיבה לא יעיל, ול-persistence תכוף ההמלצה הרשמית היא NO_BUFFERING+WRITE_THROUGH. שימו לב גם ש-metadata תמיד במטמון.231 - mapped view והמטמון חולקים דפים ועקביים. מחוץ למסגרת רק NO_BUFFERING. persistence של map הוא שני שלבים:
FlushViewOfFile+FlushFileBuffers.45 - קריאה וכתיבה סינכרוניות ב-cache hit מדלגות אפילו על IRP ב-Fast I/O. זה מה שראיתם כ-
FASTIO_ב-Procmon בפרק 1.6
ההמשך בפרק 5, “המבנה הפנימי של NTFS: מערכת קבצים מתוך הבנת MFT”. עד כאן טיפלנו בקובץ כ”היסט ורצף בתים”. מאחורי זה NTFS מסדר נתונים: MFT, כמה data streams, journal, hard link. יורדים למבנה הסטטי על הדיסק.
מאמרים קשורים
- מעמקי ה-I/O ב-Windows (פרק 1) — כל קריאה וכתיבה הופכות ל-IRP: תמונת המערכת
- מעמקי ה-I/O ב-Windows (פרק 2) — I/O סינכרוני ואסינכרוני: המשמעות האמיתית של OVERLAPPED
- מעמקי ה-I/O ב-Windows (פרק 3) — I/O completion port (IOCP) ו-thread pool של .NET: המרתף של async/await
- יסודות נעילה בשיתוף קבצים: file lock ו-claim אטומי
- מלכודות של shared memory ופרקטיקה בשטח
- SQLite באפליקציה עסקית ב-C#: מצב WAL, נעילה, הגנה מפני קלקול, והבחנה מול EF Core
- איך משווים נכון מהירויות בין גרסאות תוכנית ב-Windows
- איך מטפלים בהתקן USB באפליקציית Windows: COM וירטואלי, HID, WinUSB ובחירת SDK ייעודי
תחומי ייעוץ קשורים
ב-KomuraSoft LLC מטפלים בתכנון ובחקירת I/O לקבצים באפליקציות עסקיות ל-Windows: מקרים כמו “נתונים שחשבתי ששמרתי נעלמו” או “כתיבה לקובץ איטית, או מהירה באופן חשוד”.
מקורות
-
Microsoft Learn, File Caching. Windows כברירת מחדל שמה במטמון נתוני קבצים: קריאה ממטמון הקבצים של המערכת, וכתיבה גם היא למטמון (write-back). המטמון מנוהל לפי file object ורץ תחת Cache Manager. מדיניות לדחות כתיבה לדיסק ולהחזיק במטמון נקראת lazy writing. בקריאת קובץ מקטע של 256KB נקרא למשבצת 256KB במרחב הכתובות של המערכת, ותהליך המשתמש מעתיק נתונים מול המשבצת. Cache Manager מפעיל כל שנייה lazy writer ושם בתור הכתיבה לדיסק שמינית מהדפים שלא עברו flush לאחרונה, ובמידת הצורך מוסיף עוד. קבצים זמניים לא עוברים flush. בכשל מערכת פתאומי כמו איבוד חשמל, נתוני מטמון שלא נכתבו אובדים. גם כשמבטלים מטמון ב-FILE_FLAG_NO_BUFFERING, metadata של הקובץ עשוי להישאר במטמון. ב-FILE_FLAG_WRITE_THROUGH הנתונים נכתבים גם למטמון וגם מיד לדיסק בלי השהיית lazy writer. metadata של מערכת הקבצים תמיד במטמון, ולכן persistence של metadata דורש flush או FILE_FLAG_WRITE_THROUGH. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15 ↩16 ↩17 ↩18 ↩19
-
Microsoft Learn, FlushFileBuffers function. WriteFile בדרך כלל כותב ל-buffer פנימי וה-OS כותב מדי פעם לדיסק. FlushFileBuffers כותב אל ההתקן את כל המידע שנאגר לקובץ שצוין. קריאה בכל אחת מכתיבות רבות לא יעילה, ולאפליקציות שצריכות persistence של נתונים חשובים בכתיבות תכופות כדאי I/O בלי buffer דרך FILE_FLAG_NO_BUFFERING ו-FILE_FLAG_WRITE_THROUGH. קריאה על handle של volume (בהרשאות admin) עושה flush לכל הקבצים הפתוחים ב-volume. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, File Buffering. דרישות גישה לקובץ שנפתח עם FILE_FLAG_NO_BUFFERING: גודל הקריאה והכתיבה והיסט הקובץ (כולל כשמציינים דרך OVERLAPPED) חייבים להיות כפולה שלמה של גודל ה-sector של ה-volume; כתובת buffer הקריאה והכתיבה צריכה להיות מיושרת לגודל sector פיזי; ונדרשת התחשבות בהתקני Advanced Format עם sector פיזי של 4,096 בתים. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, File Mapping. file-mapping object נשען על קובץ בדיסק, ודחיקת דף מתבצעת ככתיבת התוכן שהשתנה אל הקובץ. כשכמה תהליכים יוצרים views של קובץ מקומי מאותו mapping object, הנתונים coherent (זהים לתוכן הקובץ בדיסק). ↩ ↩2 ↩3
-
Microsoft Learn, FlushViewOfFile function. FlushViewOfFile מתחיל כתיבה לדיסק של dirty pages בטווח ה-mapped view. הפונקציה לא עושה flush ל-metadata של הקובץ ולא ממתינה להשלמת כתיבה פיזית מ-cache חומרה של הדיסק. כדי לכתוב פיזית עד הסוף את כל ה-dirty pages וה-metadata, קוראים ל-FlushFileBuffers אחרי FlushViewOfFile. ↩ ↩2 ↩3
-
Microsoft Learn, IRPs Are Different From Fast I/O. Fast I/O הוא נתיב מהיר ל-I/O סינכרוני לקובץ שבמטמון, שקורא ישירות ל-entry points של מערכת הקבצים ושל Cache Manager בלי לייצר IRP. נתונים מועברים ישירות מהמטמון ל-user buffer (או להפך). כש-Fast I/O לא יכול לטפל, משתמשים בנתיב הרגיל מבוסס IRP. ↩ ↩2 ↩3
-
Microsoft Learn, Asynchronous disk I/O appears as synchronous on Windows. כשהנתונים במטמון הבקשה מסתיימת במקום וחוזר TRUE. מטמון Windows ממומש כמיפוי קובץ, ואין מנגנון page fault אסינכרוני כשהדף לא נוכח, ולכן קריאה אסינכרונית עם מטמון דלוק עלולה להיות מעובדת באופן סינכרוני. ↩
-
Microsoft Learn, FileStream.Flush method (.NET). Flush() כותב את ה-buffer הפנימי של הזרם אל ה-OS, ו-Flush(true) בנוסף עושה flush גם לכל buffers הביניים של הקובץ (ה-buffers של ה-OS). ↩
מאמרים קשורים
מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.
מעמקי ה-I/O ב-Windows (פרק 5) — המבנה הפנימי של NTFS: מערכת קבצים מתוך MFT
פרק 5 בסדרה שמסבירה בתרשימים את המבנה הפנימי של NTFS. המאמר עובר על MFT ו-file records, כמה data streams (Zone.Identifier), hard links וש...
מעמקי ה-I/O של Windows (חלק 3) — I/O completion ports (IOCP) ו-thread pool של .NET: המרתף שמתחת ל-async/await
חלק 3 בסדרה עם איורים על I/O completion ports (IOCP): איחוד ה-queue עם בקרת מספר ה-threads, ערך ה-concurrency, שחרור ה-threads ב-LIFO והה...
מעמקי ה-I/O של Windows (חלק 2) — I/O סינכרוני ואסינכרוני: מה OVERLAPPED באמת אומר
חלק 2 בסדרה מסביר בדיאגרמות I/O סינכרוני ואסינכרוני (Overlapped I/O) ב-Windows: המשמעות של FILE_FLAG_OVERLAPPED, ארבע דרכי ההודעה על הסיו...
מעמקי ה-I/O של Windows (חלק 1) — כל קריאה וכתיבה הופכת ל-IRP: התמונה הכוללת של מערכת ה-I/O
חלק 1 בסדרה על ה-I/O של Windows מהיסוד: מרחב השמות של ה-Object Manager, ה-driver ואובייקטי ה-device וה-file, מחזור החיים של IRP, ומה Clos...
מעמקי ה-I/O ב-Windows (פרק 6, אחרון) — filter drivers ו-minifilter: למה Procmon וסריקת אנטי-וירוס יכולים להתערב ב-I/O
הפרק האחרון בסדרה שמסבירה בתרשימים filter drivers ו-minifilter ב-Windows. המאמר עובר על Filter Manager ו-altitude, callbacks מסוג pre/pos...
נושאים קשורים
העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.
נושאים טכניים ב-Windows
שער לנושאי פיתוח Windows, חקירת תקלות וניצול נכסים קיימים.
שירותים הקשורים לנושא הזה
המאמר קשור ישירות לשירותים הבאים.
פיתוח יישומי Windows
יישומים עסקיים, חיבור התקנים וכלי תקשורת, מהגדרת הדרישות ועד הפיתוח.
שאלות נפוצות
שאלות נפוצות בפניות בנושא המאמר.
- ברגע ש-WriteFile החזיר הצלחה, הנתונים כבר נכתבו לדיסק?
- כברירת מחדל לא. מטמון הקבצים של Windows הוא write-back, ו-WriteFile מחזיר הצלחה ברגע שהעתיק את הנתונים למטמון הקבצים של המערכת. הכתיבה לדיסק נעשית אחר כך בידי lazy writer של Cache Manager, שרץ פעם בשנייה. מה שחשוב הוא ההבדל לפי סוג הכשל. גם אם תהליך האפליקציה קורס, נתונים שנכנסו למטמון לא אובדים: כל עוד ה-OS חי, הוא יכתוב אותם אחר כך. לעומת זאת, בכשל שבו כל מערכת ההפעלה נופלת (ניתוק חשמל, מסך כחול), dirty cache שעדיין לא נכתב אובד. הקריאה המדויקת של 'WriteFile הצליח' היא לא 'נהיה durable' אלא 'נמסר ל-OS'.
- איך מוודאים שהנתונים באמת מגיעים לדיסק?
- יש שלושה כלים. הראשון הוא FlushFileBuffers, שכותב עד הסוף אל ההתקן את כל הנתונים וה-metadata שנאגרו לקובץ הזה (ב-.NET המקבילה היא FileStream.Flush(true)). השני הוא FILE_FLAG_WRITE_THROUGH, שבכל כתיבה כותב גם למטמון וגם מיד לדיסק. השלישי הוא FILE_FLAG_NO_BUFFERING, שלא עובר במטמון כלל. התיעוד של Microsoft אומר שקריאה ל-FlushFileBuffers בכל כתיבה לא יעילה, ושלאפליקציות שצריכות durability אמין בכתיבות תכופות כדאי לשלב FILE_FLAG_NO_BUFFERING עם FILE_FLAG_WRITE_THROUGH. כולם מוותרים על חלק מתועלת המטמון ולכן נהיים איטיים יותר. בפועל לא שמים את זה על הכול, אלא מצמצמים לכתיבות שאי אפשר להרשות לאבד.
- מה ההבדל בין FILE_FLAG_WRITE_THROUGH ל-FILE_FLAG_NO_BUFFERING?
- WRITE_THROUGH אומר 'עדיין כותבים למטמון, אבל לפני ההשלמה כותבים גם לדיסק'. קריאות ממשיכות ליהנות מהמטמון; מה שמורידים הוא רק השהיית lazy writer בכתיבות. NO_BUFFERING אומר 'קריאה וכתיבה לא עוברות במטמון המערכת', כך ששתיהן הופכות ל-I/O אל ההתקן בכל קריאה (מה שעוקפים הוא רק את מטמון Windows עצמו, לא write cache שיושב בתוך התקן האחסון). בתמורה מגיעות מגבלות קשיחות: גודל והיסט הקובץ של כל קריאה או כתיבה חייבים להיות כפולה שלמה של גודל ה-sector של ה-volume, וגם כתובת ה-buffer חייבת להיות מיושרת לגבול sector פיזי. גם עם NO_BUFFERING, metadata של מערכת הקבצים ממשיך להיות במטמון, ולכן כדי להפוך גם metadata ל-durable עדיין צריך FlushFileBuffers או WRITE_THROUGH מעליו. בדרך כלל משתמשת בזה תוכנה שמנהלת buffers בעצמה, כמו מנוע מסד נתונים. באפליקציה רגילה סביר להתחיל מ-WRITE_THROUGH או FlushFileBuffers.
- במנהל המשימות נראה מעט זיכרון פנוי. זו אשמת מטמון הקבצים?
- לעיתים קרובות כן, וזו התנהגות תקינה. Windows משתמש בזיכרון פיזי פנוי באופן פעיל כמטמון קבצים, ולכן העתקת קובץ גדול או הרבה קריאה וכתיבה מנפחות את המטמון ושימוש הזיכרון נראה גבוה יותר. אבל רוב הדפים שהמטמון משתמש בהם הם מסוג שאפשר למחזר די מהר ברגע שאפליקציה מבקשת זיכרון, וצריך להבדיל את זה ממצב של 'הזיכרון נאכל ולא מספיק'. כשחושדים במחסור אמיתי, מעשי יותר להסתכל לא רק על כמות הפנוי הנראית אלא גם על committed memory ועל תדירות hard fault.
- אם נוגעים באותו קובץ גם דרך memory-mapped view וגם דרך ReadFile/WriteFile, התוכן יכול לסטות?
- מול I/O רגיל עם מטמון דלוק, לא. המטמון של Windows עצמו ממומש כמיפוי קובץ, ולכן mapped view והמטמון של אותו קובץ מקומי חולקים את אותם נתונים: שינוי דרך האחד נראה גם דרך השני. גם כמה תהליכים שיוצרים views מאותו file-mapping object רואים נתונים coherent. אבל קריאה וכתיבה ב-handle שנפתח עם FILE_FLAG_NO_BUFFERING עוקפות את המטמון ויוצאות ממסגרת העקביות הזו. וכדי לכתוב בוודאות לדיסק שינויים שנעשו דרך mapped view, FlushViewOfFile לבד לא מספיק: הוא לא כותב metadata ולא ממתין למטמון החומרה. אחרי FlushViewOfFile צריך לקרוא ל-FlushFileBuffers.