ما الذي يبقى بعد موت الأب ── تربية العمليات الابنة في كائن Job

· آخر تحديث: · · Windows, تطوير Windows, C#, C++, Win32 API, قياس وتحكّم

سجل التعديلات (النسخة الأولى، نُشرت في 29 Aug، 2026)
النشر الأول

أنهيت الواجهة من Task Manager ومع ذلك لا يمكن إعادة فتح الكاميرا. سقط تطبيق المراقبة لكن مساعد SDK ما زال ماسكاً منفذ COM. إعادة تشغيل الأب تصير تشغيلاً مزدوجاً وتحدث مشكلات في الذاكرة المشتركة والأنابيب المسمّاة. عندما تفصل SDK الجهاز إلى عملية أخرى تلتقي حوادث «بعد سقوط الأب» هذه.

نقطة الانطلاق للسبب أن في Windows لا تنتهي عمليّات الابن والحفيد تلقائياً حتّى عندما تنتهي العملية الأب. علاقة الأب والابن عند التشغيل وإدارة العمر عند الانتهاء شيئان مختلفان. الأداة التي تسدّ هذا الفراغ هي كائن Job.

في هذه المقالة نؤكّد أوّلاً لماذا لا يكفي إنهاء الأب وحده، ثم نرتّب طريقة الإدخال إلى Job وسياسة الإنهاء وطريقة المراقبة. أخيراً نصل إلى الحوادث التي تقع في الربط بالجهاز وإجراء التحقيق.

القرّاء المستهدفون مطوّرو WinForms / WPF / خدمات يفصلون SDK الجهاز إلى عملية أخرى. البيئة المفترضة Windows 10/11 (المواضع التي تستخدم الوظائف المتداخلة وPROC_THREAD_ATTRIBUTE_JOB_LIST)، وتُعرض الشيفرة بـ C++ (Win32 API) وC# (.NET 6 فما بعده). الصعوبة متوسّطة.

هذه المقالة، كامتداد لمقالات «لا يستجيب» وإيقاف التشغيل والعودة من النوم والأنابيب المسمّاة، تعالج عمر خارج العملية.

1. الخلاصة أوّلاً

نقطة انطلاق التصميم تقرير «ما لا يجوز إبقاؤه، وما تريد إبقاؤه» قبل «كيف تنهي».

كائن Job آلية تجعل شجرة العمليات وحدة واحدة. لكن إنهاء الذرّية قسراً مع اختفاء الأب، وإبقاء تفريغ تلك الذرّية أو حالتها الأخيرة، لا يتوافقان كما هما. قرّر هل تستردّ شغل الجهاز أم تبقي مادّة التشخيص أوّلاً.

  • وحدة إدارة العمر ليست نسب الأب والابن بل الـ Job. تضع على مجموعة عمليات قيوداً وإشعاراً وإنهاء جماعياً. العملية التي أُسندت لا تخرج حتّى تنتهي، ومن Windows 8 يمكن التداخل.12
  • ثبّت الانتماء إلى الـ Job قبل تشغيل الابن. إن أسندت بعد التشغيل فوّتت الحفيد الذي وُلد في الأثناء. تختلف نافذة السباق التي تُغلق بين CREATE_SUSPENDED وJOB_LIST عند التوليد (الفصل 4).
  • الاسترداد التلقائي وحفظ معلومات التشخيص تُختار كسياسة إنهاء. شرط إطلاق KillOnJobClose «إغلاق آخر مقبض Job». قوي تجاه انهيار الأب وضعيف تجاه التحليل اللاحق للذرّية التي تُنهى قسراً معه، فإن لزم الاثنان تأخذ جهة المراقبة أوّلاً ثم تنهي (الفصل 5).3

ما يريده تطبيق القياس ليس القتل نفسه. ألّا تبقى شاغلة للجهاز، وإمكان رصد الإنهاء غير الطبيعي.

ما تريد معرفته الفصل الذي تقرؤه
لماذا لا يكفي إنهاء الأب وحده الفصلان 2–3: علاقة الأب والابن ودور الـ Job
كيف تدير دون تفويت الذرّية الفصول 4–6: التوليد وسياسة الإنهاء والمراقبة
ما الذي يصير مشكلة في SDK أو خدمة الفصول 7–9: أمثلة حوادث وقيود الموارد والتداخل وbreakaway
ماذا تبحث في الميدان وكيف تختار الفصلان 10–11: إجراء التحقيق وجدول القرار

خريطة المعرفة التالية لمراجعة علاقة العناصر. إن أردت القراءة من الآلية فامضِ إلى الفصل 2.

في المخطّط، يشير الخطّ المتّصل إلى علاقة قائمة دائماً، ويشير الخطّ المتقطّع إلى علاقة مشروطة (شروط قيامها مذكورة في شرح كلّ علاقة في الصفحة التفصيليّة). القائمة الكاملة للعلاقات (المجموع 21، مع الأدلّة ودرجة اليقين) وتعريفات المفاهيم الرئيسة مجمّعة في صفحة تفاصيل خريطة المعرفة (باليابانية). البيانات: JSON-LD / Turtle

2. لماذا لا يكفي WaitForExit

«انتظار» الإنهاء وربط العمر أمران مختلفان

Process.WaitForExit() واجهة «ينتظر الأب إنهاء الابن». ما تفكّر فيه هذه المقالة بالاتجاه المعاكس، ماذا تفعل بالابن عندما يموت الأب أوّلاً. بعلاقة الأب والابن في Windows وحدها لا ينتقل إنهاء الأب إلى الابن.

Process.Kill() وCloseMainWindow() أيضاً ليسا بذاتهما ما يرعى العملية الحفيدة ومقابض الجهاز التي تمسكها. تُحرَّر مقابض العملية المنتهية نفسها، لكن المشكلة المقابض التي تمسكها الذرّية التي بقيت.

يتتبّع Kill(entireProcessTree: true) في .NET الذرّية وينهيها، لكن هناك تفويتاً للتوليد أثناء التعداد وحالة موت الأب أوّلاً، ولا يُستدعى بعد انهيار الأب نفسه. يلزم آلية لا تعتمد على شيفرة تنظيف الأب وحدها.

نرتّب لكلّ سبب رئيس لإنهاء الأب ما يبقى في الميدان.

سبب موت الأب ما يحدث للابن ما يبقى في الميدان
أغلقت الواجهة بـ × والإنهاء غير مكتمل لا يحدث شيء (يُتلف كائن Process فحسب) عملية المساعد، قفل الكاميرا
قتل الأب وحده من Task Manager يبقى الابن حيّاً منفذ COM، USB، ذاكرة مشتركة
انهيار باستثناء غير معالَج لا ضمان لتشغيل finally للأب ملفّات مؤقّتة، قفل حصري
مهلة إيقاف الخدمة SCM يرعى الأب فقط يبقى الابن في الجلسة 0
انحراف عمر الأب عن عمر شغل الجهازبإنهاء العملية الأب يختفي انتظار الأب وكائن Process، لكن عمليّات الابن والحفيد تبقى حيّة، ويبقى شغل مقابض الجهاز والأنابيب المسمّاة وملفّات القفلإنهاء العملية الأبيختفي: انتظار الأب وكائن Processيبقى: عمليّات الابن والحفيدشغل مقبض الجهازجهة الخادم للأنبوب المسمّىملفّ قفل وذاكرة مشتركة

الشكل 1: عمر الأب وعمر شغل الجهاز منحرفان. شيفرة تنظيف جهة الأب لا تعمل بقدر ما كان موت الأب غير طبيعي.

الهدف حالات «نظام التشغيل يعمل والأب وحده انتهى»

تدفّق إيقاف نظام التشغيل كلّه بالإيقاف أو النوم مجال مقالة الإيقاف ومقالة العودة من النوم. تعالج هذه المقالة حالات انتهاء الأب وحده ونظام التشغيل بخير. في ميدان القياس هذه أكثر تواتراً، والبقايا أصعب ملاحظة.

3. ما كائن Job

العمليّات الأساسية: اصنع، أسند، اضبط، افحص

كائن Job كائن نواة يدير مجموعة عمليات كوحدة واحدة. بتقسيم العمليّات الأساسية حسب الدور تصير الأربع التالية.14

الواجهة الدور
CreateJobObject اصنع Job لم تنتم إليه عملية بعد
AssignProcessToJobObject أسند عملية إلى Job
SetInformationJobObject اضبط قيوداً وغيرها
QueryInformationJobObject اقرأ معلومات محاسبة مثل زمن CPU وأخطاء الصفحة وعدد العمليات

