8.4 - מוכנות מתארים וזהות אירועים - הרצאה
מוכנות אינה תוצאה¶
המנוע שלנו יודע למי לקרוא שוב, אבל עדיין אינו יודע לחכות לסוקט או לצינור בלי ליצור תהליכון לכל פעולה. כאן נכנס מנגנון המוכנות של מערכת ההפעלה. אנחנו מבקשים מהקרנל לדווח כאשר פעולה מסוג מסוים עשויה להתקדם בלי לחסום. לאחר הדיווח אנחנו עדיין צריכים לבצע את הפעולה ולבדוק את התוצאה שלה.
ההבחנה הזאת מסבירה כמה תקלות שנראות בהתחלה סותרות. אפשר לקבל התראת קריאה ואז לפגוש EAGAIN, משום שצרכן אחר כבר לקח את הנתונים. אפשר לקבל מוכנות לכתיבה בלי להיות מסוגלים לשלוח את כל ההודעה. אפשר לקבל אירוע שקשור לסיום או לשגיאה, בלי שהיישום יקבל עוד בתים רגילים. התראה היא הזמנה לבדוק את המצב הנוכחי, ולא בעלות על כמות מסוימת של נתונים.
שלושה ממשקים ושלוש עלויות¶
הממשק select משתמש בקבוצות מתארים ומחזיר אותן כשהוא מסמן את המתארים המוכנים. הייצוג המסורתי באמצעות fd_set מגביל את המספרים שניתן לייצג בהתאם לספרייה ולמימוש. אסור להעביר אליו מספר שאינו נכנס במבנה רק משום שהתהליך הצליח לפתוח את המתאר.
הממשק poll מקבל מערך של מתארים ובקשות, בלי תלות באותו ייצוג של קבוצת ביטים. בכל קריאה אנחנו מוסרים שוב את רשימת העניין וקוראים את התוצאות מתוך המערך. כאשר מספר המתארים גדול ורובם שקטים, עצם העברת הרשימה ובדיקתה נעשית חלק מהעבודה.
הממשק epoll מחזיק מופע קרנלי שבו רושמים עניין לאורך זמן. היישום משנה את הרשימה באמצעות פעולות הוספה, שינוי והסרה, וממתין לקבוצת אירועים מוכנים. הדבר מתאים למנוע שמנהל חיבורים רבים, אבל אינו הופך את כל העלויות לקבועות או מבטל עלויות תזמון ונעילה. תמיד צריך למדוד לפי מספר החיבורים, קצב האירועים ושיעור העדכונים לרשימה.
שני אופני דיווח על אותו מצב¶
באופן מבוסס רמה, כל עוד התנאי נשאר אמיתי, ההמתנה יכולה לדווח עליו שוב. אם נשארו בתים בצינור, אפשר לקרוא רק חלק מהם, לחזור להמתנה ולקבל שוב מוכנות. התכונה הזאת פשוטה יחסית להבנה, אך לולאה שאינה צורכת את הנתונים עלולה לקבל התראה שוב ושוב בלי לעשות עבודה מועילה.
באופן מבוסס קצה, לא מתכננים את הלולאה מתוך הנחה שכל חזרה להמתנה תזכיר לנו את אותו מצב. הדפוס הבטוח הוא להשתמש במתאר לא חוסם ולבצע פעולות עד שמתקבל EAGAIN. אם עצרנו מוקדם יותר, האחריות לזכור שיש עוד עבודה עוברת אלינו. אפשר לשמור את החיבור בתור מוכנות פנימי ולהקצות לו תקציב נוסף, אבל אסור לשכוח אותו ולחכות בהכרח לאירוע חדש.
ניסוי שמשאיר בתים בצינור¶
הדוגמה הבאה מיועדת ללינוקס ומשתמשת ב־libc כדי לקבל את הגדרות הממשק של יעד הבנייה. צרו חבילת מעבדה והוסיפו את התלות. שמרו את קובץ הנעילה שנוצר, כדי שחזרות על הניסוי ישתמשו באותה גרסת תלות.
הקוד יוצר צינור חדש בכל ניסוי, כותב שישה בתים, קורא רק שלושה ובודק מה קורה בהמתנה נוספת. אין בתוכנית כותב או קורא מתחרה, ולכן קל לבודד את השפעת מצב הדיווח.
use std::io;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
fn owned(raw: i32) -> io::Result<OwnedFd> {
if raw < 0 { Err(io::Error::last_os_error()) }
else { Ok(unsafe { OwnedFd::from_raw_fd(raw) }) }
}
fn wait(epoll: &OwnedFd, events: &mut [libc::epoll_event]) -> io::Result<usize> {
loop {
let count = unsafe {
libc::epoll_wait(epoll.as_raw_fd(), events.as_mut_ptr(), events.len() as i32, 0)
};
if count >= 0 { return Ok(count as usize); }
let error = io::Error::last_os_error();
if error.kind() != io::ErrorKind::Interrupted { return Err(error); }
}
}
fn experiment(edge: bool) -> io::Result<()> {
let mut pipe = [-1; 2];
if unsafe { libc::pipe2(pipe.as_mut_ptr(), libc::O_NONBLOCK | libc::O_CLOEXEC) } < 0 {
return Err(io::Error::last_os_error());
}
let reader = owned(pipe[0])?;
let writer = owned(pipe[1])?;
let epoll = owned(unsafe { libc::epoll_create1(libc::EPOLL_CLOEXEC) })?;
let mut interest = libc::epoll_event {
events: (libc::EPOLLIN | if edge { libc::EPOLLET } else { 0 }) as u32,
u64: (3_u64 << 32) | 7,
};
if unsafe { libc::epoll_ctl(epoll.as_raw_fd(), libc::EPOLL_CTL_ADD,
reader.as_raw_fd(), &mut interest) } < 0 {
return Err(io::Error::last_os_error());
}
let bytes = b"abcdef";
let written = unsafe { libc::write(writer.as_raw_fd(), bytes.as_ptr().cast(), bytes.len()) };
if written < 0 { return Err(io::Error::last_os_error()); }
assert_eq!(written, 6);
let mut events = [libc::epoll_event { events: 0, u64: 0 }; 4];
assert_eq!(wait(&epoll, &mut events)?, 1);
let token = events[0].u64;
assert_eq!(token, (3_u64 << 32) | 7);
let mut part = [0_u8; 3];
let read = unsafe { libc::read(reader.as_raw_fd(), part.as_mut_ptr().cast(), part.len()) };
if read < 0 { return Err(io::Error::last_os_error()); }
assert_eq!(read, 3);
assert_eq!(&part, b"abc");
let remaining = wait(&epoll, &mut events)?;
println!("edge={edge}, repeated readiness={remaining}");
assert_eq!(remaining, if edge { 0 } else { 1 });
Ok(())
}
fn main() -> io::Result<()> {
experiment(false)?;
experiment(true)
}
הפלט הצפוי בתנאי הניסוי מתאר הבדל בדיווח, למרות שבשני המקרים נשארו שלושה בתים בצינור.
הבלוקים הלא בטוחים מעבירים מצביעים למבנים ולמערכים חיים בגודל המתאים. המתארים מועברים פעם אחת בלבד אל OwnedFd, שמחזיק את אחריות הסגירה. הקוד משתמש במבנה שסיפקה ספריית היעד במקום להמציא פריסה של epoll_event. קריאת השדה אל משתנה מקומי גם מונעת יצירת השאלה לשדה שיכול להיות לא מיושר במבנה ארוז.
מספר המתאר אינו הזהות של החיבור¶
נניח שטבלת החיבורים משתמשת במספר המתאר כמפתח יחיד. המנוע מקבל מערך אירועים, מטפל באחד מהם וסוגר חיבור. במהלך אותו סבב נפתח חיבור חדש ומקבל את אותו מספר. אירוע אחר שכבר נמצא במערך המקומי עלול עכשיו להיראות כאילו הוא שייך לחיבור החדש.
הפתרון הוא לתת לרישום זהות שאינה רק המספר. בדוגמה שמרנו בשדה הנתונים מספר משבצת ודור. כאשר משבצת ממוחזרת, מגדילים את הדור. לפני הפעלת אירוע משווים את שני החלקים למצב הנוכחי בטבלה. אירוע שדורותיו אינם מתאימים נזרק, גם אם המספר הגולמי של המתאר נראה מוכר.
הדור אינו קסם מתמטי אם מאפשרים לו להסתובב ולהשתמש שוב באותה זהות בזמן שאירועים ישנים עדיין יכולים להתקיים. צריך להגדיר מדיניות גלישה, למשל הפסקת שימוש במשבצת לפני גלישת המונה. גודל רחב מקטין את הסבירות, אבל ההוכחה נשענת על המדיניות ולא על תחושת ביטחון מהמספר הגדול.
הסרה וסגירה הן פעולות שונות¶
הרישום הקרנלי קשור גם לתיאור הקובץ הפתוח, ולא רק למספר שאנחנו רואים. שכפול מתאר יכול להשאיר את התיאור בחיים גם אחרי סגירת אחד המספרים. לכן אין לבנות מנגנון שמניח שכל close מסיר מייד ובאופן מוחלט כל עניין אפשרי. כאשר נדרשת הסרה מפורשת, מבצעים אותה כשהמתאר המתאים עדיין תקף, ואז מנהלים את הסגירה ואת מחיקת הזהות המקומית.
הסרה אינה מוחקת אירועים שכבר הועתקו למערך משתמש. בדיקת הדור נדרשת גם לאחר הצלחת ההסרה. באופן דומה, מצב EPOLLONESHOT עוזר לשלוט בחימוש מחדש, אך אינו מחליף פרוטוקול בעלות שמונע טיפול מקביל באותו חיבור.
התיעוד של מנגנון האירועים משלים את חוזי הרישום וההסרה. במימוש הבא נחבר למנגנון הזה גם זמן, ביטול והתקדמות חלקית, כדי שהלולאה לא תצטרך לבדוק כל דבר שוב באמצעות שעון מחזורי.