הקצאה פגיעות דף ומטמון הקרנל¶
יש זיכרון פנוי ובכל זאת ההקצאה נכשלת¶
נניח שהמערכת מדווחת על דפים פנויים, אבל בקשת הקצאה בקרנל נכשלת. ייתכן שהבקשה דורשת רצף פיזי גדול שאינו זמין, שהיא מוגבלת לאזור מסוים או שהיא נעשית בהקשר שאינו מאפשר מאמץ להשבת זיכרון. המספר הכולל של בתים פנויים אינו מתאר את כל התנאים של הקצאה מסוימת.
נפריד בין ארבע שאלות: מהו הגודל, האם נדרשת רציפות פיזית, אילו אזורי זיכרון מתאימים, והאם מותר לישן בזמן הטיפול. שאלה נוספת היא למי מחייבים את ההקצאה כאשר היא נגרמת על ידי בקשת משתמש. הקרנל צריך להגן גם על עצמו וגם על שאר המערכת מפני צריכה שמתרחשת בשם תהליך מוגבל.
דפים, אזורים ומקצה זוגות¶
מקצה הדפים מנהל גושים בגדלים הקשורים לחזקות של שתיים במספר דפים. בקשה מסדר גבוה דורשת גוש מתאים, ולכן פיצול יכול להקשות עליה גם אם הסכום הכולל מספיק. מיזוג גושים פנויים תלוי בכך שהחלקים המתאימים זמינים. דפים תפוסים בין גושים חופשיים אינם הופכים לרצף רק משום שסכומם גדול.
אזורים וצומתי זיכרון מתארים מגבלות כתובת וטופולוגיה. במערכת בעלת כמה צמתים, גישה לזיכרון מרוחק יכולה להיות יקרה יותר. מגבלות של התקן או של ארכיטקטורה יכולות להקטין עוד את קבוצת הדפים המתאימים. בפרק ההתקנים נלמד מדוע ממשק מיפוי ייעודי חשוב יותר מניסיון לבחור ידנית כתובת שנראית מתאימה להתקן.
אובייקטים קטנים ומרחבים רציפים¶
משפחת ההקצאות kmalloc מספקת אזור שמתאים לשימושי קרנל רבים, עם רציפות פיזית לפי חוזה ההקצאה המתאים. מקצי אובייקטים יכולים למחזר מקומות בגדלים מוגדרים כדי לצמצם עלות ופיצול פנימי. הגודל שהמקצה מחזיק בפועל יכול להיות שונה מהגודל שביקשנו, אבל אין להשתמש בזה כתירוץ לכתיבה מעבר לגבול הלוגי של האובייקט.
משפחת vmalloc מספקת טווח כתובות קרנל רציף בלי לדרוש שכל הדפים הפיזיים יהיו צמודים. הדבר דורש ניהול מיפויים ודפים נוסף. מצביע כזה אינו ניתן להמרה שרירותית לכתובת פיזית אחת שמייצגת את כל הטווח. בחירת המקצה תלויה במשתמש של הזיכרון, ולא רק בשאלה איזו פונקציה מצליחה להחזיר מצביע.
מודול הקצאה קטן¶
ניצור תיקיית מודול נפרדת ונשמור את הקוד הבא בשם alloc_probe.c. המודול נבנה מול תוצרי גרסת v6.12 מהמעבדה, ונטען רק בקרנל הניסיוני. הוא מחזיק שתי הקצאות עד לפריקה ומדווח כמה פעמים דפים סמוכים במרחב הווירטואלי אינם סמוכים במספור הפיזי.
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/vmalloc.h>
#include <linux/mm.h>
#include <linux/string.h>
static void *small;
static void *large;
static int __init alloc_probe_init(void)
{
unsigned long previous = 0;
unsigned int gaps = 0;
unsigned int index;
small = kmalloc(32768, GFP_KERNEL);
if (!small)
return -ENOMEM;
large = vmalloc(128 * PAGE_SIZE);
if (!large) {
kfree(small);
small = NULL;
return -ENOMEM;
}
memset(small, 0x5a, 32768);
memset(large, 0, 128 * PAGE_SIZE);
for (index = 0; index < 128; index++) {
struct page *page = vmalloc_to_page((char *)large + index * PAGE_SIZE);
unsigned long current = page_to_pfn(page);
if (index && current != previous + 1)
gaps++;
previous = current;
}
pr_info("alloc_probe requested=32768 actual=%zu vmalloc_gaps=%u\n",
ksize(small), gaps);
return 0;
}
static void __exit alloc_probe_exit(void)
{
vfree(large);
kfree(small);
pr_info("alloc_probe released both allocations\n");
}
module_init(alloc_probe_init);
module_exit(alloc_probe_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Amit allocation context lab");
ניצור קובץ בנייה בשם Makefile עם שורת האובייקט, ונפעיל את מערכת הבנייה של הקרנל. המשתנים בדוגמה צריכים להצביע לעץ המקור ולתיקיית התוצרים שבנינו, והפקודה רצה מתוך תיקיית המודול.
נוסיף את הקובץ alloc_probe.ko לארכיון האתחול, נאתחל ונפעיל insmod ו־rmmod מתוך האורח. נבדוק את היומן בעזרת dmesg. מספר הפערים יכול להיות אפס או גדול מאפס, משום שהמקצה אינו מבטיח שהדפים יהיו מפוזרים בכל ריצה. הניסוי מדגים כיצד לבדוק את המיפוי, ולא מייצר הבטחה שכל הקצאה וירטואלית חייבת להיות מפוצלת פיזית.
דגל הקצאה הוא חוזה הקשר¶
הדגל GFP_KERNEL מאפשר מסלולי השבת זיכרון שעלולים להמתין. הוא מתאים למקומות שבהם מותר לישן, כמו שלב האתחול של המודול שלנו, אך אינו מתאים אוטומטית לכל מטפל אירוע. דגלים שאינם מאפשרים המתנה מצמצמים את הדרכים שבהן המקצה יכול להשיג זיכרון, ולכן כשל הוא תוצאה שצריך לתכנן לה.
שימוש בדגל המאפשר עתודות אינו הופך הקצאה למובטחת. יש להחליט אם צריכת עתודות מוצדקת ומה יקרה אם גם היא נכשלת. עבור אירוע שאי אפשר לדחות, לעיתים הפתרון הוא הקצאה מראש או מאגר מוגבל שמוכן לפני תחילת הפעילות. השינוי הארכיטקטוני יכול להיות חשוב יותר מהחלפת דגל בבקשת ההקצאה.
הקצאה שנובעת מקלט משתמש יכולה לדרוש חשבונאות לקבוצת הבקרה המתאימה. אם הדרייבר יוצר אובייקטים בלי גבול בשם משתמש, מגבלה על הזיכרון במרחב המשתמש לבדה אינה מספיקה. נבחר ממשק ודגלי חשבונאות מתאימים ונבדוק תחת מכסה בפועל.
פגיעת דף אינה בהכרח שגיאת תוכנית¶
כאשר המעבד אינו יכול להשלים גישה לפי טבלאות הדפים הנוכחיות, הקרנל בוחן את אזור הזיכרון ואת סוג הגישה. ייתכן שהגישה חוקית אבל הדף טרם הוקצה, שהוא צריך להיקרא מקובץ או שכתיבה דורשת עותק פרטי. רק חלק מהמצבים מסתיימים באות שגיאה לתהליך.
נבחן העתקה בזמן כתיבה דרך תהליך ילד. התוכנית הבאה משתמשת במיפוי פרטי אנונימי, כותבת ערך לפני יצירת הילד ומוודאת ששינוי הילד אינו משנה את ערך ההורה. היא מיועדת לתהליך חד־תהליכי, והילד מבצע רק את הפעולות המינימליות לפני יציאה מיידית.
use std::io;
struct Mapping(*mut libc::c_void, usize);
impl Drop for Mapping {
fn drop(&mut self) { unsafe { libc::munmap(self.0, self.1); } }
}
fn main() -> io::Result<()> {
let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
if page_size <= 0 { return Err(io::Error::other("page size")); }
let length = page_size as usize;
let address = unsafe { libc::mmap(std::ptr::null_mut(), length,
libc::PROT_READ | libc::PROT_WRITE, libc::MAP_PRIVATE | libc::MAP_ANONYMOUS, -1, 0) };
if address == libc::MAP_FAILED { return Err(io::Error::last_os_error()); }
let mapping = Mapping(address, length);
unsafe { mapping.0.cast::<u8>().write_volatile(7); }
let child = unsafe { libc::fork() };
if child == -1 { return Err(io::Error::last_os_error()); }
if child == 0 {
unsafe {
mapping.0.cast::<u8>().write_volatile(9);
libc::_exit(if mapping.0.cast::<u8>().read_volatile() == 9 { 0 } else { 1 });
}
}
let mut status = 0;
loop {
if unsafe { libc::waitpid(child, &mut status, 0) } != -1 { break; }
let error = io::Error::last_os_error();
if error.kind() != io::ErrorKind::Interrupted { return Err(error); }
}
assert!(libc::WIFEXITED(status));
assert_eq!(libc::WEXITSTATUS(status), 0);
assert_eq!(unsafe { mapping.0.cast::<u8>().read_volatile() }, 7);
Ok(())
}
התוצאה מוכיחה הפרדה של השינוי הפרטי, אך אינה חושפת ישירות את מספר הדף הפיזי בכל שלב. כדי לחקור את המנגנון נחבר אותה למסלול פגיעת הדף ולמדדים מתאימים, בלי להניח שכל קריאת חשבונאות חושפת כתובות פיזיות למשתמש בלתי מיוחס.
קובץ, דף מלוכלך וכתיבה לאחסון¶
במיפוי קובץ מעורבים אזור הזיכרון של התהליך, אובייקט הקובץ ומטמון הדפים. שינוי שמסמן דף כמלוכלך אינו אומר שהמידע כבר הגיע להתקן. מסלול הכתיבה החוזרת מנהל הוצאה של דפים לאחסון, ולחץ יכול לגרום לתהליכים להמתין או לצמצם קצב יצירת דפים מלוכלכים.
נעקוב בעץ המקור אחרי טיפול בפגיעת דף, חיפוש דף קובץ ובקשת כתיבה חוזרת. נציין היכן מוחזקת הפניה לדף והיכן יכולה להתרחש המתנה. תצפית על קריאת write מוצלחת או שינוי במיפוי אינה מחליפה את חוזה הסנכרון שלמדנו בפרק האחסון.
הבחירה במקצה ובדגלים מתועדת במדריך ההקצאות, ומבנה האזורים מוסבר בתיעוד הזיכרון הפיזי. בכל מסלול שנקרא נשאל גם מה קורה כאשר אין משאב זמין, משום שנתיב הכשל הוא חלק מהתכנון של הקרנל ולא מקרה שאפשר להשאיר לסוף.