מסלול בקשת קלט ופלט¶
נניח ששני חלקים בתוכנית מחזיקים handle לאותו קובץ. האם לכל אחד יש offset משלו? התשובה תלויה באופן שבו ה־handleים נוצרו. שם קובץ זהה אינו אומר שכל מצב הפתיחה משותף, וערך handle שונה אינו אומר שנוצר file object חדש.
נעקוב אחרי בקשה מן הקוד עד שכבות המערכת, ונשתמש בניסוי של שכפול handle כדי לראות איזה מצב שייך לפתיחה ולא למספר שמופיע במשתנה.
שני handleים וסמן אחד¶
התוכנית הבאה פותחת קובץ סינכרוני, משכפלת את ה־handle וקוראת ארבעה בתים מכל אחד. השתמשו בקובץ מעבדה שמכיל לפחות שמונה בתים ידועים:
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <array>
#include <iostream>
struct Handle {
HANDLE value = INVALID_HANDLE_VALUE;
explicit Handle(HANDLE v = INVALID_HANDLE_VALUE) : value(v) {}
~Handle() {
if (value && value != INVALID_HANDLE_VALUE) CloseHandle(value);
}
Handle(const Handle&) = delete;
Handle& operator=(const Handle&) = delete;
};
int wmain(int argc, wchar_t** argv) {
if (argc != 2) return 2;
Handle first(CreateFileW(argv[1], GENERIC_READ, FILE_SHARE_READ,
nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr));
if (first.value == INVALID_HANDLE_VALUE) return 1;
HANDLE duplicate = nullptr;
if (!DuplicateHandle(GetCurrentProcess(), first.value,
GetCurrentProcess(), &duplicate, 0, FALSE,
DUPLICATE_SAME_ACCESS)) return 1;
Handle second(duplicate);
for (HANDLE file : {first.value, second.value}) {
std::array<char, 4> buffer{};
DWORD count = 0;
if (!ReadFile(file, buffer.data(),
static_cast<DWORD>(buffer.size()), &count, nullptr)) {
std::cerr << GetLastError() << '\n';
return 1;
}
std::cout.write(buffer.data(), count);
std::cout << '\n';
}
}
ה־handle המשוכפל מתייחס לאותו file object, ולכן סמן הקובץ של הפתיחה הסינכרונית משותף. הקריאה השנייה ממשיכה אחרי הראשונה. אם נבצע CreateFileW נוסף במקום DuplicateHandle, נקבל פתיחה נוספת עם מצב סמן נפרד, אף שהזהות של הקובץ באחסון זהה.
במערכת מקבילית, שינוי סמן באמצעות SetFilePointerEx ואז ReadFile אינו זוג פעולות אטומי מבחינת threads אחרים שמשתמשים באותה פתיחה. אפשר להגן על הזוג בנעילה או להשתמש ב־offsetים מפורשים עם פעולות overlapped לפי החוזה. אין להסיק שהנעילה קיימת רק משום שכל API בנפרד עובד.
מן הידית אל האובייקט¶
ה־handle הוא כניסה בטבלת handles של התהליך, עם גישה שהוענקה בפתיחה. מנהל האובייקטים מתרגם אותו לאובייקט המתאים תוך בדיקות נדרשות. ה־file object מחזיק מצב של פתיחה וקשרים למערכת הקבצים או להתקן. המידע אינו זהה לרשומת הספרייה שמחזיקה את שם הקובץ.
שכבת I/O manager מארגנת בקשות ומעבירה אותן לאורך stack של drivers. בקשות רבות מיוצגות באמצעות IRP, הכולל מידע על הפעולה, מצב השלמה ותחנות לאורך ה־stack. אבל אין להסיק שכל ReadFile יוצר תמיד IRP אחד חדש שעובר בדיוק אותו מסלול. קיימים מסלולי cache ו־Fast I/O, ופעולה אחת יכולה להתפצל או לגרום לפעולות נוספות בשכבות נמוכות.
מערכת הקבצים מתרגמת offset לוגי בקובץ למבנים שלה ולבקשות אחסון. שכבות filters, נפחים, הצפנה, וירטואליזציה או אחסון מרוחק יכולות להשתתף בדרך. הפרטים תלויים בתצורת המערכת. החוזה של API נשאר בסיס הקוד, ואילו ה־trace עוזר להבין איזו דרך נבחרה בהרצה מסוימת.
השלמה עוברת חזרה למעלה¶
בקשה יכולה להסתיים באופן מיידי או להישאר pending. שכבה שמסיימת פעולה מחזירה סטטוס וספירת מידע, וההשלמה עוברת דרך מנגנוני המערכת אל אירוע, callback או IOCP בהתאם לחוזה. מספר בתים שהועברו אינו בהכרח מספר הבתים שהלקוח ביקש בתחילת הדרך.
ה־buffer צריך להישאר תקף לאורך המסלול שנקבע. בשכבות שונות ייתכן העתק, מיפוי או נעילה של דפים, אבל לקוד המשתמש אין רשות לשחרר מוקדם על סמך ניחוש על מימוש ה־driver. בפרק הקרנל נלמד כיצד שיטות העברת buffers נראות בצד ה־driver.
תצפית עם מסנן ממוקד¶
פתחו Process Monitor וסננו לפי שם תוכנת המעבדה ולפי נתיב הקלט. הריצו פעם אחת עם handle משוכפל ופעם אחת עם פתיחה נוספת. בדקו את אירועי הפתיחה והקריאה, offsets, גדלים ותוצאות. שמרו גם את גרסת התוכנית ואת קובץ הסמלים.
התצפית אינה בהכרח צילום של כל פקודת דיסק. אירוע קריאה מוצלח יכול להיות מסופק מן המטמון, ופעולת filesystem אינה שקולה לפעולת התקן פיזי. בפרק הבא נפריד בין cache לבין גישה ללא buffering, ובפרק האבחון נחבר אירועי משתמש ואחסון לציר זמן.
קריאה להעמקה¶
התיעוד של שכפול handle ושל ניהול קלט ופלט ב־Windows מספק את גבולות המודל. שמות פונקציות פנימיות ב־stack הם תצפית של build מסוים, לא ABI שאפשר לבנות עליו.