פיתוח ראסט בתוך הקרנל¶
אותה שפה וסביבת ריצה אחרת¶
נניח שכתבנו שרת ראסט עם קבצים, תהליכונים וסביבת משימות, ואנחנו רוצים להעביר חלק ממנו לקרנל. אי אפשר להחליף רק את נקודת הכניסה. קוד קרנל אינו משתמש בספריית המשתמש הרגילה, אינו פותח קבצים דרך אותם ממשקי מערכת, וחי תחת מגבלות הקשר וסנכרון אחרות. הידע על בעלות והשאלות נשאר שימושי, אבל הוא צריך להתחבר להפשטות שהקרנל המסוים מספק.
הסביבה היא no_std, עם טיפוסים וממשקים של עץ הקרנל. שם כמו Vec יכול להגיע מהקדם־יבוא של הקרנל ולספק חוזה הקצאה שונה מהווקטור שהכרנו במרחב המשתמש. בפרק הזה נשתמש רק בממשקים שנמצאים בתג v6.12, ונקרא את המקור שלהם לפני הרחבה לממשק אחר.
התאמת שרשרת הכלים לעץ¶
קובץ דרישות הכלים של התג מציין בין השאר גרסת ראסט מינימלית ומינימום לכלי יצירת הקישוריות. התג הזה מציין rustc בגרסה 1.78.0 ואת bindgen בגרסה 0.65.1 כמינימום. זה אינו אומר שכל שילוב עתידי של מהדר, ספריית מקורות ומהדר סי בהכרח נבדק מול אותו עץ.
נכין שרשרת כלים מתאימה לפי מדריך גרסת הייחוס ונפעיל את בדיקת הזמינות של הבנייה. נשמור את הפלט ואת הגרסאות. ניצור תיקיית בנייה חדשה לניסוי ראסט כדי לא לערבב תוצרים עם הבנייה הקודמת.
rustc --version --verbose
bindgen --version
clang --version
rust_build="$PWD/build-rust-lab"
mkdir "$rust_build"
make LLVM=1 O="$rust_build" ARCH=x86_64 x86_64_defconfig
make LLVM=1 O="$rust_build" ARCH=x86_64 rustavailable
הבדיקה דורשת גם את מקורות ספריית השפה המתאימים ואת רכיבי התשתית הנדרשים. כשל צריך להיפתר לפני ניסיון לעקוף אותו בהעתקת קבצים מבנייה אחרת. כאשר נדרשת התאמה בין תשתית המהדר של ראסט לזו של כלי סי, נשתמש בהנחיות של אותו עץ ונאמת את הגרסאות בפועל.
מודול בעל משאבים¶
נוסיף קובץ בשם samples/rust/rust_amit.rs עם הקוד הבא. המודול אינו משתמש בממשק התקן שלא קיים בגרסה, אלא בונה אובייקט בעל משאבים ומדגים אתחול מוצלח וכשל באמצע. פעולת הדחיפה לווקטור מקבלת דגל הקצאה ומחזירה תוצאה שניתנת לכשל, בהתאם לממשק של התג.
use kernel::prelude::*;
module! {
type: RustAmit,
name: "rust_amit",
author: "AmitTech",
description: "Amit kernel ownership lab",
license: "GPL",
}
const FAIL_DURING_INIT: bool = false;
struct Marker;
impl Drop for Marker {
fn drop(&mut self) {
pr_info!("rust_amit marker released\n");
}
}
struct RustAmit {
marker: Marker,
values: Vec<u64>,
}
impl kernel::Module for RustAmit {
fn init(_module: &'static ThisModule) -> Result<Self> {
let marker = Marker;
let mut values = Vec::new();
for value in 1..=4 {
values.push(value * value, GFP_KERNEL)?;
if FAIL_DURING_INIT && value == 3 {
pr_info!("rust_amit rejecting partial initialization\n");
return Err(EINVAL);
}
}
pr_info!("rust_amit ready with {} values\n", values.len());
Ok(Self { marker, values })
}
}
impl Drop for RustAmit {
fn drop(&mut self) {
pr_info!("rust_amit module released with {} values\n", self.values.len());
let _ = &self.marker;
}
}
נוסיף אפשרות בנייה בקובץ האפשרויות של דוגמאות ראסט ושורת אובייקט בקובץ הבנייה של אותה תיקייה. הערכים הבאים הם תוספת מקומית למעבדה, ויש לשמור אותם כתיקון קטן מול התג המקורי.
נפעיל את האפשרויות המתאימות בתצורה ונבנה מחדש. יש להעתיק לבנייה הזאת גם את דרישות האתחול המינימלי שהגדרנו בתחילת הפרק, או להתאים את התצורה השמורה באופן מתועד. קרנל אחד ומודול מבנייה אחרת אינם זוג בדיקה תקף רק משום ששמות הקבצים דומים.
scripts/config --file "$rust_build/.config" -e RUST -e SAMPLES -e MODULES \
-m SAMPLE_RUST_AMIT
make LLVM=1 O="$rust_build" ARCH=x86_64 olddefconfig
make LLVM=1 O="$rust_build" ARCH=x86_64 -j2 bzImage modules
מה קורה בכשל אתחול¶
בגרסה המוצלחת נצפה להודעת מוכנות, ובהסרה להודעת ההשמדה של המודול ולהשמדת שדותיו. גוף פונקציית ההשמדה של המבנה פועל לפני השמדת שדותיו לפי כללי השפה. סדר שדות המבנה הוא חלק מהתכנון כאשר קיימת תלות בחיי שדה אחר, ולא פרט שאפשר להתעלם ממנו.
נשנה את הקבוע כדי להחזיר שגיאה אחרי שלוש דחיפות ונבנה שוב. הפעם האובייקט הסופי של המודול לא נוצר, ולכן פונקציית ההשמדה שלו אינה זו שמנקה את המצב החלקי. הערכים המקומיים שכבר נוצרו משתחררים לפי חיי התחום שלהם. נבדוק את הודעת הסמן ואת כישלון הטעינה כדי להבחין בין שני המסלולים.
הווקטור מטפל בזיכרון שהוא מחזיק, אבל הוא אינו יודע אם מסרנו מצביע לתוכן שלו לרכיב חיצוני. אם רכיב אחר ממשיך להשתמש במצביע, השמדה אוטומטית יכולה להיות מוקדמת מדי בדיוק כמו שחרור ידני שגוי. לכן העברת מצביעים דרך גבול בלתי בטוח דורשת חוזה חיים מעבר לכך שהמכל עצמו בטוח.
לקרוא הפשטה ולא רק לקרוא לה¶
נפתח את מימוש הווקטור ואת שכבת הסנכרון בעץ הייחוס. נזהה היכן הקריאה הבטוחה עוברת לממשק קרנל בלתי בטוח, איזה מידע הטיפוס שומר ואילו תנאים הקורא צריך לספק. לגבי הקצאה נבדוק כיצד דגלי ההקצאה מועברים ואיך כשל חוזר. לגבי נעילה נבדוק מה מחזיק השומר והאם יציבות הכתובת היא חלק מהדרישה.
rg -n 'pub.*Vec|fn push|GFP_KERNEL' rust/kernel samples/rust
rg -n 'pub.*Mutex|struct Guard|unsafe impl|PinInit' rust/kernel/sync
מציאת unsafe impl אינה אומרת שהקוד בהכרח מסוכן, אלא שההפשטה נושאת הוכחה שהמהדר אינו מבצע לבד. נרצה לראות כיצד היא מונעת שימוש מהקשר לא מתאים, איך היא מטפלת בשחרור ומה נשאר באחריות הקורא. טיפוס בטוח יכול להגביל סדר פעולות, אבל אינו חייב להכיר כל אילוץ חומרה או כל מדיניות של הדרייבר.
השוואה למימוש סי¶
נשווה את המודול למודול ההקצאה בשפת סי. שם כתבנו במפורש את מסלול השחרור כאשר ההקצאה השנייה נכשלת. כאן החזרת שגיאה מפעילה השמדה של ערכים מקומיים. בשני המקרים עדיין נדרש להבין אם ההקצאה רשאית לישן, מתי משאב פורסם ומי יכול להשתמש בו אחרי כשל.
ההבדל החשוב הוא אילו חובות קודדו בממשק ואילו נשארו כהסבר. ראסט יכולה למנוע שימוש בהפניה אחרי סוף חיי בעלים במודל שהיא רואה. היא אינה יודעת מעצמה שהתקן ממשיך לבצע גישה ישירה לזיכרון או שמערכת החזרות חיצונית שמרה כתובת. הגבול הזה הוא המקום שבו נדרשת סקירה מדויקת של הקוד הבלתי בטוח.
היקף התמיכה הוא חלק מהמוצר¶
לא נעתיק דוגמת דרייבר מגרסה חדשה ונניח שכל ההפשטות קיימות בתג שלנו. נבדוק את קובצי הדוגמה והטיפוסים בעץ שנבנה. אם ממשק דרוש אינו זמין, נוכל להשתמש במימוש סי עבור אותה שכבה ולהשאיר לקוח ראסט במרחב המשתמש, או לבחור במפורש עץ ייחוס חדש ולבדוק מחדש את כל החוזים.
בפרויקט הפרק ממשק האירועים הבסיסי ימומש בשכבת הקרנל הזמינה והלקוח יהיה בראסט. מודול הראסט הזה הוא חלק נפרד שחייב להיבנות ולהיבדק לפי הגרסה המדויקת. הפרדה כזאת מאפשרת ללמוד את השפה בתוך הקרנל בלי להציג ממשקים עתידיים כאילו כבר היו חלק מהבסיס שנבחר.
ההקמה מתוארת במדריך גרסת הייחוס, והממשק המעשי ניתן לבדיקה מול הדוגמה המקורית של אותו תג. בכל הרחבה נרשום את גרסת המקור ואת חובות ההפשטה, כדי שהקוד יהיה שיעור במודל חיים ולא אוסף שמות של טיפוסים.