מיפוי קבצים ושיתוף דפים¶
נניח שיש קובץ גדול ואנחנו רוצים לגשת אליו כמו למערך bytes. אפשר לקרוא אותו ל־buffer, ואפשר ליצור view שמחבר טווח כתובות לתוכן הקובץ. במקרה השני מערכת ההפעלה משתתפת בהבאת דפים ובשיתוף שלהם, אבל היא אינה פותרת בשבילנו את חוזה הפורמט, הסנכרון או העמידות.
מיפוי כולל כמה משאבים. יש file handle, יש אובייקט mapping ויש view במרחב הכתובות. כל אחד מהם יכול להיות בעל חיים שונים, ולכן עטיפה אחת שמבצעת CloseHandle על הכול אינה נכונה.
רואים עותק פרטי מול view משותף¶
התוכנית הבאה יוצרת קובץ חדש בתיקיית המעבדה. היא משתמשת ב־CREATE_NEW כדי לא לדרוס קובץ קיים; אם השם כבר קיים בוחרים תיקייה חדשה או שם ניסוי חדש.
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <cstdio>
struct Handle {
HANDLE value;
explicit Handle(HANDLE raw) : value(raw) {}
~Handle() {
if (value && value != INVALID_HANDLE_VALUE) CloseHandle(value);
}
Handle(const Handle&) = delete;
Handle& operator=(const Handle&) = delete;
};
struct View {
void* value;
explicit View(void* raw) : value(raw) {}
~View() { if (value) UnmapViewOfFile(value); }
View(const View&) = delete;
View& operator=(const View&) = delete;
};
int main() {
Handle file(CreateFileW(L"mapped-lab.bin", GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ, nullptr, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr));
if (file.value == INVALID_HANDLE_VALUE) return 1;
LARGE_INTEGER end{};
end.QuadPart = 1024 * 1024;
if (!SetFilePointerEx(file.value, end, nullptr, FILE_BEGIN) ||
!SetEndOfFile(file.value)) return 2;
Handle mapping(CreateFileMappingW(file.value, nullptr,
PAGE_READWRITE, 0, 0, nullptr));
if (!mapping.value) return 3;
View shared(MapViewOfFile(mapping.value, FILE_MAP_WRITE, 0, 0, 0));
View copy(MapViewOfFile(mapping.value, FILE_MAP_COPY, 0, 0, 0));
if (!shared.value || !copy.value) return 4;
auto* shared_bytes = static_cast<volatile unsigned char*>(shared.value);
auto* copy_bytes = static_cast<volatile unsigned char*>(copy.value);
shared_bytes[0] = 7;
copy_bytes[0] = 9;
std::printf("shared=%u private=%u\n",
static_cast<unsigned>(shared_bytes[0]),
static_cast<unsigned>(copy_bytes[0]));
if (!FlushViewOfFile(shared.value, 1)) return 5;
if (!FlushFileBuffers(file.value)) return 6;
}
הכתיבה ל־copy יוצרת שינוי פרטי לפי חוזה FILE_MAP_COPY. היא אינה אמורה לשנות את הבית ב־shared או את הקובץ דרך המיפוי הזה. אין להשתמש ב־copy-on-write כדי לבצע עדכון שאמור להישמר, ואז להתפלא שהקובץ לא השתנה.
סגירת handle וביטול מיפוי¶
ה־View מושמד לפני ה־mapping וה־file מפני שהוא נבנה אחריהם. זהו סדר ברור, אך אפשר גם לסגור mapping handle אחרי יצירת views ועדיין להחזיק אותם לפי חוזה המערכת. ה־view עצמו נשאר משאב שדורש UnmapViewOfFile.
אחרי unmap כל span או pointer שמתייחסים ל־view הופכים לבלתי תקפים. הם אינם מחזיקים reference אוטומטי לאובייקט המיפוי. לכן פונקציה שמחזירה string_view מתוך view מקומי יוצרת אותו סוג פגם חיים שלמדנו בפרק C++.
חלון קטן בתוך קובץ גדול¶
אין צורך למפות קובץ שלם כדי לקרוא רשומה אחת. בוחרים offset מיושר ל־allocation granularity, ומוסיפים delta פנימי כדי להגיע למיקום הרצוי. הכתובת שמוסרים ל־UnmapViewOfFile היא בסיס ה־view, לא המצביע המוזז אל הרשומה.
הקטע הבא מציג את החשבון עבור offset ואורך שכבר נבדקו מול גודל הקובץ. הוא מצטרף לקוד שמחזיק mapping handle חי.
SYSTEM_INFO system{};
GetSystemInfo(&system);
const unsigned long long offset = 65536ull + 13;
const SIZE_T length = 256;
const auto granularity =
static_cast<unsigned long long>(system.dwAllocationGranularity);
const auto aligned = offset / granularity * granularity;
const auto delta = static_cast<SIZE_T>(offset - aligned);
const SIZE_T view_size = delta + length;
View window(MapViewOfFile(mapping.value, FILE_MAP_READ,
static_cast<DWORD>(aligned >> 32), static_cast<DWORD>(aligned),
view_size));
if (!window.value) return 7;
const auto* record =
static_cast<const unsigned char*>(window.value) + delta;
בממשק כללי צריך לבדוק ש־delta ועוד length אינו גולש ב־SIZE_T, ושכל הטווח נמצא בתוך הקובץ. בתהליך x86 גם קובץ עם offsets בני 64 ביט אינו מבטיח שאפשר למפות view עצום אחד במרחב כתובות מוגבל.
שינוי קובץ הוא שינוי חוזה¶
מיפוי אינו snapshot אוטומטי של פורמט עסקי. אם תהליך אחר יכול לכתוב, הקורא עלול לראות שינוי בין קריאת כותרת לקריאת גוף. צריך להגדיר אם הקובץ immutable בזמן השימוש, אם יש נעילה או אם הפורמט תומך בגרסאות ו־commit markers.
הקטנה או שינוי מבני של קובץ ממופה כפופים לחוזי מערכת הקבצים ולמצב ה־views, ויכולים להיכשל או להוביל לכשל בגישה בתרחישים מתאימים. אין לתכנן parser שמניח שקובץ נשאר באותו גודל רק מפני שנבדק פעם אחת.
בתים משותפים וחיי אובייקטים¶
שמירת std::string בתוך mapping אינה מאפשרת לתהליך אחר להשתמש בה. המצביע הפנימי שלה שייך למרחב הכתובות של היוצר, וה־allocator וה־lifetime אינם חוזה בין processes.
פורמט משותף משתמש בייצוג מפורש, offsets במקום pointers, גדלים נבדקים וסנכרון מתאים. גם מספר שלם משותף אינו הופך לאטומי רק משום שהוא mapped. בפרק התקשורת המקומית נבנה פרוטוקול שמגדיר את כל השכבות.
גבולות ההבטחה של flush¶
הדוגמה מבקשת flush של ה־view ושל הקובץ. בפרק האחסון נבדיל בין נראות, השלמת כתיבה ועמידות מול כשל חשמל. גם flush מוצלח אינו הופך סדרת עדכונים מרובת שדות ל־transaction אטומית.
הבחירה במיפוי נבחנת לפי דפוס גישה, גודל חלונות, faults, חיי pointers ודרישות התאוששות. היא אינה תמיד מהירה יותר מקריאה מפורשת, ואינה מבטלת את הצורך למדוד.
לקריאה נוספת: יצירת אובייקט מיפוי, יצירת view והסרת view.