שליטה בנוכחות ובגודל של עמודים¶
נניח שיש לנו מסלול קצר שצריך להגיב מהר, והמדידה מראה קפיצות בזמן הנגיעה הראשונה בנתונים. אפשר להכין חלק מהעבודה מראש, אבל צריך להבין מה בדיוק מכינים. נעילת זיכרון, נגיעה מוקדמת ועמודים גדולים מטפלות בעלויות שונות, ואין אחת מהן שמבטיחה לבדה זמן תגובה קבוע לכל המערכת.
הפעולה mlock מבקשת להשאיר טווח בזיכרון בהתאם לחוזה המערכת, כדי שלא יפונה במסלולים הרגילים שבהם נצטרך להביאו מחדש. היא כפופה למגבלות משאבים ולהרשאות, ולכן כשל יכול להיות תוצאה צפויה של מדיניות ולא באג במצביע. מערכת שירות צריכה להחליט אם להיכשל באתחול או להמשיך במצב מוגבל כאשר הבקשה אינה מתקבלת.
העטיפה הבאה מצטרפת ל־Mapping מההרצאה השנייה. היא מחזיקה השאלה של המיפוי, ולכן הוא לא יכול להיעלם לפני ניסיון ביטול הנעילה.
struct Locked<'a> { mapping: &'a mut Mapping }
impl<'a> Locked<'a> {
fn new(mapping: &'a mut Mapping) -> io::Result<Self> {
if unsafe { libc::mlock(mapping.pointer.as_ptr().cast(), mapping.len) } == -1 {
return Err(io::Error::last_os_error());
}
Ok(Self { mapping })
}
fn bytes_mut(&mut self) -> &mut [u8] { self.mapping.bytes_mut() }
}
impl Drop for Locked<'_> {
fn drop(&mut self) {
unsafe { libc::munlock(self.mapping.pointer.as_ptr().cast(), self.mapping.len); }
}
}
fn main() -> io::Result<()> {
let mut mapping = Mapping::new(4096)?;
let mut locked = Locked::new(&mut mapping)?;
locked.bytes_mut().fill(1);
println!("small region locked");
Ok(())
}
הדוגמה נועלת אזור קטן בלבד. לפני הגדלתו בדקו את RLIMIT_MEMLOCK של תהליך המעבדה, למשל באמצעות ulimit -l ב־shell שממנו מפעילים אותו. שינוי הרשאות מנהל אינו התיקון הראשון לכל כשל. המטרה היא להבין אם המוצר באמת צריך נעילה, מה התקציב שלה וכיצד הוא מדווח כאשר אינו יכול לקיים את ההבטחה.
נעילות זיכרון אינן מונה הפניות פשוט לכל קריאה של האפליקציה. אם כמה רכיבים נועלים ומשחררים טווחים חופפים בלי תיאום, ביטול נעילה של אחד יכול לשנות את המצב שהאחר הסתמך עליו. העטיפה הלימודית מתאימה לבעלות בלעדית על אזור מוגדר, ואינה מנהל נעילות משותף לכל התהליך.
הכנה מוקדמת כוללת גם נגיעה בנתונים ובמחסניות לפני הכניסה למסלול הרגיש. אפילו אם באפר אחד נעול, התוכנית עדיין יכולה לבצע הקצאה חדשה, לטעון דף קוד שלא נגעו בו או להמתין לנעילה. לכן נמדוד את המסלול המלא אחרי ההכנה ונחפש מה עוד גורם ל־faults או להמתנה.
עמודים גדולים פותרים בעיה אחרת. כניסת TLB אחת יכולה לכסות טווח גדול יותר, ולכן עומס שנוגע בהרבה זיכרון עשוי להזדקק לפחות תרגומים. המחיר יכול לכלול הקצאה של יחידות גדולות יותר, fragmentation ועבודה בפיצול או דחיסה של זיכרון פיזי. שיפור ב־throughput אינו מבטיח שיפור בזנב ה־latency.
המנגנון THP מאפשר למערכת להשתמש בעמודים גדולים באופן שקוף לפי מדיניות וקונפיגורציה. הקריאה madvise עם עצה מתאימה יכולה להשפיע על הבחירה, אבל אינה הבטחה שהטווח קיבל עמוד גדול. מיפויים מפורשים עם huge pages משתמשים בחוזה ומשאבים אחרים, ולכן אין להחליף בין שני המנגנונים רק משום ששניהם כוללים את המילה huge.
בדקו את /proc/<pid>/smaps ואת הגדרות הסביבה הרלוונטיות, ואל תסיקו את כל מצב THP משדה יחיד בלי להבין את פירושו. למשל, נתון על כמות זיכרון anonymous שמגובה בעמודים גדולים ונתון על גודל עמוד שמדווח למיפוי אינם תמיד אותה שאלה. השוו גם מוני faults וזמני פעולות, משום שהמטרה היא להבין את העלות שנחסכה או נוספה.
ניסוי טוב מתחיל באזור ובדפוס גישה קבועים, משנה רק מדיניות אחת ושומר את שלב החימום בנפרד. אם נרצה לבדוק latency, נשמור התפלגות של זמני פעולות ולא רק ממוצע. הקצאה נדירה ויקרה יכולה להיעלם בממוצע ולהיות בדיוק הבעיה של המשתמש.
החוזה של נעילת הזיכרון מתועד בעמוד mlock, ומנגנון העמודים השקופים בתיעוד THP של הקרנל. ההחלטה התכנונית היא איזו עלות אנחנו מנסים להזיז או לצמצם, ומה המחיר שהמערכת משלמת במקום אחר.