الانتماء غير قابل للعكس، ولا تخرج العملية حتّى تنتهي. كذلك تتضمّن معلومات المحاسبة حصّة العمليات المنتهية أيضاً.

الابن الذي تصنعه عملية منتمية بـ CreateProcess ينتمي افتراضياً إلى الـ Job نفسه. أي أن الحفيد وابن الحفيد يدخلان تلقائياً في مركز قيمة الـ Job.1 لكن مسارات الخروج من الانتماء مثل breakaway والتشغيل بالوكالة عبر WMI تُؤكَّد مفصولة في الفصل 9.

البنية الأساسية لكائن Jobينتمي الابن والحفيد إلى كائن Job صنعه الأب، ويفرض الـ Job القيود ويُشعر منفذ الإكمال وينهي جماعياً بوحدة شجرة العملياتالعملية الأبكائن Jobالابن (مضيف SDK الجهاز)الحفيد (مساعد الشركة المصنّعة)قيود (ذاكرة وCPU)إشعار (منفذ إكمال)إنهاء جماعي

الشكل 2: الـ Job وعاء يقدّم «قيوداً» و«إشعاراً» و«إنهاء جماعياً» بوحدة شجرة العمليات.

فرق جيل نظام التشغيل والفرق عن صندوق الرمل

قبل Windows 7 عملية واحدة لكلّ وظيفة، ومن Windows 8 صار التداخل (انتماء متعدّد) ممكناً.5 المتن يفترض Windows 10/11، ونقاط الانتباه قبل Windows 7 في الفصل 9 والأسئلة الشائعة.

إدخال Job وحده لا يصير حاوية أو صندوق رمل. لا يمكن تقييد الوصول إلى الشبكة، ورمز الوصول (الصلاحيات) آلية أخرى. ولا يمكن صنع حدود أمنية بقيود الواجهة وحدها. الدور في هذه المقالة جعل عمر شجرة العمليات والموارد وحدة واحدة فحسب.

4. طريقة الإدخال الصحيحة ── سباق التوليد والانتماء

إن أسندت بعد التشغيل وُلد حفيد في الأثناء

قد تلد العملية الابن حفيداً في أوّل ميلّي ثوانٍ بعد أن تبدأ الجري. تشغيل مساعد SDK نموذجي. إن أخذت PID بعد Process.Start() ثم أسندت، يخرج الحفيد الذي وُلد قبل Assign خارج الـ Job.

الإدخال بعد بدء الجري يفوّت الحفيديجري الابن من لحظة Process.Start، والحفيد الذي ولده الابن أثناء نافذة السباق حتّى استدعاء AssignProcessToJobObject يخرج خارج الـ Jobيبدأ الابن الجري بـ Process.Startنافذة السباق حتّى Assignالحفيد الذي وُلد في هذه الأثناءيواصل الجري خارج الـ JobAssignProcessToJobObjectيدخل فقط الحفيد الذي يُولد بعده

الشكل 3: نافذة السباق ميلّي ثوانٍ، وتشغيل مساعد SDK يحدث هناك تماماً.

الإجراء A: ولّد متوقّفاً، أسند، ثم شغّل

الطريقة الكلاسيكية الواسعة التوافق إجراء يستخدم CREATE_SUSPENDED. تغلق نافذة سبق الابن في الحركة وولادة حفيد بهذا الترتيب.67

  1. اصنع Job بـ CreateJobObject
  2. اضبط القيود أوّلاً بـ SetInformationJobObject
  3. نفّذ CreateProcess بعلم CREATE_SUSPENDED (الخيط الأوّلي لا يجري)
  4. أدخل بـ AssignProcessToJobObject
  5. إن فشل فلا تستأنف، وTerminateProcess في المكان (لا تشغّل أمراً واحداً خارج الـ Job)
  6. شغّل بـ ResumeThread

ما يغلقه الإجراء A نافذة السباق مع توليد الحفيد. تبقى نافذة تجاه انهيار الأب نفسه. إن انهار الأب بين الخطوتين 3 و4 بقي ابن معلَّق لم يدخل Job بعد. لا يجري، لكنّه لا يختفي تلقائياً.

إن أردت إغلاق هذه النافذة بما فيها مقاومة انهيار الأب فاستخدم الإجراء B التالي.

إجراء التشغيل بـ SUSPENDED ثم الإدخال إلى Jobاصنع Job واضبط القيود، وشغّل الابن بـ CREATE_SUSPENDED وأسند بـ AssignProcessToJobObject، وإن فشل فأوقف بـ TerminateProcess دون Resume، وإن نجح فشغّل بـ ResumeThreadنجاحفشلاصنع Job بـ CreateJobObjectقيود بـ SetInformationJobObjectشغّل الابن بـ CREATE_SUSPENDEDAssignProcessToJobObjectشغّل بـ ResumeThreadأنه فوراً دون Resume

الشكل 4: هيكل الإجراء A. تنفيذ يغفل فرع «إن فشل فلا تشغّل» يلد عملية ضالّة عند الحادث فقط.

// C++: النواة الدنيا للإجراء A (معالجة الأخطاء الهيكل فقط)
HANDLE job = CreateJobObjectW(nullptr, nullptr);   // بلا اسم يكفي. لا تورّث
if (!job) return HRESULT_FROM_WIN32(GetLastError());

JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits = {};
limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
if (!SetInformationJobObject(job, JobObjectExtendedLimitInformation,
                             &limits, sizeof(limits))) {
    DWORD err = GetLastError();         // احفظ قبل أن يكتب CloseHandle فوقه
    CloseHandle(job);                   // لا تشغّل ابناً بـ Job بلا قيود
    return HRESULT_FROM_WIN32(err);
}

STARTUPINFOW si = { sizeof(si) };
PROCESS_INFORMATION pi = {};
if (!CreateProcessW(exePath, cmdline, nullptr, nullptr, FALSE,
                    CREATE_SUSPENDED, nullptr, nullptr, &si, &pi)) {
    DWORD err = GetLastError();
    CloseHandle(job);                   // لا تسرّب مقبض Job عند إعادة محاولة التشغيل
    return HRESULT_FROM_WIN32(err);
}

if (!AssignProcessToJobObject(job, pi.hProcess)) {
    DWORD err = GetLastError();         // احفظ قبل أن يكتب Terminate فوقه
    TerminateProcess(pi.hProcess, 1);   // لا تشغّل خارج الـ Job
    // أغلق المقابض وارفع err كخطأ
} else if (ResumeThread(pi.hThread) == (DWORD)-1) {
    DWORD err = GetLastError();
    TerminateProcess(pi.hProcess, 1);   // لا تترك معلّقاً
    // أغلق المقابض وارفع err كخطأ
}
CloseHandle(pi.hThread);
// عند النجاح تنتقل ملكية job و pi.hProcess إلى كائن إدارة عمر المستدعي
// (يعادل غلاف C# في الفصل 4). إغلاق job = إطلاق KillOnJobClose،
// و pi.hProcess يُستخدم في الفصل 6 لتثبيت «من مات»

الإجراء B: من Windows 10 فما بعده ولّد بحالة داخل Job

ضع مقبض Job بـ PROC_THREAD_ATTRIBUTE_JOB_LIST في قائمة سمات STARTUPINFOEX. ثم مرّره إلى CreateProcess بعلم EXTENDED_STARTUPINFO_PRESENT. دون العلم لا يُفسَّر كبنية موسَّعة وتُتجاهل السمات.89

في هذه الطريقة تنتمي العملية إلى الـ Job قبل جري الخيط الأوّلي. تختفي نافذة السباق «وُلدت لكن لم تنتم بعد» نفسها، فلا تلزم SUSPENDED ولا فرع معالجة فشل Assign بعد التوليد.

الطريقة توقيت تثبيت الانتماء نقاط انتباه باقية
الإجراء A: SUSPENDED ← Assign ← Resume بعد توليد الابن، قبل تحريك الخيط الأوّلي إن انهار الأب قبل Assign بقي ابن متوقّف
الإجراء B: التوليد بـ JOB_LIST عند توليد العملية يلزم Windows 10 فما بعده. حدّد قائمة السمات وعلم التشغيل الموسَّع
فرق نافذة السباق بين الإجراءين A وBيولّد الإجراء A متوقّفاً ثم يقوم بـ Assign وResume فيلزم فرع إنهاء عند الفشل، بينما يولّد الإجراء B بوضع Job في قائمة السمات فينتمي من لحظة الولادة ولا توجد نافذة سباق ولا فرع فشلالإجراء A: ولّد متوقّفاًانتماء بـ Assignابدأ بـ Resumeفرع الفشل إلزاميالإجراء B: ولّد بقائمة سماتمنتمٍ من لحظة الولادةلا نافذة سباق ولا فرع فشل

