מעמקי ה-I/O ב-Windows (פרק 4) —‏ Cache Manager: מתי WriteFile באמת מגיע לדיסק

· עודכן בתאריך: · · Windows, Win32, I/O, cache, kernel, file system, .NET, C#

היסטוריית עדכונים (גרסה ראשונה, פורסמה בתאריך 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

מרחב הכתובות של המערכתאפליקציה (user mode)ReadFile/WriteFile =העתקת זיכרון מול המשבצתקריאה בגישה ראשונה וכתיבה חזרהאחר כך, לפי דפיםמטמון הקבצים של המערכתמשבצת שממפה מקטע 256KB מהקובץbuffer של האפליקציה(האזור שהועבר ל-ReadFile/WriteFile)הקובץ בדיסק

איור 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 לא קבועה; היא משתנה לפי הדפוס שזוהה ולפי גודל הבקשה.

היסטוריית בקשות הקריאה של האפליקציהקוראים מההתחלה לפי הסדרCache Manager מזהה דפוסread-ahead: מקטע ההמשך נטעןלפני שהתבקש(הכמות משתנה לפי דפוס וגודל בקשה)רמז FILE_FLAG_SEQUENTIAL_SCAN= read-ahead אגרסיבי יותררמז FILE_FLAG_RANDOM_ACCESS= read-ahead מבוזבז, מדכאים אותו

איור 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, וזה לא חל על קובץ שרק “נשמע זמני” בשם.

דיסקlazy writer (כל שנייה)מטמון המערכתאפליקציהדיסקlazy writer (כל שנייה)מטמון המערכתאפליקציהמעתיק למשבצתומסמן את הדף dirty (עדיין לא נכתב)מכאן עד ה-write-back זה חלון מסוכןניתוק חשמל או קריסת OS מוחקים את הנתונים האלהכאן לראשונה זה נהיה durableWriteFile(נתונים)TRUE חוזר מידבוחר 1/8 מה-dirty pagesכותב חזרה ביחד

איור 3: כתיבה מושהית. הצלחת WriteFile היא “נמסר ל-OS”, לא “נהיה durable”

4.2. מה קורה, ומה נעלם, לפי סוג הכשל

מחדדים מה “חלון מסוכן” אומר. סוג הכשל מפצל את הגורל.

נתונים מיד אחרי הצלחת WriteFile(dirty page במטמון)מה קרהתהליך האפליקציה קרסאו נעצר בכוחכל ה-OS נעצר(ניתוק חשמל, מסך כחול)הנתונים נשאריםהמטמון שייך ל-OSlazy writer כותב חזרה כמתוכנןdirty pages אובדיםנשאר רק מה שכבר הגיע לדיסק

איור 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:\\", &sectorsPerCluster, &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. איך בוחרים

WriteFile כברירת מחדל: הצלחה חוזרת עד כאןlazy writer (כל שנייה) / WRITE_THROUGH (מיידי)תזמון ההתקן /FlushFileBuffers דורש כתיבה עד הסוףNO_BUFFERING מדלג על המטמון והולך ישרbuffer של האפליקציהמטמון הקבצים של המערכת(dirty pages)cache בתוך התקן הדיסקמדיה לא נדיפה

איור 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 גדול בבת אחת

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

מותר(לוג של כמה שניות אחרונות)אסורנקודת ציון(אישור עסקה וכו')כל רשומהלא (אפליקציה רגילה)כן (מנוע DB וכו')עומדים לכתוב את הנתונים האלהמותר לאבד אותם ברגע שלניתוק חשמל או מסך כחול?ברירת מחדל (מטמון דלוק)הכי מהיר. רוב ה-I/O כאןמה שאי אפשר לאבד הואנקודת ציון, או כל רשומה?FlushFileBuffers בנקודת ציוןב-.NET: Flush(true)העלות: המתנה רק בנקודת הציוןמנהלים buffers בעצמכםוממלאים את דרישות היישור מ-5.3?FILE_FLAG_WRITE_THROUGHבכל כתיבה ישר לדיסקקריאה נשארת מהירה במטמוןFILE_FLAG_NO_BUFFERING+ FILE_FLAG_WRITE_THROUGHהצורה שהתיעוד מציין ל-persistence תכוף

איור 6: איך בוחרים כלי. הפיצול הראשון הוא דרישת אמינות, השני הוא העלות שאפשר לשלם. אין מסלול של FlushFileBuffers בכל רשומה, כי כמו ב-5.1 התיעוד הרשמי רואה בזה חוסר יעילות

שלושה דפוסים מעשיים בלבד.

  1. “כותבים לקובץ זמני → flush → rename” הוא הכלל שלא משאיר קובץ חצי-שבור. כותבים את התוכן עד הסוף, ורק אז מקבעים בשם. המסירה האטומית הזו מפורטת ב”יסודות נעילה בשיתוף קבצים”.
  2. להשאיר את זה למסד נתונים זו גם תכנון לגיטימי. איך SQLite בונה durability עם WAL ו-flush מכוסה ב”SQLite באפליקציה עסקית ב-C#”. האפשרות “לא לכתוב בעצמכם אסטרטגיית flush” תמיד קיימת.
  3. ב-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

מרחב הכתובות של המערכתמרחב הכתובות של תהליך Aview של Cache Manager(המשבצת ש-ReadFile/WriteFile משתמשים בה)view של MapViewOfFileאותם דפים פיזיים(זיכרון שנשען על הקובץ)הקובץ בדיסקI/O עם FILE_FLAG_NO_BUFFERINGמחוץ לשיתוף הזה (ישר לדיסק)

איור 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).

