לדלג לתוכן

טעינת תוכנית וסיום תהליך

נניח שאנחנו מפעילים כלי עזר ומקבלים קוד סיום 127. האם הכלי רץ והחזיר את הקוד, או שבכלל לא הצלחנו לטעון אותו? אם המפקח מערבב את שני המצבים, הוא עלול להציג כשל התקנה כשגיאה בקלט של המשתמש. נבנה מסלול שמפריד בין כשל יצירה או טעינה, סיום רגיל וסיום באמצעות אות.

הפעולה execve מחליפה את תמונת התוכנית של התהליך הקיים. היא אינה יוצרת PID חדש בעצמה. בהצלחה היא אינה חוזרת לתוכנית הישנה, ובכשל היא חוזרת עם שגיאה. ה־heap והקוד הישנים נעלמים בהצלחה, בעוד שחלק ממאפייני התהליך והמתארים שאינם close-on-exec ממשיכים לפי כללי הירושה.

הממשק Command של ראסט מספק דרך נוחה לשמור על ההבחנה בין כשל הפעלה לבין תוצאת התוכנית. הקוד הבא הוא מפקח מינימלי שמקבל נתיב לתוכנית וארגומנטים, ואינו מעביר אותם דרך shell.

use std::{env, io};
use std::process::{Command, Stdio};
use std::os::unix::process::ExitStatusExt;

fn main() -> io::Result<()> {
    let mut args = env::args_os().skip(1);
    let program = args.next().ok_or_else(||
        io::Error::new(io::ErrorKind::InvalidInput, "missing program"))?;
    let mut child = Command::new(program)
        .args(args)
        .env_clear()
        .env("PATH", "/usr/bin:/bin")
        .stdin(Stdio::null())
        .spawn()?;
    println!("started pid={}", child.id());
    let status = child.wait()?;
    match (status.code(), status.signal()) {
        (Some(code), _) => println!("normal exit code={code}"),
        (_, Some(signal)) => println!("terminated by signal={signal}"),
        _ => println!("other wait status={status:?}"),
    }
    Ok(())
}

נסו להפעיל נתיב שאינו קיים, ואחר כך תוכנית קיימת שמחזירה קוד שאינו אפס. במקרה הראשון spawn מחזירה שגיאה. במקרה השני היא מחזירה ילד, ורק wait מדווחת את תוצאתו. אל תמירו את שניהם לאותו מספר בלי לשמור את סוג הכשל.

הארגומנטים מועברים כמערך ולא כמחרוזת פקודה. לכן ארגומנט שמכיל רווח או * אינו מתפצל או מתרחב אוטומטית. אם בוחרים במפורש להריץ shell עם -c, מוסיפים שפה נוספת עם כללי ציטוט והרחבה משלה. עבור מפקח כללי נעדיף לשמור את גבול הארגומנטים שהמשתמש מסר.

הסביבה היא חלק מחוזה ההפעלה. הסרתה בדוגמה מייצרת ניסוי צפוי יותר, אבל יכולה לשבור כלי שמסתמך על HOME, locale או משתנה תצורה. במוצר צריך להגדיר רשימת משתנים ומדיניות ירושה שמתאימה לכלי, ולא למחוק או להעתיק הכול בלי החלטה. גם ספריית העבודה משפיעה על נתיבים יחסיים ולכן כדאי להגדיר אותה כאשר היא חלק מהבקשה.

ברמה הנמוכה אפשר להבחין בכשל exec באמצעות צינור שצד הכתיבה שלו מסומן close-on-exec. הילד כותב לתוכו קוד שגיאה אם הטעינה נכשלת. בהצלחה הטעינה סוגרת את המתאר, והאב רואה סוף קלט. הפרוטוקול צריך להבדיל בין קוד שגיאה שלם, נתון חלקי וסגירה בלתי צפויה, ולשמור על מגבלות הפעולות בחלון שאחרי fork.

הפעולה posix_spawn מאפשרת לתאר חלק מפעולות ההכנה באופן מוגדר, והמימוש יכול לבחור מסלול יעיל יותר מ־fork כללי. אין להניח שהמימוש של Command תמיד משתמש באותה קריאת מערכת בכל גרסה. החוזה הציבורי של ההפעלה חשוב יותר מהשם הפנימי שנראה בהרצה אחת של strace.

תהליך שסיים עדיין צריך שמישהו יאסוף את סטטוס הסיום שלו. לפני האיסוף הוא יכול להישאר כ־zombie: אינו מריץ קוד משתמש, אבל שומר מידע שמאפשר לאב לקבל את התוצאה. השמדת ערך Child בראסט אינה הבטחה להריגה או ל־wait אוטומטי. לכן מפקח צריך להגדיר מי אחראי לאיסוף גם במסלול שגיאה.

המזהה PID יכול להיות ממוחזר לאחר שחיי הזהות הישנה מסתיימים. מתאר pidfd מאפשר להחזיק הפניה לזהות תהליך מסוימת ולשלב אותה בהמתנה או בשליחת אות מתאימה. אם כבר אספנו ילד וזרקנו את הזהות, פתיחת pidfd מאוחרת לפי אותו מספר אינה מחזירה קסם את התהליך המקורי. את ההפניה צריך להשיג ולשמור בתוך פרוטוקול החיים.

החוזים של החלפת תוכנית ואיסוף תוצאה מתועדים בעמוד execve ובעמוד wait. בדוח של מפקח תמיד נשמור את השאלה אם התוכנית נטענה בכלל, ולא רק את השאלה אם המספר האחרון היה אפס.