الشكل 5: الإجراء A «أدخل ثم شغّل»، والإجراء B «وُلد بحالة داخل». إن أمكن افتراض Windows 10 فما بعده فسبب الاختيار وجود نافذة السباق وفرع الفشل نفسه.

ثلاثة تنفيذات تُتجنَّب في أيّ إجراء

  • الإدخال بأخذ PID بعد Process.Start() (يخرج الحفيد أوّلاً)
  • توريث مقبض Job إلى الابن (حتّى بعد موت الأب يواصل الابن إمساك المقبض فلا يُطلَق KillOnJobClose ── الفصل 5)
  • ابتلاع فشل Assign ومواصلة التشغيل (عملية الجهاز خارج الـ Job تصير بطل الحادث التالي)

في .NET أظهر مالك مقبض Job في الشيفرة

ليس لـ System.Diagnostics.Process مفهوم Job، ولا غلاف رسمي. اكتب غلافاً رقيقاً بـ P/Invoke أو CsWin32.

النقطة لفّ مقبض Job بـ SafeHandle وجعله IDisposable. إغلاق آخر مقبض Job بـ Dispose() يطلق KillOnJobClose. يمكن إظهار قصد التصميم «عمر الغلاف عمر شجرة الابن» كملكية.

فيما يلي هيكل يُظهر ملكية المقبض. معالجة التوليد في جهة P/Invoke للإجراء A/B، وفي مشروع حقيقي تلزم أيضاً إعدادات CsWin32 وتحديد unsafe.

// C#: غلاف رقيق يتولّى ملكية مقبض Job فقط (التوليد عبر P/Invoke للإجراء A/B)
sealed class ChildProcessJob : IDisposable
{
    private readonly SafeFileHandle _job;    // استمرّ في الحفظ في الحقل

    public ChildProcessJob()
    {
        _job = PInvoke.CreateJobObject(default, null);
        if (_job.IsInvalid) throw new Win32Exception();
        var limits = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION();
        limits.BasicLimitInformation.LimitFlags =
            JOB_OBJECT_LIMIT.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
        if (!PInvoke.SetInformationJobObject(_job,
            JOBOBJECTINFOCLASS.JobObjectExtendedLimitInformation,
            &limits, (uint)sizeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION)))
        {
            int err = Marshal.GetLastWin32Error();  // احفظ قبل Dispose
            _job.Dispose();              // لا تمرّر Job بلا KillOnJobClose
            throw new Win32Exception(err);
        }
    }

    public void Dispose() => _job.Dispose();  // هنا تنتهي الشجرة تحته
}

وبالعكس، إغلاق المقبض دون قصد الإغلاق ينهي شجرة الابن. احتفظ بالمرجع إلى الغلاف طوال عمر الأب. إن لم تحتفظ، في لحظة استرداد GC لـ SafeHandle تُباد شجرة الابن بلا سبب.

5. KillOnJobClose ── «ربِّ» و«مُت معاً»

شرط الإطلاق ليس «موت الأب» بل «إغلاق آخر مقبض»

JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE علم قيد ينهي كلّ العمليات تحته عندما يُغلق آخر مقبض Job.3

سواء انتهى الأب باستثناء أو قُتل من Task Manager أو أُوقفت الخدمة قسراً، تغلق النواة كلّ مقابض تلك العملية. إن كان ذلك آخر مقبض Job انتهت الابن والحفيد أيضاً. قوّة هذه الآلية أنّها لا تفترض تشغيل إنهاء الأب.10

لكن توريث مقبض Job إلى الابن يُبقي المقبض بعد إنهاء الأب. عندئذ لا يصير «آخر مقبض» ولا تنتهي شجرة الابن.

خطّ زمن KillOnJobCloseسواء انتهى الأب طبيعياً أو انهار أو أُنهي قسراً، تغلق النواة كلّ مقابض الأب، وإن كان ذلك آخر مقبض Job أُغلق الـ Job وانتهت شجرة العمليات تحته جماعياًنعملا (مع وراثة)اختفاء الأب (بما فيه الانهيار)تغلق النواة كلّ المقابضآخر مقبض Job؟إنهاء الشجرة تحته جماعياًيبقى الابن حيّاً

الشكل 6: شرط الإطلاق ليس «موت الأب» بل «إغلاق آخر مقبض». لذا لا تورّث المقبض إلى الابن.

اختر سياسة الاسترداد الفوري أو إبقاء مادّة التشخيص

ما يُفقَد بالإنهاء القسري ليس شغل الجهاز فقط. تُفقَد أيضاً فرصة أخذ تفريغ انهيار الابن والحفيد اللذين يُنهيان معه، والإطار الصالح الأخير، وفرصة تفريغ ملفّ قياس قيد الكتابة.

تفريغ انهيار الأب نفسه مختلف. يعالج WER الاستثناء غير المعالَج أثناء حياة الأب، لذا يمكن كتابة تفريغ الأب قبل إغلاق المقبض. ما نناقشه هنا مادّة التحليل اللاحق للابن والحفيد في جهة الإنهاء القسري.

السياسة الميدان المناسب ما يُفقَد
مع KillOnJobClose ميدان الفتح المزدوج للجهاز أسوأ شيء مادّة التحليل اللاحق، العيّنة الأخيرة
بلا (مراقبة فقط) ميدان التفريغ والسجلّ أصل إن تُركت عمليات يتيمة وشغل منفذ
بلا + عملية مراقبة تستدعي TerminateJobObject عندما توجد خدمة تحكّم على حدة يصير التنفيذ مزدوجاً

كمادّة حكم عدّد أوّلاً «ما لا يجوز إبقاؤه» و«ما تريد إبقاؤه».

ما لا يجوز إبقاؤه: فتح الكاميرا/الرقمنة، حصرية التسلسلي وUSB، جهة خادم الأنبوب المسمّى، جلسة دونغل الترخيص، الذاكرة المشتركة وملفّ القفل.

ما تريد إبقاؤه: تفريغ الانهيار، الإطار/العدّاد الصالح الأخير، فرصة إرسال أمر إعادة الجهاز إلى الجانب الآمن (أرسل قبل القتل إن أمكن).

أقتل أم أراقب فقطإن كان الفتح المزدوج للجهاز أسوأ شيء فأضف KillOnJobClose، وإن كان تفريغ الانهيار والإطار الأخير أصلاً فلا تضف وراقب، وإن وُجدت خدمة تحكّم أخرى فاستدعِ TerminateJobObject من هناكتحرير الجهاز أولوية قصوىالتفريغ والحالة الأخيرة أصلخدمة التحكّم موجودة على حدةماذا تحمي في لحظة السقوط؟مع KillOnJobCloseمراقبة فقط (لا تقتل)جهة المراقبة تستدعي TerminateJobObject

الشكل 7: لا تختر «أقتل أم لا» بل «ماذا تحمي في لحظة السقوط». إن أردت الاثنين يصير تكوين الصفّ الثالث: جهة المراقبة تأخذ التفريغ أوّلاً ثم تطوي.

مرّر مقبض Job إلى جهة المراقبة بينما الأب حيّ

لتكوين الصفّ الثالث في الجدول، إنهاء عملية المراقبة بـ TerminateJobObject، يلزم إعداد. يجب أن تحصل جهة المراقبة على مقبض Job قبل موت الأب.

الـ Job المصنوع بإجراء هذه المقالة بلا اسم، لذا لا سبيل لتتبّعه من الخارج بعد اختفاء الأب. طريقتا التمرير التاليتان.

الطريقة ما تفعله بينما الأب حيّ
انسخ مقبض Job بلا اسم مرّر المقبض إلى عملية المراقبة بـ DuplicateHandle
استخدم Job مسمّى اصنعه مسمّى من البداية، ولتفتح جهة المراقبة بـ OpenJobObject وتحتفظ

قد يتصادم الاسم عالمياً لذا ضمّن GUID فريداً ونحوه. إن نسيت هذا الإعداد وجدت جهة المراقبة شذوذ الأب دون وسيلة لإنهاء الشجرة.

أخذ التفريغ وتأمين الجهاز قبل الإنهاء القسري

لابن يُنهى بـ KillOnJobClose، كما مع TerminateProcess، لا مقدّمة. لا يحدث استثناء غير معالَج، لذا حتّى مع ضبط WER (LocalDumps) للابن لا يبقى تفريغ ذلك الإنهاء القسري. ما يلتقطه WER حالة إنهاء الابن بانهياره نفسه.

