לדלג לתוכן

פגיעות דף והתחייבות לזיכרון

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

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

נוסיף לדוגמת Mapping מההרצאה הקודמת פונקציה שקוראת את מוני התהליך. הקטע הבא משתמש באותו פרויקט עם libc וב־io שכבר יובא.

fn faults() -> io::Result<(i64, i64)> {
    let mut usage = std::mem::MaybeUninit::<libc::rusage>::uninit();
    if unsafe { libc::getrusage(libc::RUSAGE_SELF, usage.as_mut_ptr()) } == -1 {
        return Err(io::Error::last_os_error());
    }
    let usage = unsafe { usage.assume_init() };
    Ok((usage.ru_minflt as i64, usage.ru_majflt as i64))
}
fn main() -> io::Result<()> {
    let page = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
    if page <= 0 { return Err(io::Error::other("page size unavailable")); }
    let page = page as usize;
    let mut memory = Mapping::new(page * 4096)?;
    let before = faults()?;
    for byte in memory.bytes_mut().iter_mut().step_by(page) {
        unsafe { std::ptr::write_volatile(byte, 1); }
    }
    let first = faults()?;
    for byte in memory.bytes_mut().iter_mut().step_by(page) {
        unsafe { std::ptr::write_volatile(byte, 2); }
    }
    let second = faults()?;
    println!("first minor={} major={}", first.0-before.0, first.1-before.1);
    println!("second minor={} major={}", second.0-first.0, second.1-first.1);
    std::hint::black_box(memory.bytes());
    Ok(())
}

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

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

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

הצלחת mmap או הרחבת heap אינה הבטחה שכל כתיבה עתידית תמצא זיכרון פיזי פנוי. מדיניות overcommit מאפשרת למערכת להתחייב ליותר כתובות מאשר זיכרון זמין כרגע, לפי הקונפיגורציה. כאשר הלחץ גדל, הקרנל עשוי לבצע reclaim, להשתמש ב־swap או להגיע להחלטות OOM. לכן אי אפשר להסתמך רק על Result של ההקצאה כעל תוכנית התאוששות מלאה ממחסור.

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

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

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

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