תיאום גישה ומעקב אחר שינויים¶
נניח ששני תהליכים רוצים לעדכן קובץ תצורה, וכל אחד רואה שהקובץ כרגע פנוי. בלי פרוטוקול משותף שניהם יכולים להחליף זה לזה את התוצאה. מצד שני, גם נעילה תקינה אינה מספרת לנו אילו שינויים התרחשו מאז הקריאה הקודמת. תיאום כתיבה ומעקב אחר אירועים הם שני מנגנונים שונים, ונצטרך להבין את הגבולות של כל אחד.
נעילות קבצים רבות הן advisory: הן מועילות כאשר המשתתפים משתמשים באותו פרוטוקול, אבל אינן מונעות מכל תוכנית אחרת לבצע כתיבה רגילה. לכן נעילה אינה הופכת קובץ חיצוני לאחסון בטוח מול כותב שאינו משתף פעולה. היא גם אינה מחליפה הרשאות שמגבילות מי רשאי לפתוח את הקובץ.
בלינוקס קיימות נעילות שמקושרות ל־open file description. הן שונות מנעילות רשומה מסורתיות שמקושרות לתהליך ובעלות כללי שחרור שעלולים להפתיע כאשר סוגרים מתאר אחר לאותו קובץ. הדוגמה הבאה, שדורשת libc, מבקשת נעילת כתיבה על כל הטווח דרך פתיחה אחת.
use std::{fs::OpenOptions, io, mem};
use std::os::fd::AsRawFd;
fn main() -> io::Result<()> {
let file = OpenOptions::new().read(true).write(true)
.create(true).truncate(false).open("lock-lab")?;
let mut lock: libc::flock = unsafe { mem::zeroed() };
lock.l_type = libc::F_WRLCK as _;
lock.l_whence = libc::SEEK_SET as _;
lock.l_start = 0;
lock.l_len = 0;
lock.l_pid = 0;
if unsafe { libc::fcntl(file.as_raw_fd(), libc::F_OFD_SETLK, &lock) } == -1 {
return Err(io::Error::last_os_error());
}
println!("lock held; press Enter to release");
let mut line = String::new();
io::stdin().read_line(&mut line)?;
drop(file);
Ok(())
}
הריצו שני עותקים באותה תיקיית תרגיל. הראשון מחזיק פתיחה נעולה, והשני מנסה לקבל נעילה מתחרה בלי להיחסם. בדקו את קוד השגיאה במקום להניח שכל כשל הוא הרשאה. כאשר הפתיחה האחרונה שמחזיקה את הנעילה נסגרת, הנעילה משתחררת לפי החוזה שלה.
שכפול מתאר באמצעות dup אינו יוצר בעל נעילה עצמאי עבור OFD locks, משום שהפתיחה משותפת. לעומת זאת, פתיחה חדשה של אותו נתיב יוצרת הקשר אחר שיכול להתחרות. אם ניצור שני guards לוגיים מעל אותה פתיחה ונאפשר לאחד לשחרר את הנעילה, השני עלול להמשיך תחת הנחה שגויה. צריך לנהל את הבעלות על הנעילה עצמה, ולא רק את חיי מספר ה־fd.
כעת נעבור למעקב. המנגנון inotify מספק תור אירועים על אובייקטים שנבחרו למעקב. הוא שימושי לרענון cache או להפעלת סריקה, אבל אינו יומן אמין ובלתי מוגבל של כל שינוי. אירועים יכולים להתאחד, ותור יכול לגלוש. במקרה של overflow צריך להניח שחלק מהידע המקומי התיישן ולבצע סריקה מתקנת.
אירוע של inotify כולל כותרת ואורך שם משתנה, והשם אינו בהכרח UTF-8. הפונקציה הבאה מפרשת buffer שנקרא מהמתאר בלי ליצור הפניה לא מיושרת למבנה סי. היא משתמשת ב־io מהדוגמה הקודמת.
fn inspect_events(mut bytes: &[u8]) -> io::Result<()> {
while !bytes.is_empty() {
if bytes.len() < 16 { return Err(io::ErrorKind::UnexpectedEof.into()); }
let mask = u32::from_ne_bytes(bytes[4..8].try_into().unwrap());
let cookie = u32::from_ne_bytes(bytes[8..12].try_into().unwrap());
let name_len = u32::from_ne_bytes(bytes[12..16].try_into().unwrap()) as usize;
let end = 16usize.checked_add(name_len).ok_or_else(|| io::Error::other("overflow"))?;
if end > bytes.len() { return Err(io::ErrorKind::UnexpectedEof.into()); }
if mask & libc::IN_Q_OVERFLOW != 0 {
println!("cache invalid: full rescan required");
} else {
let field = &bytes[16..end];
let length = field.iter().position(|&x| x == 0).unwrap_or(field.len());
println!("mask={mask:x} cookie={cookie} name={:?}", &field[..length]);
}
bytes = &bytes[end..];
}
Ok(())
}
כדי לחבר את הקטע למקור אמיתי, יוצרים מתאר באמצעות inotify_init1, מוסיפים watch באמצעות inotify_add_watch, קוראים לבאפר מספיק גדול ומעבירים את מספר הבייטים שהתקבל לפונקציה. ה־cookie יכול לעזור לקשר אירועי rename, אבל אינו פוטר אותנו מאובדן אירועים או משינוי נוסף שמתרחש לפני שהספקנו לפתוח את השם.
המנגנון fanotify מציע חוזים ויכולות אחרים, ולעיתים דורש הרשאות נוספות או תלוי בסוג האירוע ובמערכת הקבצים. אין לבחור בו רק משום שהוא נשמע כמו גרסה חזקה יותר. נתחיל בשאלה אם אנחנו צריכים הודעת שינוי, מידע על אובייקט או החלטת הרשאה במסלול גישה, ונבדוק את הממשק שמתאים לכך.
החוזים של הנעילות מופיעים בעמוד fcntl locks, והאירועים בעמוד inotify. מערכת נכונה יודעת גם מה לעשות כאשר המעקב מפסיק להיות שלם.