إن كان المتطلّب «استرداداً تلقائياً وتفريغاً أيضاً» فانقل فاعل الإنهاء إلى جهة المراقبة. الترتيب أخذ التفريغ بينما الهدف حيّ، وإعادة الجهاز إلى الجانب الآمن إن لزم، وأخيراً الإنهاء بـ TerminateJobObject.

طيّ يوفّق الاسترداد التلقائي والتفريغعندما ترصد عملية المراقبة شذوذاً تأخذ التفريغ أوّلاً، وترسل إن لزم أمراً يعيد الجهاز إلى الجانب الآمن، وأخيراً تطوي الشجرة بـ TerminateJobObject، فتوفّق الاسترداد التلقائي والتحليل اللاحقترصد جهة المراقبة شذوذاًخذ التفريغ أوّلاًأَعِد الجهاز إلى الجانب الآمناطوِ بـ TerminateJobObject

الشكل 8: الحلّ الوحيد لـ «استرداد تلقائي وتفريغ أيضاً». إن عُكس الترتيب لم يعد هدف الأخذ موجوداً.

في تكوين ينهي الابن قسراً مع اختفاء الأب بـ KillOnJobClose لا فرصة للابن لإرسال «أمر إعادة الجهاز إلى الجانب الآمن» أيضاً. لجهاز يحتاج ذلك اختر تكوين الصفّ الثالث في الجدول، ولترسل جهة المراقبة أمر التأمين أوّلاً ثم تستدعي TerminateJobObject.11

6. انتظر «صار فارغاً» بمنفذ إكمال

انتظار مقبض Job وحده لا يؤكّد إنهاء الشجرة

مقبض Job لا يصير حالة إشارة حتّى عندما تنتهي كلّ العمليات تحته. ما يُشار إليه فقط عندما تُنهى كلّ العمليات بتجاوز حدّ زمن الوظيفة.12

لـ «امضِ إلى التالي عندما تفرغ شجرة الابن» اربط منفذ إكمال إدخال/إخراج (IOCP) بالـ Job.1314 أنجز الربط بينما الـ Job فارغ، قبل إدخال عملية. الربط في الأثناء قد يفوّت إشعار عملية تغيّرت حالتها أثناء ذلك.15

أربع رسائل ترصد الحدوث والإنهاء والشذوذ والصفر

الرسالة ما تُعلمه
JOB_OBJECT_MSG_NEW_PROCESS انضمام عملية إلى الـ Job. ترصد أيضاً حدوث حفيد
JOB_OBJECT_MSG_EXIT_PROCESS انتهاء عملية
JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS الانتهاء برمز إنهاء غير طبيعي مثل انتهاك وصول. مهمّ خصوصاً في تطبيق قياس13
JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO صيرورة عدد العمليات النشطة 0

ما يدخل حزمة NEW_PROCESS PID الجديد فقط. لا تُعلم علاقة الأب والابن من ولد من. إن لزم النسب فأضف وسيلة أخرى مثل ETW.

تدفّق إشعار منفذ الإكماليلقي الـ Job رسائل حدوث العملية وإنهائها والإنهاء غير الطبيعي والصفر إلى منفذ الإكمال، ويتلقّاها خيط مراقبة مخصَّص بـ GetQueuedCompletionStatus، ويمرّر النتيجة فقط إلى خيط الواجهةخيط الواجهةخيط المراقبةمنفذ الإكمالكائن Jobخيط الواجهةخيط المراقبةمنفذ الإكمالكائن JobNEW / EXIT_PROCESSABNORMAL_EXIT / ZEROانتظر حزمة الإكمالالرسالة وPIDمرّر إشعار النتيجة فقط

الشكل 9: أدر GetQueuedCompletionStatus في خيط مخصَّص. الانتظار في خيط الواجهة يجعل الواجهة «لا تستجيب» عند كلّ شذوذ ابن.

انتظر بخيط مخصَّص ومنفذ مخصَّص، وعند المهلة استعلم المحاسبة

أدر حلقة المراقبة هذه على منفذ إكمال صُنع خصّيصاً لإشعار الـ Job. الركوب على المنفذ نفسه لإدخال/إخراج قائم يعني أن الحزمة أُخرجت عندما عاد GetQueuedCompletionStatus. إن رميتها بـ continue لأن المفتاح مختلف انتظر مالك ذلك الإدخال/الإخراج الإكمال إلى الأبد. فرع حزمة الفشل كذلك.

عند المشاركة يلزم آلية توصيل إلى المالك لكلّ مفتاح على حدة. في هذه المقالة نفصل المنفذ ونمرّر النتيجة فقط إلى خيط الواجهة.

كذلك الفرض Job واحد كجيل تشغيل واحد، يُعاد صنعه عند كلّ تشغيل. إعادة الاستخدام عند إعادة المحاولة تتضمّن الجيل السابق في TotalProcesses فلا يعمل حارس تمييز الـ Job الفارغ قبل التشغيل.

// C++: هيكل خيط المراقبة (تأمين بمهلة + محاسبة استعداداً لتفويت الإشعار)
DWORD msg; ULONG_PTR key; LPOVERLAPPED info;
bool treeEmpty = false;
while (!treeEmpty) {
    if (!GetQueuedCompletionStatus(iocp, &msg, &key, &info, 5000)) {
        if (info != nullptr) continue;                // حزمة إكمال إدخال/إخراج فاشل. واصل المراقبة
        if (GetLastError() != WAIT_TIMEOUT) break;    // إتلاف المنفذ وغيره إنهاء
        JOBOBJECT_BASIC_ACCOUNTING_INFORMATION acct = {};
        if (QueryInformationJobObject(job, JobObjectBasicAccountingInformation,
                                      &acct, sizeof(acct), nullptr))
            treeEmpty = (acct.TotalProcesses > 0 &&   // لا تخطئ اعتبار Job فارغ قبل التشغيل اكتمالاً
                         acct.ActiveProcesses == 0);  // تأمين عندما يسقط إشعار ZERO
            // الفرض: أعد صنع الـ Job عند كلّ تشغيل (1 Job = جيل تشغيل 1). إعادة استخدام
            // الـ Job عند إعادة المحاولة تُبقي TotalProcesses عادّاً الجيل السابق،
            // فلا يميّز هذا الحارس الأجيال
        continue;                        // عند فشل الاستعلام لا تجزم بالفراغ
    }
    if ((HANDLE)key != job) continue;    // طابق CompletionKey عند الربط.
                                         // الفرض أن هذا المنفذ صُنع خصّيصاً لمراقبة Job
                                         // (انظر المتن أدناه. بهذه الرمية على منفذ مشترك
                                         //  ينتظر مالك إدخال/إخراج آخر إلى الأبد)
    DWORD pid = (DWORD)(UINT_PTR)info;   // يدخل PID حسب الرسالة
    switch (msg) {
    case JOB_OBJECT_MSG_NEW_PROCESS:          /* سجّل حدوث حفيد */ break;
    case JOB_OBJECT_MSG_EXIT_PROCESS:         /* سجّل الإنهاء */ break;
    case JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS:/* إنهاء غير طبيعي: إلى تأكيد التفريغ */ break;
    case JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO:  treeEmpty = true; break;
    }                                    // break في switch وحده لا ينهي الانتظار
}

الإشعار والمحاسبة والمقبض معلومات يمكن تثبيتها مختلفة

الإشعار من حيث المبدأ بلا ضمان وصول. ما يُضمَن فقط إشعار الحدّ المضبوط بـ JobObjectNotificationLimitInformation. لا يمكن الحكم «لا إشعار = لم يقع».15

حالة تجميع مثل «هل فرغ» تُؤكَّد بجمع استطلاع معلومات المحاسبة. لكن المحاسبة عدّاد تجميع، لذا لا يمكن استعادة PID ورمز الإنهاء وما إذا كان غير طبيعي لـ EXIT/ABNORMAL_EXIT الساقط. ذلك مجال حفظ مقبض العملية وETW.

ACTIVE_PROCESS_ZERO أيضاً ليس دليل إنهاء طبيعي. قد صار صفراً بإنهاء قسري، والرسالة نفسها لا تميّز. جودة الإنهاء تُحكَم بـ EXIT/ABNORMAL_EXIT ورمز الإنهاء.

لا تجزم أنّه العملية نفسها بـ PID وحده

يُعاد استخدام PID حزمة الإكمال. ما لم تحفظ مقبض العملية لا ضمان أن ذلك PID ما زال يشير إلى العملية نفسها.13

