זיכרון משותף והתאוששות עמית¶
נניח ששני תהליכים ממפים את אותם bytes. האם אפשר להניח בהם std::vector ולתת לשניהם להשתמש בו? המצביע הפנימי של ה־vector מתייחס למרחב כתובות מסוים, וההקצאות שלו אינן נעשות פתאום משותפות. גם ABI, חיי אובייקטים וסנכרון נשארים חלק מן הבעיה.
נבנה layout מפורש שמכיל offsets מתוך תחילת המיפוי במקום pointers. נשתמש ב־named mutex כדי להגן על גישות של שני תהליכי מעבדה, ונגדיר מה עושים אם הבעלים הקודם מת באמצע עדכון.
מיפוי בעל גרסה¶
התוכנית הבאה מקבלת suffix קצר שמזהה ניסוי, ממפה ארבעת אלפים תשעים ושישה בתים ומגדילה מונה. שתי הרצות עם אותו suffix חולקות את אותו מיפוי כל עוד לפחות אחת מחזיקה אותו:
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <cstddef>
#include <cstdint>
#include <iostream>
#include <limits>
#include <string>
struct Handle {
HANDLE value;
explicit Handle(HANDLE v) : value(v) {}
~Handle() { if (value) CloseHandle(value); }
Handle(const Handle&) = delete;
Handle& operator=(const Handle&) = delete;
};
struct OwnedMutex {
HANDLE value;
~OwnedMutex() { ReleaseMutex(value); }
};
struct View {
std::byte* value;
~View() { if (value) UnmapViewOfFile(value); }
};
void put(std::byte* bytes, std::size_t offset,
std::uint64_t value, unsigned width) {
for (unsigned i = 0; i < width; ++i)
bytes[offset + i] = std::byte((value >> (i * 8)) & 0xffu);
}
std::uint64_t get(const std::byte* bytes,
std::size_t offset, unsigned width) {
std::uint64_t value = 0;
for (unsigned i = 0; i < width; ++i)
value |= std::uint64_t(std::to_integer<unsigned char>(
bytes[offset + i])) << (i * 8);
return value;
}
int wmain(int argc, wchar_t** argv) {
if (argc != 2) return 2;
const std::wstring suffix = argv[1];
if (suffix.empty() || suffix.size() > 32 ||
suffix.find_first_not_of(
L"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") !=
std::wstring::npos) return 2;
const std::wstring base = L"Local\\AmitMemoryLab_" + suffix;
Handle mutex(CreateMutexW(nullptr, FALSE, (base + L"_mutex").c_str()));
if (!mutex.value) return 1;
Handle mapping(nullptr);
View view{nullptr};
{
const DWORD wait = WaitForSingleObject(mutex.value, 2000);
if (wait != WAIT_OBJECT_0 && wait != WAIT_ABANDONED) return 1;
OwnedMutex ownership{mutex.value};
if (wait == WAIT_ABANDONED) {
std::cerr << "previous owner died; recovery required\n";
return 3;
}
mapping.value = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr,
PAGE_READWRITE, 0, 4096, (base + L"_mapping").c_str());
const DWORD creation_status = GetLastError();
if (!mapping.value) return 1;
const bool existed = creation_status == ERROR_ALREADY_EXISTS;
view.value = static_cast<std::byte*>(MapViewOfFile(
mapping.value, FILE_MAP_ALL_ACCESS, 0, 0, 4096));
if (!view.value) return 1;
if (!existed) {
put(view.value, 0, 0x314d5441u, 4);
put(view.value, 4, 1, 4);
put(view.value, 8, 4096, 4);
put(view.value, 12, 32, 4);
put(view.value, 16, 8, 4);
put(view.value, 32, 0, 8);
}
if (get(view.value, 0, 4) != 0x314d5441u ||
get(view.value, 4, 4) != 1 ||
get(view.value, 8, 4) != 4096 ||
get(view.value, 12, 4) != 32 ||
get(view.value, 16, 4) != 8) return 1;
const auto count = get(view.value, 32, 8);
if (count == std::numeric_limits<std::uint64_t>::max()) return 1;
put(view.value, 32, count + 1, 8);
std::cout << "count=" << count + 1 << '\n';
}
std::cout << "press Enter to release this process's mapping\n";
std::cin.get();
}
הניסוי משתמש ב־DACL ברירת המחדל ובהרצות מאותו חשבון לצורכי לימוד. אין להציג אותו כגבול אבטחה לשירות רב־משתמשים. שם אקראי או suffix אינם אימות. מוצר צריך להגדיר ACL ולבדוק גם התנגשות עם אובייקט קיים שלא נוצר על ידו.
הפרוטוקול מתחיל לפני הנתון הראשון¶
ה־mutex נוצר ונרכש לפני יצירת המיפוי והבדיקה אם כבר היה קיים. כך שני תהליכים תקינים אינם מאתחלים יחד את הכותרת. התהליך שיצר מיפוי חדש כותב גרסה, גודל ו־offsetים; תהליך שמצא מיפוי קיים מאמת אותם לפני גישה למונה.
המספר שלושים ושתיים הוא offset בתוך המיפוי, לא כתובת. כל תהליך מוסיף אותו לבסיס view משלו. בגרסה עם טווחים משתנים נבדוק ש־offset אינו עובר את גודל המיפוי וש־length אינו גדול מן ההפרש שנותר, כדי למנוע overflow בחיבור.
הפורמט הוא bytes עם קידוד מוגדר. אנחנו לא טוענים שהנחת std::atomic בתוך מיפוי הופכת את כל השימוש לחוצה־תהליך ונייד לפי תקן C++. כאן הסנכרון נשען על חוזה Windows של mutex, והייצוג מפוענח במפורש.
בעלים שמת אינו משלים עסקה¶
החזרה WAIT_ABANDONED אומרת שהקורא קיבל בעלות על mutex שננטש. היא אינה אומרת שהנתונים המוגנים תקינים. ייתכן שהבעלים הקודם כתב חצי כותרת או עדכן מונה בלי לעדכן אינדקס קשור.
לכן הדוגמה מסרבת להמשיך ומדווחת שנדרש recovery. היא גם משחררת את ה־mutex שהתקבל. במערכת אמיתית אפשר לשחזר מן היומן העמיד, להשתמש בגרסאות כפולות עם commit marker או להכריז על האובייקט כלא תקין ולבנות דור חדש לפי פרוטוקול. אסור לאפס בשקט נתונים שאולי כבר אושרו למשתמש.
בדיקת checksum יכולה לעזור לזהות כתיבה חלקית, אך אינה מחליפה את משמעות העסקה. גם checksum תקין אינו אומר שכל שאר המבנים עודכנו. בנוסף, בעלים שמת אחרי שסיים את העדכון ולפני ReleaseMutex יכול להשאיר נתונים תקינים, ולכן נדרש פרוטוקול שמסוגל להכריע ולא רק לנחש.
חיי המיפוי אינם התמדה¶
המיפוי בדוגמה מגובה במנגנון הזיכרון של המערכת ולא בקובץ נתונים של היישום. כאשר כל ההפניות וה־views הרלוונטיים נסגרים, אין לנו מאגר עמיד שאפשר לפתוח מחדש אחרי אתחול. המתנה ל־Enter משאירה את המיפוי חי כדי שאפשר יהיה לצרף תהליך נוסף.
הפרדה בין cache משותף לבין מקור מידע עמיד עוזרת לפשט recovery. אפשר לשחזר cache מתוך WAL, אבל צריך לזהות באיזה דור הוא נמצא ומה כבר פורסם. גם כאן זמן חיים, זהות ועמידות הם שלושה חוזים שונים.
קריאה להעמקה¶
התיעוד של שיתוף קבצים וזיכרון ושל אובייקטי mutex מגדיר את מנגנוני המערכת. פרוטוקול הגרסה והתאוששות הבעלים הוא באחריות המשתתפים.