אפשראי אפשרקריאה וכתיבה סינכרוניות ל-handle עם מטמון דלוקאפשר לטפל ב-Fast I/O(הקובץ במטמון וכו')Fast I/Oהעתקה ישירה מול המטמון בלי IRPב-Procmon מופיע כ-FASTIO_נתיב רגילבונים IRP ושולחים למחסנית ההתקנים(עולם איור 6 בפרק 1)

איור 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. יורדים למבנה הסטטי על הדיסק.

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

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

ב-KomuraSoft LLC מטפלים בתכנון ובחקירת I/O לקבצים באפליקציות עסקיות ל-Windows: מקרים כמו “נתונים שחשבתי ששמרתי נעלמו” או “כתיבה לקובץ איטית, או מהירה באופן חשוד”.

מקורות

  1. 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

  2. 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

  3. Microsoft Learn, File Buffering. דרישות גישה לקובץ שנפתח עם FILE_FLAG_NO_BUFFERING: גודל הקריאה והכתיבה והיסט הקובץ (כולל כשמציינים דרך OVERLAPPED) חייבים להיות כפולה שלמה של גודל ה-sector של ה-volume; כתובת buffer הקריאה והכתיבה צריכה להיות מיושרת לגודל sector פיזי; ונדרשת התחשבות בהתקני Advanced Format עם sector פיזי של 4,096 בתים. ↩ ↩2 ↩3 ↩4

  4. Microsoft Learn, File Mapping. file-mapping object נשען על קובץ בדיסק, ודחיקת דף מתבצעת ככתיבת התוכן שהשתנה אל הקובץ. כשכמה תהליכים יוצרים views של קובץ מקומי מאותו mapping object, הנתונים coherent (זהים לתוכן הקובץ בדיסק). ↩ ↩2 ↩3

  5. Microsoft Learn, FlushViewOfFile function. FlushViewOfFile מתחיל כתיבה לדיסק של dirty pages בטווח ה-mapped view. הפונקציה לא עושה flush ל-metadata של הקובץ ולא ממתינה להשלמת כתיבה פיזית מ-cache חומרה של הדיסק. כדי לכתוב פיזית עד הסוף את כל ה-dirty pages וה-metadata, קוראים ל-FlushFileBuffers אחרי FlushViewOfFile. ↩ ↩2 ↩3

  6. 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

  7. Microsoft Learn, Asynchronous disk I/O appears as synchronous on Windows. כשהנתונים במטמון הבקשה מסתיימת במקום וחוזר TRUE. מטמון Windows ממומש כמיפוי קובץ, ואין מנגנון page fault אסינכרוני כשהדף לא נוכח, ולכן קריאה אסינכרונית עם מטמון דלוק עלולה להיות מעובדת באופן סינכרוני. ↩

  8. Microsoft Learn, FileStream.Flush method (.NET). Flush() כותב את ה-buffer הפנימי של הזרם אל ה-OS, ו-Flush(true) בנוסף עושה flush גם לכל buffers הביניים של הקובץ (ה-buffers של ה-OS). ↩

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

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

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

שאלות נפוצות

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

ברגע ש-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.

פרופיל הכותב

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

Go Komura

מנהל KomuraSoft LLC

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

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

חזרה לבלוג