ما يمكن لـ pi.hProcess المحفوظ في الفصل 4 تثبيته PID الابن المشغَّل مباشرة فقط. للحفيد الذي ولده SDK افتح مقبضاً بـ OpenProcess عند تلقّي NEW_PROCESS، وطابق بعد ذلك على أساس ذلك المقبض.

ومع ذلك تبقى نافذة قصيرة من الإشعار إلى Open. إن أُعيد استخدام PID في تلك الأثناء أمسكت عملية أخرى. عندما يلزم تحديد الفرد بصرامة أكّد أيضاً بقياس عن بُعد يملك زمن التوليد مثل حدث بدء عملية ETW.

مراقبة لا تعتمد على الإشعار وحدهرسائل منفذ الإكمال لغرض الإشعار بلا ضمان وصول، لذا اجمع استطلاع معلومات المحاسبة وحفظ مقبض العملية استعداداً للتفويت وإعادة استخدام PIDإشعار منفذ الإكمالبلا ضمان وصول (لغرض الإشعار)جمع استطلاع معلومات المحاسبةيُعاد استخدام PIDاحفظ مقبض العملية

الشكل 10: الإشعار المسار الرئيس، والمحاسبة والمقبض تأمين. لا يقال «نرصد» إلّا باكتمال الاثنين.

يُذكر أن تنفيذاً مثل «اقتل بـ TerminateThread الخيط الذي ينتظر فراغ الـ Job» شاع سابقاً، لكن مع منفذ إكمال لا يلزم الإنهاء القسري لخيط الانتظار. نشر Raymond Chen أيضاً مقالة تعيد كتابة هذا النمط القديم إلى طريقة منفذ الإكمال.16

7. حوادث تقع فعلاً في القياس والربط بالجهاز

نطبّق الآلية حتّى الآن على ستّ حوادث تقع في ميدان الجهاز. في كلّ مثال نؤكّد السبب والتدبير إضافة إلى «ما بقي».

الحادث 1: مات الأب وحده وبقيت الكاميرا مفتوحة

في تكوين يملك SDK الشركة المصنّعة عملية مساعد لنقل الإطارات، إسقاط واجهة الأب من Task Manager يُبقي المساعد وحده. الأب المعاد تشغيله device busy عند تهيئة SDK. في بعض الميادين لا يتعافى حتّى إعادة تشغيل طاقة الجهاز.

ما بقي: عملية المساعد وفتح الكاميرا الحصري.

مات الأب وحده وبقيت الكاميرا مفتوحةبإنهاء الواجهة قسراً يختفي الأب لكن عملية مساعد SDK تبقى ماسكة مقبض الكاميرا، فيفشل الأب المعاد تشغيله في إعادة الفتح بـ device busyأنه الواجهة من Task Managerيختفي الأبيبقى مساعد SDKماسك الكاميراdevice busy بعد إعادة التشغيل

الشكل 11: حقيقة «اختفت العملية لكن لا يُفتح الجهاز». الجاني كثيراً ليس العملية ذات الاسم المعروض في Task Manager.

الحادث 2: يخرج الحفيد خارج الـ Job

المسار اثنان ويلزم فصل التدبير.

(أ) عندما يستخدم SDK Jobه. حالة دخول الطرف الآخر Jobاً آخر أصلاً. في Windows 7 عملية واحدة لكلّ وظيفة فيفشل Assign هنا. من Windows 8 يمكن الالتقاط بالتداخل، لكن إن وُجدت قيود واجهة على Jobك لا يمكن التداخل نفسه (الفصل 9).

(ب) عندما يلد SDK حفيداً بـ CREATE_BREAKAWAY_FROM_JOB. لا يقوم إلّا عندما يسمح Jobك بـ BREAKAWAY_OK، ويُولد الحفيد خارج الشجرة من البداية. لا يُلتقَط بالتداخل أيضاً. إن لم تسمح فشل توليد جهة SDK، لذا إن قدّمت المراقبة فالأساس المنع والرصد كفشل.

ما بقي: حفيد يجري خارج المراقبة، وAssign ابتُلع فشله.

مساران لخروج الحفيد خارج الـ Jobمسار استخدام SDK Jobه يبقى مجال التقاط بالتداخل من Windows 8 ويفشل إن وُجدت قيود واجهة، ومسار توليد SDK حفيداً بـ breakaway لا يقوم إلّا عندما تسمح أنت ويصير الحفيد خارج الشجرة من البدايةيخرج الحفيد خارج الـ Job(أ) SDK يستخدم Jobه(ب) توليد بـ breakawayمن Win8 التقاط بالتداخليفشل إن وُجدت قيود واجهةلا يقوم إلّا عند السماحالحفيد خارج الشجرة من البداية

الشكل 12: حتّى مع «الخروج» نفسه، (أ) يبقى مجال التقاط بالتداخل، و(ب) يتحدّد عدم إمكان الالتقاط لحظة السماح. يبدأ التدبير بتحديد المسار.

الحادث 3: عملية جهاز شُغِّلت من خدمة

ما ينتظره SCM عند إيقاف الخدمة الأب فقط، والابن والحفيد المولودان في الجلسة 0 خارج ولاية معالجة الإيقاف. Job+KillOnJobClose يرعى حتّى خارج هذه الولاية. تصميم التكوين الذي يعبر الخدمة والجلسة التفاعلية نفسَه يُوكَل إلى مقالة حدود المستخدم.

ما بقي: عملية متبقية في الجلسة 0، وانفجار خاطئ لحكم التشغيل المزدوج عند التشغيل التالي.

الحادث 4: ابن يسقط بعد شهر

دون تسجيل محاسبة الـ Job دورياً (PeakJobMemoryUsed وعدّادات الإدخال/الإخراج وإجمالي العمليات) لا يمكن تتبّع «مساعد أيّ جيل بدأ الانتفاخ متى» لاحقاً.17 بنية السقوط بعد شهر بتسرّب مقابض كما شُرِّحت في مقالة العطل طويل الأمد لكاميرا صناعية، ومحاسبة الـ Job مدخل ذلك التحقيق.

ما بقي: سجلّ لا يكفي لتثبيت السبب.

الحادث 5: إنهاء الأب ينتظر إنهاء الابن

انتظار WaitForExit أو إنهاء الشجرة في خيط الواجهة يجعل الأب «لا يستجيب» يوم يتجمّد الابن. أوكِل انتظار الإنهاء إلى خيط IOCP، وضع في الواجهة التقدّم وزر القطع فقط. الآلية كما في مقالة «لا يستجيب».

ما بقي: أب علّق معه.

لا تنتظر الإنهاء في خيط واجهة الأبانتظار إنهاء الابن في خيط الواجهة ينشر تعليق الابن إلى عدم استجابة الأب، لذا أوكِل انتظار الإنهاء إلى خيط مراقبة IOCP وضع في خيط الواجهة عرض التقدّم وزر القطع فقطانتظر إنهاء الابن في خيط الواجهةيوم علّق الابنالأب أيضاً لا يستجيب (معه)انتظر في خيط IOCPالواجهة تقدّم وقطع فقط

الشكل 13: جهة رصد شذوذ الابن لا يجوز أن تتجمّد بشذوذ الابن. فصل مكان الانتظار وحده يُزيل الذهاب معاً.

الحادث 6: يفشل تحت المنقّح فقط

قد تكون أداة تطوير أو مشغّل قد أدخلت عمليتك الأب إلى Job ما أصلاً. من Windows 8 يُنقَذ غالباً بالتداخل، لكن على جهاز قياس قبل 7 يصير Assign ERROR_ACCESS_DENIED، فيولد فرق «لا يعمل على جهاز التطوير فقط» أو «لا يعمل في الإنتاج فقط». القاعدة تأكيد انتمائك أوّلاً بـ IsProcessInJob.18

ما بقي: زمن تحقّق لا يمكن فيه تحديد سبب فرق البيئة.

8. أيّ قيود تضع

للقيود غرض وأثر جانبي

نضيّق على قيود ذات معنى في تطبيق قياس ونرتّب سبب الاستخدام والأثر عند الإفراط.319

