לדלג לתוכן

פתרון פרויקט מנהל תהליכי עבודה

הפתרון משתמש בשמות events מקומיים ל־session ובירושת handles כבויה. השמות מיועדים למעבדה שבה ההורה והילד רצים באותה זהות. מערכת בין משתמשים שונים צריכה security descriptor ופרוטוקול זהות מפורשים, שנלמד בפרק האבטחה.

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

מימוש מלא של שני התפקידים

שומרים את התוכנית בשם supervisor.cpp. מצב רגיל מופעל בלי ארגומנט, ומצב סיום כפוי מופעל עם --force.

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

struct Handle {
    HANDLE value = nullptr;
    explicit Handle(HANDLE raw = nullptr) : value(raw) {}
    ~Handle() { if (value) CloseHandle(value); }
    Handle(const Handle&) = delete;
    Handle& operator=(const Handle&) = delete;
    Handle(Handle&& other) noexcept
        : value(std::exchange(other.value, nullptr)) {}
    Handle& operator=(Handle&&) = delete;
};

struct Child {
    Handle process;
    Handle thread;
    bool observed_exit = false;

    explicit Child(const PROCESS_INFORMATION& info)
        : process(info.hProcess), thread(info.hThread) {}

    bool wait(DWORD timeout) {
        const DWORD result = WaitForSingleObject(process.value, timeout);
        if (result == WAIT_OBJECT_0) observed_exit = true;
        return observed_exit;
    }

    ~Child() {
        if (observed_exit) return;
        if (!TerminateProcess(process.value, 199)) {
            const DWORD error = GetLastError();
            if (WaitForSingleObject(process.value, 0) != WAIT_OBJECT_0)
                std::fprintf(stderr, "rollback terminate error=%lu\n", error);
        }
        if (WaitForSingleObject(process.value, 5000) != WAIT_OBJECT_0)
            std::fprintf(stderr, "rollback exit not confirmed\n");
    }
};

Handle new_event(const std::wstring& name) {
    HANDLE value = CreateEventW(nullptr, TRUE, FALSE, name.c_str());
    const DWORD error = GetLastError();
    if (!value) {
        std::fprintf(stderr, "event error=%lu\n", error);
        return Handle{};
    }
    if (error == ERROR_ALREADY_EXISTS) {
        CloseHandle(value);
        std::fprintf(stderr, "event name collision\n");
        return Handle{};
    }
    return Handle{value};
}

unsigned long long time_ticks(FILETIME value) {
    return (static_cast<unsigned long long>(value.dwHighDateTime) << 32) |
           value.dwLowDateTime;
}

int worker(int argc, wchar_t** argv) {
    if (argc != 4 && argc != 5) return 20;
    Handle ready(OpenEventW(EVENT_MODIFY_STATE, FALSE, argv[2]));
    Handle stop(OpenEventW(SYNCHRONIZE, FALSE, argv[3]));
    if (!ready.value || !stop.value) return 21;
    const bool ignore_stop = argc == 5;
    std::vector<unsigned char> state(4096, 7);
    if (!SetEvent(ready.value)) return 22;
    for (;;) {
        if (ignore_stop) {
            Sleep(100);
            continue;
        }
        const DWORD result = WaitForSingleObject(stop.value, 100);
        if (result == WAIT_OBJECT_0) break;
        if (result != WAIT_TIMEOUT) return 23;
        state[0] = static_cast<unsigned char>(state[0] + 1);
    }
    std::puts("worker cooperative cleanup");
    return 0;
}

