לדלג לתוכן

תרגום כתובות ותקלות דף

נניח ש־VirtualAlloc הצליחה, אבל הגישה הראשונה לכל דף איטית יותר מהגישה השנייה. אין כאן סתירה. בקשת commit הכינה זכות להשתמש בזיכרון, והגישה הראשונה יכולה להפעיל מנגנון שמכין את המיפוי הפיזי בפועל.

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

שואלים על דף לפני ואחרי נגיעה

התוכנית הבאה מבצעת commit למספר קטן של דפים ושואלת על תושבותם באמצעות QueryWorkingSetEx. היא אינה מניחה שכל שאילתה נותנת צילום קפוא לכל שאר המערכת.

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <psapi.h>
#include <cstdio>
#include <vector>

unsigned resident(void* base, SIZE_T page_size, unsigned pages) {
    std::vector<PSAPI_WORKING_SET_EX_INFORMATION> information(pages);
    for (unsigned i = 0; i < pages; ++i)
        information[i].VirtualAddress =
            static_cast<unsigned char*>(base) + i * page_size;
    if (!QueryWorkingSetEx(GetCurrentProcess(), information.data(),
            static_cast<DWORD>(information.size() * sizeof(information[0]))))
        return static_cast<unsigned>(-1);
    unsigned count = 0;
    for (const auto& item : information)
        if (item.VirtualAttributes.Valid) ++count;
    return count;
}

int main() {
    SYSTEM_INFO system{};
    GetSystemInfo(&system);
    constexpr unsigned pages = 64;
    const SIZE_T size = static_cast<SIZE_T>(system.dwPageSize) * pages;
    void* memory = VirtualAlloc(nullptr, size,
                                MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
    if (!memory) return 1;
    const unsigned before = resident(memory, system.dwPageSize, pages);
    auto* bytes = static_cast<volatile unsigned char*>(memory);
    for (unsigned i = 0; i < pages; ++i)
        bytes[static_cast<SIZE_T>(i) * system.dwPageSize] = 1;
    const unsigned after = resident(memory, system.dwPageSize, pages);
    const BOOL released = VirtualFree(memory, 0, MEM_RELEASE);
    if (before == static_cast<unsigned>(-1) ||
        after == static_cast<unsigned>(-1) || !released)
        return 2;
    std::printf("resident before=%u after=%u\n", before, after);
}
cl /nologo /std:c++20 /EHsc /W4 residency.cpp /Feresidency.exe /link Psapi.lib

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

תקלה בדף אינה בהכרח קריאה מהדיסק

תקלה יכולה להיפתר באמצעות דף מאופס שהמערכת מספקת, באמצעות דף שכבר קיים בזיכרון אך אינו נמצא כרגע ב־working set המתאים, או באמצעות יצירת עותק פרטי. במקרים אחרים נדרשת קריאה ממקור אחסון.

המונחים soft fault ו־hard fault מבחינים באופן כללי בין טיפול שאינו דורש קריאת דף מהאחסון לבין טיפול שדורש אותה. לכן מונה page faults כללי אינו מונה פעולות דיסק. גישה ראשונה לדף פרטי חדש יכולה ליצור fault בלי שקיים קובץ שממנו צריך לקרוא את התוכן.

גם TLB miss אינו בהכרח page fault. המעבד יכול למצוא תרגום תקף בטבלאות ולהמשיך בלי להיכנס למסלול הטיפול של מערכת ההפעלה. ערבוב שני המנגנונים מוביל להסברים שגויים של ביצועים.

איפוס לפי דרישה - demand zero

המערכת מתחייבת שהזיכרון החדש יהיה מאופס לפי חוזה ההקצאה. היא יכולה לדחות חלק מהעבודה עד לגישה הראשונה, כל עוד נשמרת ההתנהגות הנצפית. כך reservation ו־commit של טווח גדול אינם חייבים לגעת בכל byte מראש.

המחיר מופיע לעיתים בתחילת עבודה רגישה לזמן. תוכנה שזקוקה לזמן תגובה צפוי יכולה לבחור לגעת בדפים מראש, אך זו החלטת תקציב וביצועים: היא צורכת זמן ויכולה להגדיל working set מוקדם יותר. אין להציג pre-touch כהמלצה אוניברסלית.

כתיבה יכולה להפוך שיתוף לפרטיות

ב־copy-on-write כמה מיפויים יכולים להתחיל עם תוכן משותף. כתיבה של צד מסוים גורמת למערכת ליצור עבורו עותק פרטי בהתאם לחוזה המיפוי. הפעולה נראית לכותב כמו כתיבה רגילה, אך מאחוריה יש fault, הקצאה ועדכון תרגום.

קריאת VirtualQuery יכולה להמשיך להציג סוג מיפוי שאינו מספר לבדו את כל מצב השיתוף של הדף. כדי לחקור פרטיות ותושבות משתמשים גם במידע working set ובכלים מתאימים. בשיעור מיפוי קבצים נבצע ניסוי COW ממשי.

תרגום שייך להקשר

אותה כתובת וירטואלית בשני processes יכולה להתייחס לדפים שונים. החלפת הקשר כתובות ומנגנוני המעבד לטיפול בתרגומים מאפשרים את הבידוד. פרטים כמו מספר רמות טבלה, שימוש ב־large pages וסימוני TLB תלויים בארכיטקטורה ובתצורה.

ב־x64 נלמד לקרוא טבלאות ו־PTEs ביעד kernel debugging. ב־ARM64 קיימים פרטי תרגום וחומרה אחרים. החוזה של VirtualAlloc נשאר שימושי גם כשמספרי הביטים והטבלאות אינם זהים.

בודקים מה באמת נמדד

מדידה לפני ואחרי נגיעה מוכיחה שהגישה שינתה מצב שניתן לצפייה, אם זה אכן נראה בתוצאה. היא אינה מזהה לבדה כל סוג fault. בשביל אבחנה עמוקה משלבים counters, trace של faults ומידע על backing store.

ב־WinDbg kernel אפשר להשתמש ב־!pte לכתובת של תהליך מעבדה אחרי בחירת ההקשר הנכון. הפקודה אינה זמינה באותה משמעות בדיבוג משתמש רגיל, וכתובת מספרית בלי process context יכולה להוביל לטבלה הלא נכונה.

לקריאה נוספת: שאילתת מידע על דפים וחקירת PTE ב־WinDbg.