القيد سبب الاستخدام عند الإفراط
KILL_ON_JOB_CLOSE حرّر الجهاز باختفاء الأب تختفي مادّة التحليل اللاحق
ACTIVE_PROCESS أوقف تكاثر ابن جامح لـ SDK يُرفض توليد المساعد الطبيعي أيضاً
JOB_MEMORY / PROCESS_MEMORY حدّ تسرّب التشغيل الطويل يبدأ فشل تخصيص مخزن صورة ضخم
DIE_ON_UNHANDLED_EXCEPTION لا تُظهر حوار خطأ على جهاز بلا مراقبة يصعب التنقيح التفاعلي
التحكّم في معدّل CPU لا تجوّع الواجهة ابن معالجة الصورة لا يلحق بأجل الإطار
BREAKAWAY_OK اترك طريق خروج لـ SDK يحتاج وظيفة أخرى يختفي من هدف المراقبة
قيود الواجهة تضييق شبيه بصندوق الرمل ينكسر التداخل (الفصل 9)

حدّ الإشعار للرصد، والحدّ القسري للرفض والإنهاء

افصل «حدّاً فضفاضاً للإشعار» عن «حدّاً يوقف عند التجاوز». JobObjectNotificationLimitInformation يُشعر بالتجاوز فقط، وتواصل العملية الجري.15

حدّ Extended Limit يُفرَض، لكن شكل الفرض يختلف لكلّ حدّ.3

الحدّ ما يحدث عند التجاوز
حدّ الذاكرة تفشل عملية الالتزام التي تتجاوز. العملية نفسها حيّة
ACTIVE_PROCESS يفشل التوليد أو الانتماء الذي يتجاوز. العملية التي تجاوزت بالانتماء تُنهى
زمن العملية (PROCESS_TIME) تُنهى تلك العملية المتجاوزة فقط
زمن الوظيفة (JOB_TIME) حدّ للقيمة المجمَّعة، وافتراضياً تُنهى كلّ العمليات تحت الـ Job

ضبط دون معرفة هذا الفرق يُشخَّص «مساعد يختفي فور التوليد» كعطل آخر. في التشغيل الطويل الترتيب الآمن الرصد أوّلاً بحدّ إشعار، ثم تقرير الحدّ القسري بعد اتّضاح الميل.

حدّ للإشعار وحدّ للفرضحدّ JobObjectNotificationLimitInformation يُشعر بالتجاوز فقط وتواصل العملية الجري، وحدّ Extended Limit فرض، وحدّ الذاكرة فشل العملية، وACTIVE_PROCESS فشل التوليد أو الانتماء، وحدّ الزمن إنهاء العمليةأريد الرصدأريد الإيقافغرض الحدّ؟حدّ إشعار: يجري حتّى عند التجاوزحدّ قسري: رفض أو إنهاءحدّد الجيل بسجلّ المحاسبةفشل التزام ورفض توليد وإنهاء

الشكل 14: حتّى مع «حدّ» نفسه الإشعار والفرض شيئان مختلفان، وفعل الفرض يختلف لكلّ حدّ. فرض حدّ قسري فجأة بلا رصد ينفجر خطأ على جبل التشغيل الطبيعي.

علاقة التحكّم في معدّل CPU والمعالجة الدورية تُوكَل إلى مقالة الوقت الحقيقي الليّن، ونكتفي هنا بتدبير «عملية الجهاز تأكل الواجهة» تقريباً.

9. التداخل وBreakaway وطرف دخل Job أصلاً

فكّر في التداخل كـ «تضمين مجموعة عمليات»

يمكن ترتيب قواعد التداخل من Windows 8 في أربع.5

  • الوظيفة الأب مجموعة أوسع، والوظيفة الابن مجموعة جزئية منها (يفشل Assign بترتيب لا يقوم فيه هذا التضمين)
  • القيود الرئيسة للموارد يصير الأشدّ على السلسلة هو الفعّال
  • وظيفة وُضعت عليها قيود واجهة لا يمكن تداخلها
  • يصل الإشعار أيضاً إلى منافذ إكمال كلّ الوظائف الأب على السلسلة (يجوز ألّا يكون لجهة الوظيفة الابن منفذ)
هرمية الوظائف المتداخلة والقيود الفعّالةالوظيفة الأب مجموعة أوسع والوظيفة الابن مجموعة جزئية منها، وتصير القيمة الأشدّ على السلسلة هي القيود الفعّالة للموارد الرئيسة. وظيفة وُضعت عليها قيود واجهة لا يمكن تداخلهاالوظيفة الأب (مجموعة أوسع)الوظيفة الابن (مجموعة جزئية)العملية المنتميةالقيود الفعّالة = القيمة الأشدّوظيفة بقيود واجهةلا يمكن التداخل

الشكل 15: فكّر في التداخل كـ «تضمين مجموعة». قيود الواجهة تكسر التداخل لذا من الحصافة ألّا تضعها على Job إدارة العمر.

Breakaway مسار توليد خارج الـ Job من البداية

breakaway مسار نظامي يخرج فيه الذرّية المولودة بـ CreateProcess من الشجرة.3

إعداد جهة الـ Job شرط ولادة الابن خارج الـ Job
JOB_OBJECT_LIMIT_BREAKAWAY_OK ولّد بتحديد CREATE_BREAKAWAY_FROM_JOB
SILENT_BREAKAWAY_OK لا يلزم تحديد علم. يُولد كلّ ابن خارجاً

قد يلزم لـ SDK يستخدم Jobه. لكن العملية المولودة بذلك المسار تخرج من الإنهاء الجماعي ومن المراقبة أيضاً. إن سمحت فقرّر أيضاً من يدير الوجهة التي خرجت إليها.

مسار الخروج من الشجرة بـ Breakawayعندما يكون BREAKAWAY_OK على الـ Job يُولد الحفيد المصنوع بـ CREATE_BREAKAWAY_FROM_JOB خارج الـ Job ويختفي من هدف الإنهاء الجماعي والمراقبةتوليد معتادتوليد بتحديد BREAKAWAYJob (مع BREAKAWAY_OK)العملية الابنالحفيد أيضاً داخل الـ Jobالحفيد إلى خارج الـ Jobخارج هدف المراقبة والإنهاء الجماعي

الشكل 16: لـ breakaway وجهان «طريق خروج لـ SDK لازم» و«ثقب مراقبة». إن أضفت فقرّر أيضاً من يرعى الوجهة التي خرجت إليها.

التشغيل بالوكالة عبر WMI لا يُمنَع حتّى بحظر breakaway

مسار تشغيل طرف ثالث بالوكالة مثل Win32_Process.Create في WMI مشكلة أخرى. الأب الفعلي موفّر WMI، لذا العملية المولودة خارج الـ Job من البداية. حتّى بحظر breakaway لا يُسدّ هذا الثقب.1

ما إذا استخدم SDK هذا المسار يُؤكَّد بعلاقة الأب والابن في Process Explorer لا بسجلّ NEW_PROCESS.

أكّد أوّلاً الانتماء إلى Job قائم وقيود ما قبل Windows 7

إجراء عندما يكون الطرف الآخر في Job أصلاً (الحادث 6): أكّد بـ IsProcessInJob ← إن أمكن تركيب تداخل فأسند كما هو ← إن تعذّر (Windows 7 أو قيود واجهة) فغيّر التصميم، بهذا الترتيب.18

قبل Windows 7 لا يمكن Assign مزدوج لطرف ينتمي أصلاً إلى Job آخر. BREAKAWAY_OK ليس علماً يُخرج لاحقاً عملية منتمية. طريق الخروج ذلك لا يقوم إلّا عندما يطلب SDK الطرف الآخر breakaway عند توليد الابن. إن لم يُرجَ ذلك فغيّر التصميم قبل التشغيل على فرض «عملية واحدة لكلّ وظيفة».12

إدخال الأب نفسه في Job يُحكَم أخيراً

أخيراً نلمس أيضاً تصميماً «أدخل عمليتي في Jobي». إدخال الأب نفسه تحته يُدرجك أيضاً في هدف KillOnJobClose عند انهيار الأب، فيتطابق عمر الشجرة كلّها تماماً. لكنّه سيف ذو حدّين يصير إنهاء جماعياً غير مقصود إن أخطأت طريقة حمل مقبض Job، فالأمان البدء من «الأب خارج، شجرة الابن فقط داخل».

10. طريقة التحقيق

يتقدّم التحقيق بترتيب الانتماء ← سجلّ المحاسبة والإشعار ← شغل الجهاز. إجراء تأكيد كي لا تستنتج عدم وجود بقايا بمجرّد النظر إلى اسم العملية.

  • Process Explorer: لخصائص العملية علامة تبويب Job، فيظهر الـ Job المنتمي والقيود. أسرع تأكيد لـ «في أيّ Job هذا المساعد»
  • IsProcessInJob: مدخل تأكيد انتمائك أو الطرف الآخر من الشيفرة18
  • QueryInformationJobObject: سجّل دورياً Basic Accounting (إجمالي العمليات وزمن CPU) وExtended Limit (PeakJobMemoryUsed وغيره)417
  • أبقِ سجلّ منفذ الإكمال في ملفّ: السلسلة الزمنية لـ NEW_PROCESS / EXIT / ABNORMAL_EXIT تصير الدليل الوحيد في تحقيق بعد شهر
  • لا تؤكّد البقايا بـ PID: ما يُنظَر مقبض الجهاز واسم الأنبوب وملفّ القفل. «لا تُرى عملية في Task Manager» لا يعني «حُرِّر الجهاز»
