מעמקי הקלט/פלט ב-Windows (פרק 4) — מנהל המטמון: מתי ה-WriteFile שלכם מגיע לדיסק

· · Windows, Win32, I/O, מטמון, ליבה, מערכת קבצים, .NET, C#

WriteFile החזיר הצלחה. ובכן, איפה הנתונים עכשיו?

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

פרק 4 בסדרה «מעמקי הקלט/פלט ב-Windows» עוסק במה שעומד באמצע: מנהל המטמון. בפרק 2 כתבנו ש«אם הנתונים כבר במטמון, גם קלט/פלט אסינכרוני מסתיים באופן סינכרוני», ובפרק 1 השארנו כשיעורי בית את «הדרך הקצרה שאינה בונה IRP — Fast I/O». הפעם נסגור את שני החוטים האלה.

1. השורה התחתונה קודם

  • מטמון הקבצים של Windows הוא מטמון write-back. קריאה באה קודם ממטמון הקבצים של המערכת, וגם כתיבה הולכת קודם למטמון. ההשתקפות לדיסק נעשית אחר כך בידי מערכת ההפעלה.1
  • הזהות האמיתית של המטמון היא מיפוי קובץ. מנהל המטמון ממפה מקטעים של 256KB מהקובץ למרחב הכתובות של המערכת, והקריאה והכתיבה הופכות ל«העתקת זיכרון מול התצוגה הזו» (פרק 2).1
  • את הכתיבה מדביק lazy writer שרץ כל שנייה. קריסת יישום אינה מאבדת נתונים, אבל הפסקת חשמל או קריסת מערכת ההפעלה מאבדות מטמון מלוכלך שעדיין לא נכתב (פרק 4).1
  • יש שלושה כלים ל«לכתוב בוודאות». FlushFileBuffers (= Flush(true) ב-.NET), FILE_FLAG_WRITE_THROUGH, ו-FILE_FLAG_NO_BUFFERING. שטיפה בכל כתיבה תכופה אינה יעילה, והתיעוד הרשמי מציין שילוב של NO_BUFFERING+WRITE_THROUGH (פרק 5).21
  • ל-NO_BUFFERING יש דרישות יישור. גודל והיסט חייבים להיות כפולה שלמה של גודל המגזר, וגם כתובת המאגר מיושרת לגבול מגזר פיזי. וגם עם NO_BUFFERING מטא-נתונים ממשיכים להיות במטמון (סעיף 5.3).31
  • תצוגה ממופה והמטמון חולקים את אותם נתונים. קובץ ממופה לזיכרון וקלט/פלט מטמון רגיל עקביים, והפיכת מיפוי לעמיד היא שני שלבים: FlushViewOfFile+FlushFileBuffers (פרק 6).45
  • קריאה וכתיבה סינכרוניות שעלו למטמון לפעמים אינן בונות אפילו IRP. דרך קצרה בשם Fast I/O הולכת ישר למנהל המטמון — זו התשובה לשיעורי הבית מפרק 1 (פרק 7).6

מפת הידע של המאמר

הצלחת WriteFile אינה משמעותה התמדה בדיסק: מטמון הקבצים של Windows מעתיק תחילה לתצוגת מטמון המערכת, ואת ההשתקפות לדיסק רודף lazy writer בכל שנייה. בניתוק חשמל או בקריסת מערכת הפעלה אובדים דפים מלוכלכים שטרם נכתבו, ולכן לנתונים שרוצים לכתוב בוודאות יש לבחור בין FlushFileBuffers, ‏FILE_FLAG_WRITE_THROUGH ו-FILE_FLAG_NO_BUFFERING.

