זהות מתארים ובעלות על משאבי קרנל¶
נניח ששתי פונקציות קיבלו שני מספרי fd שונים, ובכל זאת קריאה באחת משנה את המיקום של השנייה. המספרים שונים, אבל הם אינם בהכרח מייצגים שתי פתיחות עצמאיות. כדי לתכנן בעלות נכונה נפריד בין תא בטבלת המתארים של התהליך, אובייקט פתיחה בקרנל והקובץ שאליו האובייקט מתייחס.
כל פתיחה רגילה יוצרת open file description שמחזיקה מצב כגון offset ו־file status flags. המתאר הוא מספר שמפנה אליה מתוך טבלת התהליך. הפעולה dup יוצרת מתאר נוסף שמפנה לאותה פתיחה, ולכן אינה יוצרת offset עצמאי. לעומת זאת, פתיחה נוספת של אותו נתיב בדרך כלל יוצרת אובייקט פתיחה אחר, אף ששניהם מתייחסים לאותו inode.
צרו קובץ ניסוי בשם letters.txt שמכיל abcdefgh, והפעילו עליו את התוכנית הבאה. שמרו את הקובץ בתיקיית תרגיל ייעודית כדי שלא תחליפו תוכן קיים.
use std::fs::File;
use std::io::{self, Read};
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd};
fn report(fd: BorrowedFd<'_>) { println!("descriptor={}", fd.as_raw_fd()); }
fn main() -> io::Result<()> {
let mut first = File::open("letters.txt")?;
let mut duplicate = first.try_clone()?;
let mut independent = File::open("letters.txt")?;
let mut a = [0; 2];
let mut b = [0; 2];
let mut c = [0; 2];
first.read_exact(&mut a)?;
duplicate.read_exact(&mut b)?;
independent.read_exact(&mut c)?;
assert_eq!(&a, b"ab");
assert_eq!(&b, b"cd");
assert_eq!(&c, b"ab");
let owned: OwnedFd = first.into();
report(owned.as_fd());
drop(duplicate);
report(owned.as_fd());
Ok(())
}
ההמרה מ־File ל־OwnedFd מעבירה בעלות בלי לפתוח מחדש את הקובץ. ה־Drop של הבעלים החדש יסגור את המתאר. הקריאה as_fd מחזירה BorrowedFd שזמן החיים שלו קשור לבעלים, ולכן פונקציה בטוחה שמקבלת אותו אינה יכולה להניח שהמתאר חי לנצח. ההבדל בין העברה להשאלה מופיע כאן בטיפוס, בדיוק כפי שהופיע בזיכרון רגיל.
המתודה as_raw_fd מחזירה מספר בלבד. המספר אינו נושא בעלות ואינו מאריך חיים. אם נשמור אותו, נשמיד את הקובץ ואז נשתמש במספר, המערכת עשויה כבר להקצות אותו למשאב אחר. זאת אינה רק שגיאה של מספר לא תקין, אלא אפשרות לבצע פעולה מוצלחת על הקובץ הלא נכון.
הבנאי OwnedFd::from_raw_fd הוא unsafe משום שהוא מקבל אחריות סגירה. אסור לקרוא לו על מספר שהתקבל מ־file.as_raw_fd() בזמן שה־File עדיין בעלים. במקרה כזה שני destructors ינסו לסגור אותו, וביניהם המספר עשוי להתמחזר. הדרך הנכונה להעברה היא into_raw_fd בצד המוסר ו־from_raw_fd בצד המקבל, או המרה בטוחה קיימת שמבצעת את אותו מעבר בלי לחשוף מספר.
גם שכפול צריך להיות מפורש. אם רוצים שני בעלים שמסוגלים להיסגר בנפרד, משתמשים בפעולה שיוצרת מתאר חדש כגון try_clone, ולא מעתיקים את המספר. לאחר השכפול כל בעלים סוגר את התא שלו, אבל מצב הפתיחה המשותף ממשיך לחיות כל עוד קיימת אליו הפניה רלוונטית.
הדגל FD_CLOEXEC שייך למתאר, ואילו דגל כמו O_NONBLOCK שייך למצב הפתיחה המשותף. לכן שינוי מצב חסימה דרך אחד מהמתארים המשוכפלים יכול להשפיע על השני, בעוד שהגדרת close-on-exec למתאר אחד אינה בהכרח משנה את האחר. בפרק הבא נשתמש בהבחנה הזאת כדי להבין ירושה לתהליך בן.
סגירה מתהליכון אחד אינה מנגנון ביטול כללי של פעולה שחסומה בתהליכון אחר. בלינוקס פעולה פעילה יכולה להחזיק הפניה לאובייקט הפתיחה ולהשלים גם אחרי שהמתאר נסגר בטבלה. בנוסף, שימוש חוזר במספר יוצר סיכון במעקב שמסתמך על המספר בלבד. ביטול צריך להיות חלק מפרוטוקול ברור ולא תופעת לוואי של close אקראי.
הטיפוסים של ראסט עוזרים למנוע חלק ממרוצי החיים בקוד בטוח, אבל אינם פותרים אוטומטית את השיתוף בתוך הקרנל. שתי עטיפות בטוחות של מתארים משוכפלים עדיין יכולות להתחרות על offset. כאשר רוצים גישה לטווחים עצמאיים, נבחן בפרק הקבצים פעולות ממוקמות כמו pread ו־pwrite.
ההבחנה בין מתאר לפתיחה משותפת מתועדת בעמוד open ובעמוד dup. בכל תרגיל ציירו שני חצים נפרדים: מהמשתנה לבעל המתאר, ומהמתאר לאובייקט הקרנל.