إجراء تحقيق البقاياأكّد الانتماء أوّلاً بـ IsProcessInJob وعلامة تبويب Job في Process Explorer، واقرأ المحاسبة بـ QueryInformationJobObject، وأخيراً احكم البقايا لا بوجود العملية بل بمقبض الجهاز واسم الأنبوب وملفّ القفلأكّد الانتماء بـ IsProcessInJobعلامة تبويب Job في Process Explorerمحاسبة بـ QueryInformationJobObjectحكم البقايا بمقبض الجهاز واسم الأنبوب

الشكل 17: التحقيق ترتيب «انتماء ← محاسبة ← شغل». لا تستنتج «لم يبقَ» بالنظر إلى اسم العملية وحده.

11. دليل تقريبي (جدول قرار)

نلخّص الاختيارات حتّى الآن حسب الموقف. سياسة الإنهاء يمكن الرجوع إليها في الفصل 5، وفرض المراقبة في الفصل 6، والعلاقة مع Job قائم في الفصل 9.

الوضع التوصية
أخرجت جسم الواجهة وSDK الجهاز إلى عملية أخرى أدخل Job وراقب بمنفذ إكمال
شغل الجهاز بعد اختفاء الأب أسوأ شيء أضف KillOnJobClose
الإطار أو التفريغ في لحظة السقوط أصل لا تضف KillOnJobClose، ولتؤمّن جهة المراقبة ثم TerminateJobObject
SDK الشركة المصنّعة يلد مساعداً اجعل التوليد SUSPENDED أو JOB_LIST وسجّل NEW_PROCESS
Assign هو ERROR_ACCESS_DENIED انظر أوّلاً إلى الوظيفة القائمة وإمكان التداخل. اشلك قيود الواجهة
تخرج ابناً لجلسة تفاعلية من خدمة لا تضع قيود واجهة. عُد إلى تصميم مقالة حدود المستخدم
تنتظر إنهاء الحفيد في خيط الواجهة توقّف. أخرجه إلى خيط IOCP

12. خلاصة

عمر الابن ليس عمر كائن Process للأب. ولا ينتقل إنهاء الأب تلقائياً إلى الابن. كائن Job آلية تجعل شجرة العمليات هذه وحدة واحدة وتعالج القيود والإشعار والإنهاء الجماعي.

لإدارة حتّى الحفيد ثبّت الانتماء قبل تشغيل الابن. الإجراء A هو SUSPENDED+Assign، والإجراء B من Windows 10 فما بعده هو JOB_LIST. يعمل KillOnJobClose بإغلاق آخر مقبض Job بغضّ النظر عن سبب إنهاء الأب، لكنّه يفقد أيضاً مادّة التحليل اللاحق للذرّية التي تُنهى معه.

لذا عدّد أوّلاً «ما لا يجوز إبقاؤه بعد السقوط» و«ما تريد إبقاؤه»، واختر الاسترداد الفوري أو الإنهاء بعد الأخذ والتأمين في جهة المراقبة. هذا إجراء التصميم الذي تريد هذه المقالة إيصاله.

إن كتبت تالياً فتفصيل تشغيل عملية يعبر الجلسة 0 والجلسة التفاعلية، أو حديث انتظار الجهاز بإدخال/إخراج متداخل. بعد إمساك «العمر الخارجي» للعملية ينتظر عمر الإدخال/الإخراج.

مقالات ذات صلة

مجالات الاستشارة ذات الصلة

تتعامل شركة كومورا سوفت ذ.م.م. مع تصميم فصل عمليات تطبيقات Windows التي ترتبط بكاميرا وأجهزة قياس وتسلسلي/USB، وتحقيق أعطال شغل الجهاز مثل بقايا مساعد SDK وdevice busy، وبناء آليات مراقبة التطبيقات طويلة التشغيل والتعافي التلقائي. نرحّب بالاستشارة حتّى من حالة «إعادة تشغيل الأب فلا يُفتح الجهاز».

روابط مرجعيّة

  1. Microsoft Learn, Job Objects. حول كون كائن Job كائن نواة يدير مجموعة عمليات كوحدة واحدة، وارتباط العملية الابن التي تصنعها عملية منتمية افتراضياً بالـ Job نفسه (عدا عبر Win32_Process.Create)، وعلمي قيد breakaway، والإنهاء الجماعي بـ TerminateJobObject، وطريقة إدارة شجرة العمليات في بيئة لا يمكن فيها التداخل.  2 3 4 5

  2. Microsoft Learn, AssignProcessToJobObject function (jobapi2.h). حول تعذّر فكّ ارتباط العملية والـ Job، وكون العملية قبل Windows 7 لوظيفة واحدة وإمكان الانتماء المتعدّد (التداخل) من Windows 8، والقيود الفعّالة وانتشار breakaway في التداخل.  2

  3. Microsoft Learn, JOBOBJECT_BASIC_LIMIT_INFORMATION structure (winnt.h). حول أعلام القيد JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE (إنهاء كلّ العمليات عندما يُغلق آخر مقبض Job)، وACTIVE_PROCESS (حدّ عدد العمليات النشطة المتزامنة)، وJOB_MEMORY (حدّ الالتزام للوظيفة كلّها)، وDIE_ON_UNHANDLED_EXCEPTION، وBREAKAWAY_OK / SILENT_BREAKAWAY_OK.  2 3 4 5

  4. Microsoft Learn, JOBOBJECT_BASIC_ACCOUNTING_INFORMATION structure (winnt.h). حول احتفاظ الـ Job بمعلومات محاسبة مثل إجمالي العمليات وزمن CPU وعدد أخطاء الصفحة بما فيها حصّة العمليات المنتهية، وإمكان الجلب بـ QueryInformationJobObject.  2

  5. Microsoft Learn, Nested Jobs. حول صنع الوظائف المتداخلة هرمية أب وابن (الوظيفة الابن مجموعة جزئية من عمليات الوظيفة الأب)، وتعذّر تداخل وظيفة ضُبطت عليها قيود واجهة، وصيرورة القيود الفعّالة القيمة الأشدّ على السلسلة، وإرسال الإشعار إلى كلّ منافذ إكمال سلسلة الوظائف الأب، وبدء إنهاء الهرمية من الطبقة الأدنى.  2

  6. Microsoft Learn, Process Creation Flags. حول CREATE_SUSPENDED (توليد الخيط الأوّلي معلّقاً وعدم التنفيذ حتّى ResumeThread)، وCREATE_BREAKAWAY_FROM_JOB (يلزم JOB_OBJECT_LIMIT_BREAKAWAY_OK على Job المستدعي). 

  7. Raymond Chen, Closing the race window between creating a suspended process and putting it in a job (The Old New Thing). حول الإجراء الكلاسيكي للتوليد بـ CREATE_SUSPENDED ثم الإدخال إلى Job، وطريقة إغلاق نافذة السباق تلك. 

  8. Microsoft Learn, UpdateProcThreadAttribute function (processthreadsapi.h). حول إمكان تخصيص مقابض Job بالترتيب المحدَّد للعملية الابن المولَّدة بـ PROC_THREAD_ATTRIBUTE_JOB_LIST، والدعم من Windows 10 / Windows Server 2016 فما بعده. 

  9. Raymond Chen, A more direct and mistake-free way of creating a process in a job object (The Old New Thing). حول طريقة إسناد العملية إلى Job من لحظة التوليد باستخدام PROC_THREAD_ATTRIBUTE_JOB_LIST. 

  10. Raymond Chen, Destroying all child processes (and grandchildren) when the parent exits (The Old New Thing). حول تكوين إنهاء الذرّية مجتمعة عند اختفاء الأب بـ Job مع KILL_ON_JOB_CLOSE، وأهمّية عدم توريث مقبض Job. 

  11. Microsoft Learn, TerminateJobObject function (jobapi2.h). حول إنهاء كلّ العمليات المرتبطة بالـ Job قسراً كما باستدعاء TerminateProcess لكلّ منها. 

  12. Microsoft Learn, Job Objects - Managing Job Objects. حول صيرورة كائن Job حالة إشارة عندما تنتهي كلّ العمليات بتجاوز حدّ زمن الوظيفة، وإتلاف الـ Job عندما يُغلق آخر مقبض، وتسبيب الإغلاق إنهاء كلّ العمليات المنتمية عند تحديد KILL_ON_JOB_CLOSE. 

  13. Microsoft Learn, JOBOBJECT_ASSOCIATE_COMPLETION_PORT structure (winnt.h). حول قائمة الرسائل المُرسَلة إلى منفذ الإكمال مثل JOB_OBJECT_MSG_NEW_PROCESS / EXIT_PROCESS / ABNORMAL_EXIT_PROCESS / ACTIVE_PROCESS_ZERO، ورموز الإنهاء المحكوم عليها كإنهاء غير طبيعي، وتعذّر نفي إعادة استخدام PID في رسائل تعيد PID ما لم يُحفَظ مقبض العملية، وعدم ضمان تسليم الإشعار.  2 3

  14. Raymond Chen, How do I wait until all processes in a job have exited? (The Old New Thing). حول تعذّر رصد «صار فارغاً» بانتظار مقبض Job، وضرورة الانتظار بـ JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO لمنفذ الإكمال. 

  15. Microsoft Learn, Job Objects - Job Limits and Notifications. حول أفضلية ربط منفذ الإكمال بينما الـ Job غير نشط (تقليل إمكان تفويت إشعار عملية تغيّرت حالتها أثناء الربط)، وعدم ضمان تسليم الرسائل عدا الحدّ المضبوط بـ JobObjectNotificationLimitInformation، ومواصلة العملية الجري بعد التجاوز في حدّ الإشعار.  2 3

  16. Raymond Chen, Removing the TerminateThread from code that waits for a job object to empty (The Old New Thing). حول طريقة إعادة كتابة النمط القديم لقتل خيط الانتظار بـ TerminateThread إلى انتظار قائم على منفذ إكمال. 

  17. Microsoft Learn, JOBOBJECT_EXTENDED_LIMIT_INFORMATION structure (winnt.h). حول ضبط حدّ الذاكرة لكلّ عملية ولكلّ وظيفة، وجلب ذروة الذاكرة بـ PeakProcessMemoryUsed / PeakJobMemoryUsed.  2

  18. Microsoft Learn, IsProcessInJob function (jobapi.h). حول إمكان الحكم ما إذا كانت العملية تعمل في الـ Job المحدَّد (أو أيّ Job).  2 3

  19. Microsoft Learn, JOBOBJECT_CPU_RATE_CONTROL_INFORMATION structure (winnt.h). حول إمكان التحكّم في معدّل CPU (نسبة الدورات أو الوزن) لكلّ Job. 

أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.

ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.

الأسئلة الشائعة

أسئلة شائعة حول موضوع هذه المقالة.

هل كائن Job حاوية أو صندوق رمل؟
لا. كائن Job كائن نواة يضع على مجموعة عمليات «قيوداً» و«إشعاراً» و«إنهاء جماعياً». يمكن فرض حدود للذاكرة وCPU وعدد العمليات، لكن لا يمكن تقييد الوصول إلى الشبكة، ولا يتغيّر رمز الوصول (الصلاحيات). توجد قيود واجهة، لكنّها وحدها لا تصير حدوداً أمنية. إن كان الغرض الفصل أو الأمن يلزم الجمع مع آلية أخرى مثل AppContainer أو الحاويات. ما تعالجه هذه المقالة استخدام «معاملة عمر شجرة العمليات والموارد كوحدة واحدة».
ألا يكفي Process.Kill()؟
ينهى Process.Kill() تلك العملية الواحدة فقط ولا يصل إلى العمليات الحفيدة. Kill(entireProcessTree: true) من .NET Core 3.0 فما بعده يتتبّع الذرّية وينهيها، لكنّه يعدّد شجرة العمليات من علاقة الأب والابن في تلك اللحظة، فيبقى مجال لتفويت عملية وُلدت أثناء التعداد أو عملية انقطع نسبها بموت الأب أوّلاً. كذلك لا يُستدعى أيّ من الطريقتين «عندما ينهار الأب نفسه». إن أردت استرداد الذرّية بغضّ النظر عن حياة الأب فالموثوق إيكال العمر إلى النواة بـ Job Object + KillOnJobClose.
إن كان الأب في Job، هل يدخل الابن Job تلقائياً أيضاً؟
افتراضياً نعم. العملية الابن التي يصنعها عملية تنتمي إلى Job بـ CreateProcess تنتمي تلقائياً إلى الـ Job نفسه. الاستثناء breakaway. عندما يُضبط JOB_OBJECT_LIMIT_BREAKAWAY_OK على الـ Job ويُولَّد الابن بعلم CREATE_BREAKAWAY_FROM_JOB، أو عندما يُضبط JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK، يُولد الابن خارج الـ Job. العملية المصنوعة عبر Win32_Process.Create في WMI لا ترتبط بـ Job.
هل يمكن إخراج عملية وُضعت في Job منه؟
لا. الارتباط بـ AssignProcessToJobObject غير قابل للعكس، ويستمرّ الانتماء حتّى تنتهي العملية. لذا خيارات التصميم ثلاثة: «لا تُدخل» و«وُلد خارجاً من البداية بـ breakaway» و«تداخل Job آخر»، وليس «أخرج لاحقاً». هذا اللاتعكاس سبب وجوب تجهيز الـ Job قبل التوليد أيضاً.
هل يعمل KillOnJobClose عند انهيار الأب أيضاً؟
نعم. JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE آلية تنهي من تحته «عندما يُغلق آخر مقبض Job»، وتُطلَق سواء انتهى الأب طبيعياً أو سقط باستثناء غير معالَج أو قُتل من Task Manager، لأن النواة تغلق المقابض كتنظيف للعملية. لكن إن وُرِّث مقبض Job إلى العملية الابن، يبقى المقبض الذي يحمله الابن بعد موت الأب فلا يصير «آخر مقبض» ولا يُطلَق. لا تورّث مقبض Job.
هل يصل إشعار منفذ الإكمال حتماً؟
قد لا يصل. تنصّ الوثائق الرسمية صراحة على أن تسليم الرسائل إلى منفذ الإكمال غير مضمون، عدا إشعار الحدّ المضبوط بـ JobObjectNotificationLimitInformation. عدم وصول إشعار لا يعني أن الحدث لم يقع. للمراقبة التي تحتاج يقيناً اجمع استطلاع معلومات المحاسبة بـ QueryInformationJobObject، واحفظ مقابض العمليات بنفسك لتثبيت الحياة والموت.
هل لـ .NET واجهة رسمية لكائن Job؟
لا. ليس لـ System.Diagnostics.Process مفهوم Job، ولا غلاف في BCL. الحلّ العملي استدعاء CreateJobObject / SetInformationJobObject / AssignProcessToJobObject بـ P/Invoke، أو توليد التوقيع بمولّد المصدر CsWin32 من Microsoft وكتابة غلاف رقيق. لفّ مقبض Job بـ SafeHandle وصمّم إغلاقه في Dispose لـ IDisposable فيظهر معنى KillOnJobClose «عمر الغلاف = عمر شجرة العملية الابن» في الشيفرة كما هو.
كيف أصمّم على جهاز قياس يعمل بـ Windows 7؟
قبل Windows 7 لا تدخل العملية سوى Job واحد ولا يمكن التداخل. إن استخدم SDK الطرف الآخر Job بنفسه فشل AssignProcessToJobObject هنا. JOB_OBJECT_LIMIT_BREAKAWAY_OK علم يسمح لـ «عملية دخلت Jobها أصلاً أن تصنع ابناً خارج الـ Job بعلم CREATE_BREAKAWAY_FROM_JOB»، وليس سحراً يمرّر Assign مزدوجاً بعد الدخول. أي أن طريق الخروج لا يقوم إلّا عندما يطلب SDK الطرف الآخر breakaway عند التوليد. إن لم يُرجَ ذلك فلا خيار سوى تغيير التصميم قبل التشغيل على فرض «Job واحد فقط لي». تعرض وثائق Microsoft أيضاً طريقة إدارة الشجرة بعلمي القيد لـ breakaway في بيئة بلا تداخل. ومع ذلك نظام التشغيل منتهي الدعم، فالترحيل أولى إن أمكن.

الملف الشخصي للمؤلف

صفحة الملف الشخصي لمؤلف المقالة.

غو كومورا

مؤسّس شركة كومورا سوفت ذ.م.م.

يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.

العودة إلى المدونة