לדלג לתוכן

מרחב כתובות ותרגום כתובת בחומרה

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

מרחב הכתובות של תהליך מתואר בין השאר באמצעות אזורים וירטואליים עם הרשאות ומאפיינים. אזור כזה אינו רשימה של כל העמודים הפיזיים שכבר קיימים. מיפוי anonymous יכול לקבל תוכן אפס בלי שכל הטווח יוקצה מיד כעמודים פרטיים. מיפוי קובץ יכול להישען על page cache משותף עם תהליכים אחרים.

התוכנית הבאה משתמשת ב־libc כדי למפות אזור אנונימי ולגעת בכל עמוד. היא משאירה נקודות עצירה לצורך קריאת /proc מבחוץ, ולכן שמרו אותה בפרויקט ניסוי נפרד.

use std::{io, ptr};
fn pause(label: &str) -> io::Result<()> {
    println!("pid={} stage={label}", std::process::id());
    let mut line = String::new();
    io::stdin().read_line(&mut line)?;
    Ok(())
}
fn main() -> io::Result<()> {
    let page = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
    if page <= 0 { return Err(io::Error::other("invalid page size")); }
    let page = page as usize;
    let len = page.checked_mul(4096).ok_or_else(|| io::Error::other("overflow"))?;
    let raw = unsafe { libc::mmap(ptr::null_mut(), len,
        libc::PROT_READ | libc::PROT_WRITE,
        libc::MAP_PRIVATE | libc::MAP_ANONYMOUS, -1, 0) };
    if raw == libc::MAP_FAILED { return Err(io::Error::last_os_error()); }
    if raw.is_null() {
        unsafe { libc::munmap(raw, len); }
        return Err(io::Error::other("null mapping unsupported by this experiment"));
    }
    pause("mapped, untouched")?;
    for offset in (0..len).step_by(page) {
        unsafe { raw.cast::<u8>().add(offset).write_volatile(1); }
    }
    pause("one write per page")?;
    if unsafe { libc::munmap(raw, len) } == -1 {
        return Err(io::Error::last_os_error());
    }
    Ok(())
}

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

קראו את maps ואת smaps_rollup בשתי העצירות. מפת הכתובות אמורה להראות את האזור כבר אחרי mmap, אבל מדדי הזיכרון התושב יכולים להשתנות אחרי הכתיבה. אל תצפו להתאמה מושלמת בין כל מונה לגודל שביקשנו: התהליך כולל ספריות, מחסנית והקצאות אחרות, והמדידה אינה snapshot מסונכרן של כל המערכת.

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

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

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

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

הגדרת המיפוי נמצאת בעמוד mmap, ופרטי המדדים בתיעוד smaps. בכל מדידה ציינו אם אתם מדברים על מרחב כתובות וירטואלי, על עמודים תושבים או על הקצאה לוגית של האפליקציה.