int wmain(int argc, wchar_t** argv) {
    if (argc >= 2 && std::wcscmp(argv[1], L"--worker") == 0)
        return worker(argc, argv);
    const bool force = argc == 2 && std::wcscmp(argv[1], L"--force") == 0;

    const ULONGLONG started = GetTickCount64();
    const auto suffix = std::to_wstring(GetCurrentProcessId()) + L"." +
                        std::to_wstring(started);
    const auto ready_name = L"Local\\AmitTech.Ready." + suffix;
    const auto stop_name = L"Local\\AmitTech.Stop." + suffix;

    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;
    Handle ready = new_event(ready_name);
    Handle stop = new_event(stop_name);
    if (!ready.value || !stop.value) return 3;

    std::vector<wchar_t> path(32768);
    const DWORD path_length = GetModuleFileNameW(
        nullptr, path.data(), static_cast<DWORD>(path.size()));
    if (path_length == 0 || path_length >= path.size()) return 4;
    std::wstring command = L"\"" + std::wstring(path.data()) +
        L"\" --worker " + ready_name + L" " + stop_name;
    if (force) command += L" ignore";

    STARTUPINFOW startup{};
    startup.cb = sizeof(startup);
    PROCESS_INFORMATION info{};
    if (!CreateProcessW(path.data(), command.data(), nullptr, nullptr, FALSE,
                        CREATE_SUSPENDED, nullptr, nullptr, &startup, &info)) {
        std::fprintf(stderr, "create error=%lu\n", GetLastError());
        return 5;
    }
    Child child(info);
    if (!AssignProcessToJobObject(job.value, child.process.value)) {
        std::fprintf(stderr, "assign error=%lu\n", GetLastError());
        return 6;
    }
    if (ResumeThread(child.thread.value) == static_cast<DWORD>(-1)) {
        std::fprintf(stderr, "resume error=%lu\n", GetLastError());
        return 7;
    }
    HANDLE readiness[] = {ready.value, child.process.value};
    const DWORD ready_result =
        WaitForMultipleObjects(2, readiness, FALSE, 3000);
    if (ready_result != WAIT_OBJECT_0) {
        std::fprintf(stderr, "ready wait=%lu\n", ready_result);
        return 8;
    }

    Sleep(200);
    if (!SetEvent(stop.value)) return 9;
    bool forced = false;
    if (!child.wait(500)) {
        forced = true;
        if (!TerminateJobObject(job.value, 198)) {
            std::fprintf(stderr, "job terminate error=%lu\n", GetLastError());
            return 10;
        }
        if (!child.wait(5000)) {
            std::fprintf(stderr, "job exit not confirmed\n");
            return 11;
        }
    }

    DWORD exit_code = 0;
    if (!GetExitCodeProcess(child.process.value, &exit_code)) return 12;
    FILETIME creation{}, exit{}, kernel{}, user{};
    if (!GetProcessTimes(child.process.value, &creation, &exit, &kernel, &user))
        return 13;
    const auto cpu = time_ticks(kernel) + time_ticks(user);
    std::printf("mode=%s exit=%lu wall_ms=%llu cpu_ms=%.3f\n",
        forced ? "forced" : "cooperative", exit_code,
        static_cast<unsigned long long>(GetTickCount64() - started),
        static_cast<double>(cpu) / 10000.0);
    return forced == force ? 0 : 14;
}
cl /nologo /std:c++20 /EHsc /W4 /O2 /Zi supervisor.cpp /Fesupervisor.exe /link /DEBUG
.\supervisor.exe
.\supervisor.exe --force

הקוד אינו משנה processes חיצוניים. ה־Child מחזיק רק את ה־process handle שהוחזר מהיצירה שלו, וה־job נוצר ללא שם ומקבל רק את הילד הזה.

מפרשים את המצבים

לפני CreateProcess אין ילד שצריך לנקות. אחרי הצלחה, ה־Child הופך לבעלים של שני handles ומבטיח מסלול rollback אם יוצאים לפני שהסיום נצפה. לכן כשל ב־AssignProcessToJobObject אינו משאיר ילד suspended שנשכח.

ההמתנה ל־ready כוללת גם את process handle. אם הילד מסיים לפני שהוא מסמן מוכנות, ההורה מקבל אירוע סיום ויכול לדווח שהאתחול נכשל. אם שני האובייקטים מסומנים יחד, הבחירה לפי סדר המערך אינה הוכחת חיים מתמשכים, ולכן ההורה עדיין בודק את סוף התהליך בהמשך.

ה־Sleep הקצר אחרי ready מדמה חלון עבודה בלבד. הוא אינו משמש להוכחת מוכנות. הראיה למוכנות היא האיתות שנשלח אחרי שהילד הקצה את state.

הבעלות נשארת מפורשת

משאב בעלים נקודת שחרור
ה־job ההורה אחרי סיום ניהול הילד ויציאה מה־scope
אירועי ready ו־stop המקוריים ההורה אחרי סיום הילד
ההפניות שפתח הילד לאירועים הילד בסיום שיתופי או בניקוי מערכת בסיום כפוי
ה־process וה־thread handles אובייקט Child בהורה לאחר אישור סיום או דיווח rollback
ה־state של הילד ה־worker בסיום שיתופי באמצעות C++

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

בודקים את מסלול הכשל

להזרקה לפני ResumeThread אפשר להוסיף תנאי בדיקה שמחזיר מיד אחרי AssignProcessToJobObject. ה־destructor של Child מבקש סיום וממתין לאישור. הבדיקה צריכה לאמת שהילד נעלם, ולא רק שההורה הדפיס שגיאה.

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

ה־deadline של המתנה ל־termination אינו מבטיח שהמערכת תמיד תוכל לסיים כל I/O תקוע בתוך חמש שניות. אם האישור לא הגיע, מדווחים זאת במפורש. אין לכתוב שהילד הסתיים רק מפני ששלחנו בקשה.

מגבלות מכוונות והרחבה

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

הפתרון יורש את סביבת ההורה באופן מפורש באמצעות null. אפשר להחליף זאת בבלוק מסונן משיעור 4.4. רשימת handles מפורשת באמצעות STARTUPINFOEX היא חלופה טובה לשמות events; היא דורשת InitializeProcThreadAttributeList, רשימת handles מתאימה וניקוי הרשימה לאחר היצירה.

השוואת זמן קיר לזמן CPU מראה את השפעת ההמתנה. היא אינה לבדה תחקיר scheduler. תיעוד trace של creation עד exit יכול להוסיף הסבר של ready ו־wait, וההגשה צריכה להבדיל בין מדידה שבוצעה לבין שלב שלא היה זמין.

הקבלה של הפתרון נשענת על מעבר מצבים ובעלות: אין שימוש ב־context שכבר הושמד, אין handle שמועבר בלי כוונה, וסיום כפוי אינו מוצג כסיום עסקי מוצלח.