מסירת אות וביצוע עבודה בהקשר בטוח¶
נניח שאות מגיע בדיוק בזמן שהתוכנית מעדכנת את ה־buffer של stdout. אם ה־handler קורא שוב לפונקציית הדפסה שמשתמשת באותו מצב פנימי, הוא עלול לראות מצב חלקי או להמתין לנעילה שהתהליכון המופרע עצמו מחזיק. הבעיה אינה דורשת שני תהליכונים. כניסה אסינכרונית לאותו מצב מספיקה.
לכן handler אינו callback רגיל של האפליקציה. המערכת יכולה להפעיל אותו בנקודה שבה כמעט שום אינווריאנט מקומי שלנו אינו מובטח. הרשימה של פעולות async-signal-safe מוגדרת עבור הממשק הרלוונטי, ואינה כוללת באופן כללי הקצאה, עיצוב מחרוזות או שימוש ב־mutex. גם פונקציה שנראית קצרה יכולה לקרוא בפנים לספרייה שאינה בטוחה בהקשר הזה.
במקום לבצע את עבודת הסגירה ב־handler, נבחר לקבל אות בהקשר רגיל באמצעות sigwait. הדוגמה הבאה דורשת libc, והיא תוכנית חד־תהליכונית שחוסמת מראש את האותות שאותם היא רוצה לקבל באופן סינכרוני.
use std::{io, mem::MaybeUninit, ptr};
fn main() -> io::Result<()> {
let mut storage = MaybeUninit::<libc::sigset_t>::uninit();
if unsafe { libc::sigemptyset(storage.as_mut_ptr()) } == -1 {
return Err(io::Error::last_os_error());
}
let mut set = unsafe { storage.assume_init() };
for signal in [libc::SIGUSR1, libc::SIGINT, libc::SIGTERM] {
if unsafe { libc::sigaddset(&mut set, signal) } == -1 {
return Err(io::Error::last_os_error());
}
}
let status = unsafe { libc::pthread_sigmask(libc::SIG_BLOCK, &set, ptr::null_mut()) };
if status != 0 { return Err(io::Error::from_raw_os_error(status)); }
println!("pid={} waiting", std::process::id());
loop {
let mut signal = 0;
let status = unsafe { libc::sigwait(&set, &mut signal) };
if status != 0 { return Err(io::Error::from_raw_os_error(status)); }
println!("received signal={signal}");
if signal == libc::SIGINT || signal == libc::SIGTERM { break; }
}
println!("cleanup in ordinary execution context");
Ok(())
}
הריצו את התוכנית ושלחו אליה SIGUSR1 ממסוף אחר. לאחר מכן שלחו SIGTERM ובדקו שהקוד יוצא דרך מסלול הניקוי הרגיל. ההדפסה כאן מותרת משום שהיא מתבצעת אחרי חזרה מ־sigwait, ולא בתוך handler שנכנס באמצע פעולת ספרייה אחרת.
שימו לב לאופן דיווח השגיאה. הפונקציות pthread_sigmask ו־sigwait מחזירות קוד שגיאה ישירות בממשק שבו השתמשנו, ולכן אין לקרוא באופן אוטומטי את last_os_error עבורן. התבנית של כל קריאת libc אינה זהה, גם כאשר כולן נראות כמו פונקציות סי שמחזירות מספר.
במערכת רב־תהליכונית צריך לחסום את קבוצת האותות לפני יצירת התהליכונים הרלוונטיים, כדי שיירשו את ה־mask. אחר כך אפשר להקדיש תהליכון רגיל להמתנה. אם תהליכון אחר נשאר עם האות לא חסום, אות שמופנה לתהליך יכול להימסר אליו במקום להגיע למסלול שתכננו. ההגדרה היא חלק מאתחול המערכת, ולא תיקון מקומי רק סביב הקריאה ל־sigwait.
בלינוקס אפשר להשתמש גם ב־signalfd כדי לקרוא מידע על אותות דרך מתאר ולשלב אותו בלולאת אירועים. גם שם צריך לנהל את ה־mask, ואין בכך ביטול של כללי מסירת האות. היתרון הוא שמנגנון ההמתנה יכול לראות יחד קלט רשת, טיימר ובקשת עצירה, בלי לבצע לוגיקה מורכבת מתוך handler.
דפוס self-pipe משתמש ב־handler קטן שכותב הודעה לצינור, והלולאה הרגילה קוראת את ההודעה ומבצעת עבודה. הצינור צריך להיות לא חוסם בצד ה־handler, אחרת handler יכול להיתקע על צינור מלא. אם הכתיבה נכשלת עם EAGAIN, התכנון חייב להסביר איך נשמרת בקשת העצירה, למשל כמצב שמתאחד ולא כמונה של כל אירוע. גם חיי המתאר חשובים: אסור לסגור אותו ולהשאיר handler שיכתוב למספר שכבר הוקצה מחדש.
הקרנל צריך לשמור מצב שמאפשר לחזור מהמקום שהופרע. הוא בונה signal frame ומשתמש במנגנון חזרה מתאים, ובחלק מהתכנונים אפשר להגדיר alternate signal stack. מחסנית חלופית מועילה במיוחד לחקירת מצבים שבהם המחסנית הרגילה אינה זמינה, אבל אינה הופכת פונקציות לא בטוחות לבטוחות ואינה מתקנת corruption של התוכנית.
אל ננסה להפוך כל SIGSEGV לשגיאת Result רגילה. כשל גישה סינכרוני יכול להעיד שהאינווריאנטים של הזיכרון כבר הופרו, וחזרה לאותה הוראה עלולה לחזור מיד על הכשל. בתחקיר כזה נרצה לשמור אבחון מוגבל או לעצור בדיבאגר, לא להמשיך כאילו נכשל קובץ אופציונלי.
ההגבלות והפעולות המותרות מתועדות בעמוד signal-safety, וההמתנה הסינכרונית בעמוד sigwait. ההחלטה החשובה היא באיזה הקשר מתבצעת העבודה, ולא רק באיזו פונקציה רשמנו את ה־handler.