מפת הידע של מנהל המטמון ושל כתיבה מושהיתתרשים שמראה את הקשרים בין מנהל המטמון, מטמון כתיבה לאחור, תצוגת מטמון המערכת של 256KB, קריאה מראש, כתיבה מושהית של lazy writer, אובדן דפים מלוכלכים בניתוק חשמל, כתיבה ודאית באמצעות FlushFileBuffers, ‏FILE_FLAG_WRITE_THROUGH ו-FILE_FLAG_NO_BUFFERING, דרישות יישור, עקביות עם מיפוי קובץ לזיכרון, ו-Fast I/Oמממש אתמשתמש במשתמש במשתמש במשתמש במשתמש במאוטמט אתמצמצםאינו מתיישב עםעלול לגרום לנשמר בעלול לגרום למונעמשתמש במוגדר באמצעותמוגדר באמצעותמונעמונעמצמצםמחייבמונעעלול לגרום לשימוש לא מומלץ למענה מומלץ למענה מומלץ לאינו מתיישב עםמחייבצריך לקדום לנבדק באמצעותמחייבאינו מתיישב עםמחייבמנהל המטמוןמטמון כתיבה לאחור (שיטת כתיבה מושהית)מיפוי קובץ לזיכרון (memory-mapped file)תצוגת מטמון המערכת (חריץ 256KB)אובייקט קובץlazy writer (ת'רד כתיבה מושהית)אובדן נתונים שטרם נכתבו לדיסקFILE_ATTRIBUTE_TEMPORARYדף מלוכלך (dirty page)ניתוק חשמל או קריסת מערכת הפעלהקריאה מראש (read-ahead)FILE_FLAG_SEQUENTIAL_SCANFILE_FLAG_RANDOM_ACCESSFlushFileBuffersFILE_FLAG_WRITE_THROUGHFILE_FLAG_NO_BUFFERINGדרישת יישור למגזרERROR_INVALID_PARAMETER(87)התמדה ודאית בכתיבות תכופותFlushViewOfFileFast I/OProcess Monitor(procmon.exe)IRP (חבילת בקשת קלט/פלט)קלט/פלט סינכרוני

בתרשים, קו מלא מציין קשר שמתקיים תמיד וקו מקווקו מציין קשר מותנה (תנאי ההתקיימות מפורטים בהסבר של כל קשר בעמוד המפורט). רשימת כל הקשרים (סך הכול 32, עם אסמכתה ורמת ודאות) והגדרות המושגים המרכזיים מרוכזות בעמוד המפורט של מפת הידע (ביפנית). נתונים: JSON-LD / Turtle

2. הזהות האמיתית של המטמון — הקובץ ממופה לזיכרון

2.1. משבצות של 256KB והעתקת זיכרון

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

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

איור 1: התמונה האמיתית של קלט/פלט עם מטמון מופעל. «קריאה וכתיבה של קובץ» מנקודת מבט היישום הן, ברוב המקרים, רק העתקת זיכרון

נקודה אחת שקל לטעות בה. 256KB הוא יחידת החלוקה של התצוגה (המיפוי), ולא טענה שקלט/פלט לדיסק תמיד מתבצע ביחידות של 256KB. דפים בתוך משבצת נקראים לפי הצורך, וכמות הקלט/פלט שבאמת עפה לדיסק משתנה לפי גודל הבקשה ודפוס הגישה. אם זה מקטע שנקרא בפעם הראשונה, מתרחש קלט/פלט לדיסק כדי למלא אותו (כאן IRP מפרק 1 יורד למחסנית האחסון התחתונה). אם כבר במטמון, הקריאה מסתיימת בהעתקה בלבד. מה שראינו בפרק 2, סעיף 5 — «פגיעה במטמון משלימה באופן סינכרוני גם כשהונפקה באופן אסינכרוני» — הוא ביטוי של התנועה הזו, «בקשה שאפשר לענות עליה מיד משלימים במקום». ולהפך, כשהמטמון מופעל אבל הדף אינו בזיכרון, לטיפול ב-page fault אין מנגנון אסינכרוני, ולכן קריאה אסינכרונית עלולה להיות מעובדת באופן סינכרוני — מלכודת שגם אותה ראינו בפרק 2.7

2.2. הזהות האמיתית של «הזיכרון הפנוי קטן»

האם משתמשים במטמון ומצב הקריאה מראש מנוהלים לפי אופן הפתיחה (ליחידת אובייקט קובץ),1 אבל גוף הנתונים שבמטמון משותף לפי קובץ (זרם). כמה פעמים שלא תפתחו את אותו קובץ, לא נוצרים מטמונים נפרדים — מכל handle נראה אותו תוכן מטמון (זה הבסיס לעקביות בפרק 6). המטמון פועל תחת פיקוד מנהל המטמון כל עוד Windows רצה.1 אם מעתיקים קובץ גדול או קוראים וכותבים בהיקף גדול, זיכרון פיזי פנוי מוסב יותר ויותר למטמון. גם כשבמנהל המשימות הזיכרון הפנוי נראה קטן, רובו הוא «זיכרון בהמתנה שמשמש שימוש בעל ערך, ויימסר במהירות ברגע שיישום יבקש אותו». כדי לא לטעות בהבחנה הזו באבחון מחסור בזיכרון, פרקטיקת התצפית טופלה גם ב«להבחין בין עיכוב GC לדליפת זיכרון ב-.NET».

אפשר לאשר את התנועה הזו על המסך. פותחים מנהל המשימות > ביצועים > זיכרון, וסרגל «הרכב הזיכרון» בתחתית מחולק לבשימוש / שונה / בהמתנה / פנוי. רוב מטמון הקבצים נכנס לבהמתנה הזו, וברשימה מימין מסוכם כ«במטמון». לפירוט דק יותר, צג המשאבים > לשונית זיכרון מציג את אותן קטגוריות עם נפחים. מעתיקים קובץ אחד של כמה ג’יגה-בתים ומסתכלים שוב: ההמתנה גדלה והפנוי קטן, אבל «בשימוש» כמעט אינו משתנה — כלומר לא «הזיכרון נאכל» אלא «זיכרון שהיה פנוי הוסב למטמון», וזה נראה בעין.

3. קריאה מראש — ספקולציה על קריאה

מנהל המטמון, לפי דפוסי גישה מהעבר, קורא מראש את המקטע שכנראה ייקרא הבא (read-ahead). לקובץ שנקרא לפי הסדר, נתוני ההמשך כבר יושבים במטמון לפני שהיישום ביקש — זה פירוק הקסם של מהירות קריאה רציפה. כמות הקריאה מראש אינה קבועה; היא משתנה לפי הדפוס שזוהה וגודל הבקשה.

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

איור 2: קריאה מראש. מלבד זיהוי דפוס הגישה, אפשר לתת רמז בדגלי CreateFile

FileOptions.SequentialScan / RandomAccess מטבלת ההתאמה בפרק 1 הם רמזים למנוע הקריאה מראש הזה. לעיבוד אצווה ש«מלקק הכול לפי הסדר» — הראשון; לגישה שמדלגת כמו מעבר על אינדקס — השני. חושבים עליהם כדגלים שבהם היישום מספר למערכת ההפעלה על עתיד שרק הוא יודע, והמקום להשתמש בהם מתבהר.

4. כתיבה מאוחרת — המשמעות של «הצלחה» של WriteFile

4.1. lazy writer מגיע כל שנייה

בצד הכתיבה זה מטמון write-back. WriteFile מחזיר הצלחה ברגע שהנתונים הועתקו למשבצת, וההשתקפות לדיסק נדחית. מדיניות «לכתוב באיחור» הזו היא כתיבה מאוחרת (lazy writing).1

את ההשתקפות נושא lazy writer, שמנהל המטמון מפעיל כל שנייה. הוא שם בתור שמינית מהדפים שלא נשטפו לאחרונה וכותב אותם, ואם יש עוד הרבה לכתוב הוא מוסיף עוד. קבצים זמניים שנוצרו עם המאפיין FILE_ATTRIBUTE_TEMPORARY אינם יעד לשטיפה של lazy writer — אין טעם לכתוב משהו שצפוי להימחק מיד.1 אבל זה רמז לפי מאפיין: אם הזיכרון לוחץ עדיין אפשר לכתוב בחזרה, ועל קובץ ש«רק השם נשמע זמני» זה אינו חל.

דיסקlazy writer(מופעל כל שנייה)מטמון המערכתיישוםדיסקlazy writer(מופעל כל שנייה)מטמון המערכתיישוםמעתיק למשבצתומסמן את הדף כמלוכלך(עדיין לא נכתב)מכאן עד הכתיבה לאחור זה «חלון מסוכן»בהפסקת חשמל או קריסת מערכת ההפעלה הנתונים האלה ייעלמורק כאן זה הופך לעמידWriteFile(נתונים)TRUE חוזר מידבוחר 1/8 מהדפים המלוכלכיםכותב אותם בחזרה בבת אחת

איור 3: כתיבה מאוחרת. הצלחת WriteFile היא «נמסר למערכת ההפעלה», לא «נעשה עמיד»

4.2. מה קרה, ועד כמה נעלם

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

הנתונים מיד אחרי הצלחת WriteFile(דף מלוכלך במטמון)מה קרהתהליך היישוםקרס / הסתיים בכפייהמערכת ההפעלה כולה נעצרה(הפסקת חשמל · מסך כחול)הנתונים נשאריםהמטמון שייך למערכת ההפעלה ולכןlazy writer כותב בחזרה כמתוכנןדפים מלוכלכים אובדיםנשאר רק מה שכבר הגיע לדיסק

איור 4: סוג הכשל וקו פרשת המים של הישרדות. המטמון אינו «רכוש התהליך» אלא «רכוש מערכת ההפעלה»

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

כלומר השאלה בתכנון יישום עסקי היא: «האם מותר לנתונים האלה להיאבד ברגע של הפסקת חשמל?» כמה שניות של יומן אולי מותר. רשומת הזמנה מאושרת — כנראה אסור. רק על מה שאסור להפסיד משתמשים בכלים בפרק הבא.

5. ארגז הכלים ל«נכתב בוודאות»

5.1. FlushFileBuffers — לכתוב עד הסוף עכשיו

FlushFileBuffers כותב עד הסוף אל ההתקן את הנתונים שנאגרו לקובץ שצוין. מטא-נתונים של מערכת הקבצים תמיד במטמון, ולכן כדי להגיע בוודאות גם למטא-נתונים נדרשת שטיפה (או WRITE_THROUGH) — גם זו נקודה לאחוז בה.12 ב-.NET המקבילה היא FileStream.Flush(true) (Flush() לבדו רק מוסר את המאגר הפנימי של .NET למערכת ההפעלה, ומטמון מערכת ההפעלה נשאר כפי שהוא).8

אבל התיעוד הרשמי נועץ מסמר במפורש — לקרוא בכל כתיבה אינו יעיל. אם נדרשת עמידות בכל אחת מתוך כתיבות רבות, יש להשתמש ב-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 מוציא את מטמון המערכת עצמו מקריאה וכתיבה. כל קריאה וכתיבה הופכות לקלט/פלט אל התקן הדיסק, בלי לעבור במטמון.1 אבל מה שאפשר לעקוף הוא רק את מטמון המערכת של Windows; כפי שרואים באיור 5, מטמון הכתיבה בתוך ההתקן הוא קומה נפרדת. אם דרושה גם עמידות להפסקת חשמל, עדיין נדרשים שילוב עם WRITE_THROUGH או FlushFileBuffers. זה כלי להעברה המונית של נפח גדול, או למנוע מסד נתונים שמנהל מאגרים בעצמו — אבל מצורף חוזה קשיח.3

  • הגודל והיסט הקובץ של קריאה וכתיבה הם כפולה שלמה של גודל המגזר של הכרך (למגזר 512 בתים: 512, 1024, 1536…).
  • גם כתובת המאגר מיושרת לגודל המגזר הפיזי (נדרשת גם התחשבות בדיסקי «Advanced Format» עם מגזר פיזי של 4096 בתים).
  • ועדיין מטא-נתונים ממשיכים להיות במטמון, ולכן לעמידות מלאה נדרשים שילוב עם WRITE_THROUGH או FlushFileBuffers.12

«החוזה» הזה הוא המקום הראשון שבו נופלים מי שחשבו שמספיק להוסיף דגל. קריאה וכתיבה בלי לשמור על יישור נכשלות ב-ERROR_INVALID_PARAMETER (87). נסדר את שלוש הנקודות שצריך לקיים.3

מה ליישר תנאי איך מקיימים
גודל הקריאה והכתיבה כפולה שלמה של גודל המגזר של הכרך לקחת lpBytesPerSector מ-GetDiskFreeSpace ולעגל לכפולה שלו
היסט הקובץ כנ״ל (גם כשמציינים ב-Offset של OVERLAPPED) להתקדם בצעדים שהם כפולה של גודל המגזר
כתובת המאגר יישור לגודל המגזר הפיזי להקצות ב-VirtualAlloc (מוחזר אזור מיושר לגבול דף, בדרך כלל 4096 בתים)

את השלישית הכי קל לפספס. לכתובת שמחזירים malloc, new או מערך C# אין הבטחת יישור לגבול מגזר. VirtualAlloc, שמקצה לפי גבול דף, ממלא בדרך גם את דרישת דיסקי «Advanced Format» עם מגזר פיזי של 4096 בתים. הצורה המינימלית נראית כך.

// C++ / Win32. טיפול השגיאות מצומצם למינימום.
DWORD sectorsPerCluster = 0, bytesPerSector = 0, freeClusters = 0, totalClusters = 0;
if (!GetDiskFreeSpaceW(L"C:\\", &sectorsPerCluster, &bytesPerSector,
                       &freeClusters, &totalClusters))
{
    return GetLastError();
}

// יחידת הקריאה/כתיבה כפולה שלמה של גודל המגזר (כאן בערך 1MiB)
const DWORD chunk = (1024 * 1024 / bytesPerSector) * bytesPerSector;

// המאגר הוא אזור מיושר לגבול דף (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 ישירות, וגם אז צריך לקיים בעצמכם את דרישות היישור למעלה. סדר הבחינה הוא «NO_BUFFERING כי אני מנהל מאגרים בעצמי», לא «NO_BUFFERING כי אני רוצה שיהיה מהיר».

5.4. סיכום מתי להשתמש במה

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

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

שיטה מה קורה לאיזה מצב מתאים
ברירת מחדל (מטמון מופעל) מסתיים בהעתקה למטמון. ההשתקפות בידי lazy writer כמעט כל קלט/פלט קבצים
FlushFileBuffers / Flush(true) כותב עד הסוף את הנתונים של הרגע הזה + מטא-נתונים אישור באבן דרך (commit של עסקה וכו’)
FILE_FLAG_WRITE_THROUGH בכל כתיבה מיד לדיסק (קריאה מהמטמון) יומן / journal שאי אפשר לאבד
FILE_FLAG_NO_BUFFERING (+WRITE_THROUGH) בלי לעבור במטמון. יש דרישות יישור ניהול מאגרים עצמי, קלט/פלט המוני

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

מותר(יומן של השניות האחרונות וכו')אסוראבן דרך(אישור עסקה וכו')כל רשומהלא(יישום רגיל)כן(מנוע מסד נתונים וכו')עומדים לכתוב את הנתונים האלהברגע של הפסקת חשמל או מסך כחולהאם מותר שיאבדולהשאיר כברירת מחדל(מטמון מופעל)הכי מהיר. כמעט כל הקלט/פלט כאןמה שאי אפשר לאבד הוא«אבן דרך» או «כל רשומה»באבן דרך FlushFileBuffersב-.NET זה Flush(true)העלות: רק ההמתנה באבן הדרךמנהלים מאגרים בעצמכם ומקיימים את דרישות היישור מ-5.3FILE_FLAG_WRITE_THROUGHבכל כתיבה מיד לדיסקהקריאה נשארת מהירה דרך המטמוןFILE_FLAG_NO_BUFFERING+ FILE_FLAG_WRITE_THROUGHצורת «עמידות תכופה» שהתיעוד הרשמי מציין

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

שלושה דפוסים מעשיים שכדאי לתת להם שם.

  1. «לכתוב לקובץ זמני → לשטוף → לשנות שם» הוא הכלל הקבוע שלא להשאיר קובץ שנכתב למחצה. קודם כותבים את התוכן עד הסוף, ואחר כך מאשרים בשם — המסירה האטומית הזו טופלה בפירוט ב«ידע בסיסי על בקרת הרשאות בלעדיות בהעברת קבצים».
  2. להשאיר למסד הנתונים זו גם בנייה לגיטימית לגמרי. איך SQLite בונה עמידות מ-WAL ומשטיפה — ב«שימוש ב-SQLite ביישום עסקי מ-C#». האפשרות «לא לכתוב אסטרטגיית שטיפה בעצמכם» תמיד על השולחן.
  3. במדד ביצועים — לחשוד במטמון. מדידה ש«הקריאה מהירה מדי» בדרך כלל תופסת פגיעה במטמון מהמעבר השני ואילך. נוהל המדידה מסודר ב«איך משווים נכון מהירות בין גרסאות תוכנה ב-Windows».

ואל תשכחו את הקומה האחרונה באיור 5 — המטמון בתוך התקן הדיסק. FlushFileBuffers דורש כתיבה עד הסוף כולל לשם, אבל בכונני USB ודיסקים חיצוניים מתערב מדיניות מטמון הכתיבה של ההתקן («הסרה מהירה» מול «ביצועים טובים יותר»). לטיפול בהתקנים נתיקים ראו גם «איך לטפל בהתקני USB מיישום Windows».

6. עקביות עם קובץ ממופה לזיכרון

בפרק 1, כששמעו ש«הזהות האמיתית של המטמון היא מיפוי קובץ», בוודאי היה מי שחשב: אז התצוגה שאני עצמי עשיתי ב-MapViewOfFile והמטמון של ReadFile/WriteFile לא רבים?

לא. כי הם יושבים על אותו מנגנון. אובייקט מיפוי קובץ נשען על קובץ, ודחיקת דף מבוצעת ככתיבה לאחור אל הקובץ. גם אם כמה תהליכים יוצרים תצוגות של אותו קובץ מקומי, התוכן שנראה קוהרנטי (עקבי).4

מרחב הכתובות של המערכתמרחב הכתובות של תהליך Aהתצוגה של מנהל המטמון(המשבצת שמשמשת את ReadFile/WriteFile)התצוגה של MapViewOfFileאותם דפים פיזיים(זיכרון הנשען על הקובץ)הקובץ בדיסקקלט/פלט עם FILE_FLAG_NO_BUFFERINGמחוץ לשיתוף הזה(ישר לדיסק)

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

שתי נקודות לתשומת לב.

  • קלט/פלט עם FILE_FLAG_NO_BUFFERING מחוץ לעקביות הזו. קריאה וכתיבה שעוקפות את המטמון אינן מיושרות מול תוכן דרך תצוגה ממופה / מטמון. אם מערבבים — צריך ליישר בעצמכם.
  • הפיכת תצוגה ממופה לעמידה היא שני שלבים. FlushViewOfFile מתחיל כתיבה של דפים מלוכלכים בטווח, אבל אינו כותב מטא-נתונים ואינו ממתין לכתיבה פיזית ממטמון התקן הדיסק. כדי להגיע בוודאות, אחרי FlushViewOfFile קוראים ל-FlushFileBuffers.5

הפרקטיקה של מיפוי קובץ כזיכרון משותף (שיתוף בעל שם, סנכרון, דפוסי תקלה) מטופלת ב«המלכודות של הזיכרון המשותף ושיטות העבודה המומלצות בפועל».

7. Fast I/O — סגירת שיעורי הבית מפרק 1

קודם בשתי שורות. Fast I/O הוא דרך קצרה שהוכנה לקריאה וכתיבה סינכרוניות לקובץ שכבר במטמון; הוא מחליף נתונים ישירות עם המטמון בלי לבנות IRP (חבילת בקשת קלט/פלט — המיכל שבו הליבה מוסרת בקשה למנהל). בעמודת Operation של Procmon מתערבבים IRP_MJ_READ ו-FASTIO_READ כי אותה «קריאה» עברה בנתיב הרגיל או בדרך הקצרה.

בסעיף 5.2 בפרק 1 כתבנו ש«לא כל קלט/פלט הופך ל-IRP». הנה התשובה.

לקובץ שיושב במטמון ידוע שקריאה וכתיבה מסתיימות בהעתקת זיכרון מול המטמון, בלי לטרוח לבנות IRP ולהזרים במחסנית ההתקנים. לכן Windows מכינה לקלט/פלט סינכרוני לקובץ שבמטמון דרך קצרה בשם Fast I/O: אינו בונה IRP, קורא ישירות ל«נקודות הכניסה של Fast I/O» של מערכת הקבצים, ומעתיק ישירות ממנהל המטמון.6 כש-Fast I/O אינו יכול לטפל (אין במטמון, מעורבת נעילה, מסנן מתערב וכו’) חוזרים לנתיב IRP הרגיל. שימו לב שזו דרך מהירה לבקשה סינכרונית דווקא, ולא «פגיעה במטמון = תמיד 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_ — זה מסביר. לקריאה שפגעה במטמון, אפילו IRP הוא מותרות. קיום הנתיב הזה משפיע גם על מנהלי המסנן בפרק 6 (מיניפילטר יכול להתערב גם ב-Fast I/O).

8. סיכום

  • מטמון הקבצים של Windows הוא write-back, והזהות האמיתית היא מיפוי מקטעי 256KB של קובץ. קריאה וכתיבה עם מטמון מופעל הופכות להעתקת זיכרון מול משבצת.1
  • את הקריאה קריאה מראש משערת, ו-SequentialScan/RandomAccess הם הרמזים לכך.1
  • הכתיבה מודבקת בידי lazy writer כל שנייה. גם אם היישום מת הנתונים נשארים, ואם מערכת ההפעלה כולה מתה נעלם רק החלק המלוכלך. שאלת התכנון היא «האם מותר לנתונים האלה להיאבד ברגע של הפסקת חשמל?»1
  • הכלים לכתוב בוודאות הם FlushFileBuffers (אישור באבן דרך) / WRITE_THROUGH (בכל כתיבה) / NO_BUFFERING (בלי מטמון + דרישות יישור). שטיפה בכל פעם אינה יעילה; לעמידות תכופה ההמלצה הרשמית היא שילוב NO_BUFFERING+WRITE_THROUGH. שימו לב גם שמטא-נתונים תמיד במטמון.231
  • תצוגה ממופה והמטמון חולקים את אותם דפים והם עקביים. מחוץ למעגל רק NO_BUFFERING. הפיכת מיפוי לעמיד היא שני שלבים: FlushViewOfFile+FlushFileBuffers.45
  • קריאה וכתיבה סינכרוניות שפגעו במטמון מדלגות אפילו על IRP דרך Fast I/O. זו הזהות האמיתית של רשומות FASTIO_ שראינו ב-Procmon בפרק 1.6

ההמשך הוא פרק 5, «המבנה הפנימי של NTFS — מערכת קבצים מתוך הבנת MFT». עד כאן טיפלנו בקובץ כ«היסט ורצף בתים»; מכאן יורדים אל איך NTFS באמת מסדרת נתונים בדיסק — MFT, כמה זרמי נתונים, היומן, קישורים קשיחים — אל המבנה הסטטי שעל הדיסק עצמו.

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

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

KomuraSoft LLC מטפלת בתכנון ובחקירת קלט/פלט קבצים ביישומים עסקיים ל-Windows — מקרים כמו «נתונים שחשבתי ששמרתי נעלמו» או «כתיבה לקובץ איטית, או מהירה באופן חשוד».

מקורות

  1. Microsoft Learn, File Caching. על כך ש-Windows כברירת מחדל שמה במטמון נתוני קבצים, שהקריאה נעשית ממטמון הקבצים של המערכת וגם הכתיבה הולכת למטמון — מטמון write-back; על כך שהמטמון מנוהל לפי אובייקט קובץ ופועל תחת פיקוד מנהל המטמון; על כך שמדיניות לדחות כתיבה לדיסק ולהחזיק במטמון נקראת כתיבה מאוחרת (lazy writing); על כך שבקריאת קובץ מקטע של 256KB נקרא למשבצת 256KB במרחב הכתובות של המערכת, ותהליך המשתמש מעתיק נתונים מול המשבצת הזו; על כך שמנהל המטמון מפעיל כל שנייה lazy writer ושם בתור הכתיבה לדיסק שמינית מהדפים שלא נשטפו לאחרונה, ובמידת הצורך מוסיף עוד; על כך שקבצים זמניים אינם נשטפים; על כך שבכשל מערכת פתאומי כמו אובדן חשמל נתוני מטמון שלא נכתבו אובדים; על כך שגם כשמבטלים מטמון ב-FILE_FLAG_NO_BUFFERING מטא-נתוני קובץ עשויים להישאר במטמון; על כך שב-FILE_FLAG_WRITE_THROUGH הנתונים נכתבים גם למטמון וגם מיד לדיסק בלי עיכוב lazy writer; ועל כך שמטא-נתוני מערכת קבצים תמיד במטמון, ולכן לעמידות מטא-נתונים נדרשות שטיפה או 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 בדרך כלל כותב למאגר פנימי ומערכת ההפעלה כותבת מדי פעם לדיסק; על כך ש-FlushFileBuffers כותב אל ההתקן את כל המידע שנאגר לקובץ שצוין; על כך שקריאה בכל אחת מכתיבות רבות אינה יעילה, וליישומים שזקוקים לעמידות של נתונים חשובים בכתיבות תכופות כדאי להשתמש בקלט/פלט ללא אגירה דרך FILE_FLAG_NO_BUFFERING ו-FILE_FLAG_WRITE_THROUGH; ועל כך שקריאה על handle של כרך (בהרשאות מנהל) שוטפת את כל הקבצים הפתוחים בכרך.  2 3 4 5

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

  4. Microsoft Learn, File Mapping. על כך שאובייקט מיפוי קובץ נשען על קובץ בדיסק, ודחיקת דף מבוצעת ככתיבת התוכן שהשתנה אל הקובץ; ועל כך שכשכמה תהליכים יוצרים תצוגות של קובץ מקומי מאותו אובייקט מיפוי, הנתונים קוהרנטיים (זהים לתוכן הקובץ בדיסק).  2 3

  5. Microsoft Learn, FlushViewOfFile function. על כך ש-FlushViewOfFile מתחיל כתיבה לדיסק של דפים מלוכלכים בטווח התצוגה הממופה; על כך שהפונקציה אינה שוטפת מטא-נתוני קובץ ואינה ממתינה להשלמת כתיבה פיזית ממטמון דיסק חומרה; ועל כך שכדי לכתוב פיזית עד הסוף את כל הדפים המלוכלכים והמטא-נתונים יש לקרוא ל-FlushFileBuffers אחרי FlushViewOfFile.  2 3

  6. Microsoft Learn, IRPs Are Different From Fast I/O. על כך ש-Fast I/O הוא נתיב מהיר לקלט/פלט סינכרוני לקובץ שבמטמון, שקורא ישירות לנקודות הכניסה של מערכת הקבצים ומנהל המטמון בלי לייצר IRP; על כך שנתונים מועברים ישירות מהמטמון למאגר המשתמש (או להפך); ועל כך שכש-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() כותב את המאגר הפנימי של הזרם אל מערכת ההפעלה, ועל כך ש-Flush(true) בנוסף שוטף גם את כל מאגרי הקבצים הביניים (המאגרים של מערכת ההפעלה). 

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

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

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

שאלות נפוצות

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

ברגע ש-WriteFile החזיר הצלחה, האם הנתונים כבר נכתבו לדיסק?
כברירת מחדל לא. מטמון הקבצים של Windows הוא מטמון write-back, ו-WriteFile מחזיר הצלחה ברגע שהעתיק את הנתונים למטמון הקבצים של המערכת. הכתיבה לדיסק נעשית אחר כך בידי lazy writer של מנהל המטמון, שמופעל פעם בשנייה. מה שחשוב הוא ההבדל לפי סוג הכשל. גם אם תהליך היישום קורס, נתונים שנכנסו למטמון אינם אובדים — כל עוד מערכת ההפעלה חיה, היא תכתוב אותם אחר כך. לעומת זאת, בכשל שבו נופלת מערכת ההפעלה כולה (הפסקת חשמל, מסך כחול), מטמון מלוכלך שעדיין לא נכתב אובד. הקריאה המדויקת של «WriteFile הצליח» אינה «נעשה עמיד» אלא «נמסר למערכת ההפעלה».
איך מבטיחים שהנתונים באמת מגיעים לדיסק?
יש שלושה כלים. הראשון הוא FlushFileBuffers, שכותב עד הסוף אל ההתקן את כל הנתונים והמטא-נתונים שנאגרו לקובץ הזה (ב-.NET המקבילה היא FileStream.Flush(true)). השני הוא FILE_FLAG_WRITE_THROUGH, שכותב בכל כתיבה גם למטמון וגם מיד לדיסק. השלישי הוא FILE_FLAG_NO_BUFFERING, שאינו עובר במטמון כלל. התיעוד של Microsoft מציין שקריאה ל-FlushFileBuffers בכל כתיבה אינה יעילה, ושליישומים שזקוקים לעמידות אמינה בכתיבות תכופות כדאי לשלב FILE_FLAG_NO_BUFFERING עם FILE_FLAG_WRITE_THROUGH. כולם מוותרים על חלק מתועלת המטמון ולכן נעשים איטיים יותר, ולכן בפועל לא «שמים על הכול» אלא מצמצמים לכתיבות של נתונים שאי אפשר להרשות לעצמנו לאבד.
מה ההבדל בין FILE_FLAG_WRITE_THROUGH ל-FILE_FLAG_NO_BUFFERING?
WRITE_THROUGH פירושו «עדיין כותבים למטמון, אבל לפני ההשלמה כותבים גם לדיסק». קריאות ממשיכות ליהנות מהמטמון; מוסר רק עיכוב ה-lazy writer בכתיבות. NO_BUFFERING פירושו «קריאה וכתיבה אינן עוברות במטמון המערכת», כך ששתיהן הופכות לקלט/פלט אל ההתקן בכל קריאה (אבל מה שעוקפים הוא רק את מטמון Windows עצמו — לא מדלגים על מטמון כתיבה שיושב בתוך התקן האחסון). בתמורה מגיעות מגבלות קשיחות: גודל והיסט הקובץ של כל קריאה או כתיבה חייבים להיות כפולה שלמה של גודל המגזר של הכרך, וגם כתובת המאגר חייבת להיות מיושרת לגבול מגזר פיזי. וגם עם NO_BUFFERING, מטא-נתונים של מערכת הקבצים ממשיכים להיות במטמון, ולכן כדי להפוך גם מטא-נתונים לעמידים עדיין צריך FlushFileBuffers או WRITE_THROUGH מעליו. בדרך כלל משתמשת בזה תוכנה שמנהלת מאגרים בעצמה, כמו מנוע מסד נתונים; ביישום רגיל סביר להתחיל מ-WRITE_THROUGH או FlushFileBuffers.
במנהל המשימות נראה מעט זיכרון פנוי — האם זו אשמת מטמון הקבצים?
לעיתים קרובות כן, וזו התנהגות תקינה. Windows משתמשת בזיכרון פיזי פנוי באופן פעיל כמטמון קבצים, ולכן העתקת קובץ גדול או הרבה קריאה וכתיבה מנפחות את המטמון והשימוש בזיכרון נראה גבוה יותר. עם זאת, רוב הדפים שהמטמון משתמש בהם הם מסוג שאפשר למחזר די במהירות ברגע שיישום מבקש זיכרון, וצריך להבדיל זאת ממצב של «הזיכרון נאכל ולא מספיק». כשחושדים במחסור אמיתי, מעשי יותר להסתכל לא רק על כמות הפנוי הנראית אלא גם על זיכרון committed ועל תדירות hard fault.
אם נוגעים באותו קובץ גם דרך תצוגה ממופה לזיכרון וגם דרך ReadFile/WriteFile, התוכן יכול להיסחף?
מול קלט/פלט רגיל עם מטמון מופעל — לא. המטמון של Windows עצמו ממומש כמיפוי קובץ, ולכן תצוגה ממופה והמטמון של אותו קובץ מקומי חולקים את אותם נתונים — שינוי דרך האחד נראה גם דרך השני. גם כמה תהליכים שיוצרים תצוגות מאותו אובייקט מיפוי קובץ רואים נתונים קוהרנטיים (עקביים). אבל קריאה וכתיבה ב-handle שנפתח עם FILE_FLAG_NO_BUFFERING עוקפות את המטמון ויוצאות ממסגרת העקביות הזו. וגם, כדי לכתוב בוודאות לדיסק שינויים שנעשו דרך תצוגה ממופה, FlushViewOfFile לבדו אינו מספיק — הוא אינו כותב מטא-נתונים ואינו ממתין למטמון החומרה — ולכן אחרי FlushViewOfFile צריך לקרוא ל-FlushFileBuffers.

פרופיל הכותב

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

Go Komura

מנהל KomuraSoft LLC

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

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

חזרה לבלוג