לדלג לתוכן

מחזור חיי תהליך ובידוד משאבים

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

ה־job object מאפשר לנהל קבוצת processes יחד ולהחיל מגבלות מסוימות. הוא שימושי ל־supervisor, אך אינו sandbox מלא: הוא אינו מחליף token, הרשאות קבצים, בידוד רשת או מנגנוני אבטחה אחרים.

מכניסים ילד ל־job לפני שהוא רץ

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

התוכנית הבאה מפעילה עותק של עצמה כילד שישן לזמן מוגבל. ה־job שנוצר מכיל רק את ילדי הניסוי. שומרים אותה כ־job_demo.cpp.

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <cstdio>
#include <cwchar>
#include <string>
#include <vector>

struct Handle {
    HANDLE value = nullptr;
    explicit Handle(HANDLE h = nullptr) : value(h) {}
    ~Handle() { if (value) CloseHandle(value); }
    Handle(const Handle&) = delete;
    Handle& operator=(const Handle&) = delete;
};

int wmain(int argc, wchar_t** argv) {
    if (argc == 2 && std::wcscmp(argv[1], L"--child") == 0) {
        Sleep(500);
        return 7;
    }
    Handle job(CreateJobObjectW(nullptr, nullptr));
    if (!job.value) return 1;
    JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits{};
    limits.BasicLimitInformation.LimitFlags =
        JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE | JOB_OBJECT_LIMIT_ACTIVE_PROCESS;
    limits.BasicLimitInformation.ActiveProcessLimit = 2;
    if (!SetInformationJobObject(job.value, JobObjectExtendedLimitInformation,
                                 &limits, sizeof(limits))) return 2;

    std::vector<wchar_t> path(32768);
    const DWORD length = GetModuleFileNameW(
        nullptr, path.data(), static_cast<DWORD>(path.size()));
    if (length == 0 || length >= path.size()) return 3;
    std::wstring command = L"\"" + std::wstring(path.data()) + L"\" --child";
    STARTUPINFOW startup{};
    startup.cb = sizeof(startup);
    PROCESS_INFORMATION process{};
    if (!CreateProcessW(path.data(), command.data(), nullptr, nullptr,
                        FALSE, CREATE_SUSPENDED, nullptr, nullptr,
                        &startup, &process)) return 4;
    Handle process_owner(process.hProcess);
    Handle thread_owner(process.hThread);

    if (!AssignProcessToJobObject(job.value, process.hProcess)) {
        const DWORD error = GetLastError();
        TerminateProcess(process.hProcess, 100);
        WaitForSingleObject(process.hProcess, INFINITE);
        std::printf("assign error=%lu\n", error);
        return 5;
    }
    if (ResumeThread(process.hThread) == static_cast<DWORD>(-1)) {
        const DWORD error = GetLastError();
        TerminateJobObject(job.value, 101);
        WaitForSingleObject(process.hProcess, INFINITE);
        std::printf("resume error=%lu\n", error);
        return 6;
    }
    DWORD result = WaitForSingleObject(process.hProcess, 2000);
    if (result == WAIT_TIMEOUT) {
        if (!TerminateJobObject(job.value, 102)) return 7;
        result = WaitForSingleObject(process.hProcess, INFINITE);
    }
    if (result != WAIT_OBJECT_0) return 8;
    DWORD exit_code = 0;
    if (!GetExitCodeProcess(process.hProcess, &exit_code)) return 9;
    std::printf("exit=%lu\n", exit_code);
}

ה־Handle הקטן בדוגמה מתאים ל־handles שמחזירים null בכשל. אין להשתמש בו אוטומטית ל־CreateFileW בלי להתאים את sentinel. המטרה היא לראות את סדר החיים, לא להחליף את הספרייה הכללית שבנינו בפרק הראשון.

שלב ה־rollback חשוב כמו המסלול התקין

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

במוצר צריך לבדוק גם את תוצאות פעולות ה־rollback ולדווח אם לא הצלחנו לאמת סיום. ההפניה לתהליך נשארת הכלי שלנו לדעת מה קרה; PID בלבד אינו תחליף. תרחישי כשל משניים נכללים בפרויקט supervisor.

השימוש ב־CREATE_SUSPENDED אינו הוכחה ששום קוד מערכת לא פעל ביצירה. הוא מונע מה־thread הראשוני של הילד לרוץ באופן רגיל עד ResumeThread, ובכך נותן לנו נקודה להחלת מדיניות לפני קוד המשתמש שלו.

קבוצת job קיימת יכולה להשפיע עלינו

ייתכן שההורה עצמו רץ בתוך job של סביבת עבודה, שירות או כלי פיתוח. ב־Windows מודרני קיימים nested jobs, אבל יש מגבלות ותנאים לשיוך. אם AssignProcessToJobObject נכשל, שומרים את השגיאה ובודקים את ההקשר במקום לבטל את ההגנה בשקט.

לא הפעלנו דגלי breakaway. בחירה כזו צריכה להיות חלק מפורש מחוזה ה־supervisor. ילד שיוצר נכדים מושפע מכללי ההורשה וה־job, ולכן בדיקת עץ תהליכים היא חלק מהמעבדה.

סיום שיתופי וסיום כפוי

במסלול שיתופי ההורה שולח בקשת stop, הילד מפסיק לקבל עבודה, מסיים פעולות נדרשות ומשחרר משאבים. אפשר לקבל אישור שהנתונים נשמרו ושהפרוטוקול נסגר.

ב־TerminateProcess או TerminateJobObject אין הבטחה להרצת destructors וקוד ניקוי יישומי. מערכת ההפעלה משחררת משאבי תהליך בהתאם לחוזים שלה, אך אינה יכולה להשלים עבור היישום transaction עסקית או flush שהיה אמור להתבצע בקוד שלו.

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

מגבלות משאבים אינן הרשאות

אפשר להגדיר מגבלות על מספר processes, זיכרון וזמן CPU לפי סוגי המידע הנתמכים. לכל מגבלה יש יחידות ותנאי אכיפה משלה. מגבלת working set אינה זהה למגבלת commit, ואחוז CPU אינו הבטחה לזמן תגובה.

ה־job בדוגמה אינו מונע מהילד לפתוח קובץ שה־token שלו רשאי לפתוח. הוא גם אינו משנה אוטומטית את זהותו. בפרק האבטחה נוסיף הפרדה בין פיקוח על משאבים לבין הגבלת גישה.

מצב הסיום נשמר אחרי שהקוד הפסיק

אובייקט process יכול להישאר קיים לצורך שאילתת exit status כל עוד יש אליו handles. זה ממשיך את המודל מהשיעור הקודם: סוף ביצוע וסוף חיי אובייקט הקרנל אינם אותה נקודה.

לקריאה נוספת: ניהול processes באמצעות jobs, כללי השיוך ל־job וחוזה סיום תהליך כפוי.