سجل التعديلات (النسخة الأولى، نُشرت في 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 |
flowchart TB
accTitle: انحراف عمر الأب عن عمر شغل الجهاز
accDescr: بإنهاء العملية الأب يختفي انتظار الأب وكائن Process، لكن عمليّات الابن والحفيد تبقى حيّة، ويبقى شغل مقابض الجهاز والأنابيب المسمّاة وملفّات القفل
parent["إنهاء العملية الأب"] --> gone["يختفي: انتظار الأب وكائن Process"]
parent --> live["يبقى: عمليّات الابن والحفيد"]
live --> dev["شغل مقبض الجهاز"]
live --> pipe["جهة الخادم للأنبوب المسمّى"]
live --> lock["ملفّ قفل وذاكرة مشتركة"]
الشكل 1: عمر الأب وعمر شغل الجهاز منحرفان. شيفرة تنظيف جهة الأب لا تعمل بقدر ما كان موت الأب غير طبيعي.
الهدف حالات «نظام التشغيل يعمل والأب وحده انتهى»
تدفّق إيقاف نظام التشغيل كلّه بالإيقاف أو النوم مجال مقالة الإيقاف ومقالة العودة من النوم. تعالج هذه المقالة حالات انتهاء الأب وحده ونظام التشغيل بخير. في ميدان القياس هذه أكثر تواتراً، والبقايا أصعب ملاحظة.
3. ما كائن Job
العمليّات الأساسية: اصنع، أسند، اضبط، افحص
كائن Job كائن نواة يدير مجموعة عمليات كوحدة واحدة. بتقسيم العمليّات الأساسية حسب الدور تصير الأربع التالية.14
| الواجهة | الدور |
|---|---|
CreateJobObject |
اصنع Job لم تنتم إليه عملية بعد |
AssignProcessToJobObject |
أسند عملية إلى Job |
SetInformationJobObject |
اضبط قيوداً وغيرها |
QueryInformationJobObject |
اقرأ معلومات محاسبة مثل زمن CPU وأخطاء الصفحة وعدد العمليات |
الانتماء غير قابل للعكس، ولا تخرج العملية حتّى تنتهي. كذلك تتضمّن معلومات المحاسبة حصّة العمليات المنتهية أيضاً.
الابن الذي تصنعه عملية منتمية بـ CreateProcess ينتمي افتراضياً إلى الـ Job نفسه. أي أن الحفيد وابن الحفيد يدخلان تلقائياً في مركز قيمة الـ Job.1 لكن مسارات الخروج من الانتماء مثل breakaway والتشغيل بالوكالة عبر WMI تُؤكَّد مفصولة في الفصل 9.
flowchart TB
accTitle: البنية الأساسية لكائن Job
accDescr: ينتمي الابن والحفيد إلى كائن Job صنعه الأب، ويفرض الـ Job القيود ويُشعر منفذ الإكمال وينهي جماعياً بوحدة شجرة العمليات
parent["العملية الأب"] --> job["كائن Job"]
job --> child["الابن (مضيف SDK الجهاز)"]
child --> gc1["الحفيد (مساعد الشركة المصنّعة)"]
job -.-> lim["قيود (ذاكرة وCPU)"]
job -.-> note["إشعار (منفذ إكمال)"]
job -.-> kill["إنهاء جماعي"]
الشكل 2: الـ Job وعاء يقدّم «قيوداً» و«إشعاراً» و«إنهاء جماعياً» بوحدة شجرة العمليات.
فرق جيل نظام التشغيل والفرق عن صندوق الرمل
قبل Windows 7 عملية واحدة لكلّ وظيفة، ومن Windows 8 صار التداخل (انتماء متعدّد) ممكناً.5 المتن يفترض Windows 10/11، ونقاط الانتباه قبل Windows 7 في الفصل 9 والأسئلة الشائعة.
إدخال Job وحده لا يصير حاوية أو صندوق رمل. لا يمكن تقييد الوصول إلى الشبكة، ورمز الوصول (الصلاحيات) آلية أخرى. ولا يمكن صنع حدود أمنية بقيود الواجهة وحدها. الدور في هذه المقالة جعل عمر شجرة العمليات والموارد وحدة واحدة فحسب.
4. طريقة الإدخال الصحيحة ── سباق التوليد والانتماء
إن أسندت بعد التشغيل وُلد حفيد في الأثناء
قد تلد العملية الابن حفيداً في أوّل ميلّي ثوانٍ بعد أن تبدأ الجري. تشغيل مساعد SDK نموذجي. إن أخذت PID بعد Process.Start() ثم أسندت، يخرج الحفيد الذي وُلد قبل Assign خارج الـ Job.
flowchart TB
accTitle: الإدخال بعد بدء الجري يفوّت الحفيد
accDescr: يجري الابن من لحظة Process.Start، والحفيد الذي ولده الابن أثناء نافذة السباق حتّى استدعاء AssignProcessToJobObject يخرج خارج الـ Job
s["يبدأ الابن الجري بـ Process.Start"] --> w["نافذة السباق حتّى Assign"]
w --> g["الحفيد الذي وُلد في هذه الأثناء"]
g --> out["يواصل الجري خارج الـ Job"]
s --> a2["AssignProcessToJobObject"]
a2 --> in2["يدخل فقط الحفيد الذي يُولد بعده"]
الشكل 3: نافذة السباق ميلّي ثوانٍ، وتشغيل مساعد SDK يحدث هناك تماماً.
الإجراء A: ولّد متوقّفاً، أسند، ثم شغّل
الطريقة الكلاسيكية الواسعة التوافق إجراء يستخدم CREATE_SUSPENDED. تغلق نافذة سبق الابن في الحركة وولادة حفيد بهذا الترتيب.67
- اصنع Job بـ
CreateJobObject - اضبط القيود أوّلاً بـ
SetInformationJobObject - نفّذ
CreateProcessبعلمCREATE_SUSPENDED(الخيط الأوّلي لا يجري) - أدخل بـ
AssignProcessToJobObject - إن فشل فلا تستأنف، و
TerminateProcessفي المكان (لا تشغّل أمراً واحداً خارج الـ Job) - شغّل بـ
ResumeThread
ما يغلقه الإجراء A نافذة السباق مع توليد الحفيد. تبقى نافذة تجاه انهيار الأب نفسه. إن انهار الأب بين الخطوتين 3 و4 بقي ابن معلَّق لم يدخل Job بعد. لا يجري، لكنّه لا يختفي تلقائياً.
إن أردت إغلاق هذه النافذة بما فيها مقاومة انهيار الأب فاستخدم الإجراء B التالي.
flowchart TB
accTitle: إجراء التشغيل بـ SUSPENDED ثم الإدخال إلى Job
accDescr: اصنع Job واضبط القيود، وشغّل الابن بـ CREATE_SUSPENDED وأسند بـ AssignProcessToJobObject، وإن فشل فأوقف بـ TerminateProcess دون Resume، وإن نجح فشغّل بـ ResumeThread
a["اصنع Job بـ CreateJobObject"] --> b["قيود بـ SetInformationJobObject"]
b --> c["شغّل الابن بـ CREATE_SUSPENDED"]
c --> d["AssignProcessToJobObject"]
d -->|"نجاح"| e["شغّل بـ ResumeThread"]
d -->|"فشل"| f["أنه فوراً دون 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 فما بعده. حدّد قائمة السمات وعلم التشغيل الموسَّع |
flowchart TB
accTitle: فرق نافذة السباق بين الإجراءين A وB
accDescr: يولّد الإجراء A متوقّفاً ثم يقوم بـ Assign وResume فيلزم فرع إنهاء عند الفشل، بينما يولّد الإجراء B بوضع Job في قائمة السمات فينتمي من لحظة الولادة ولا توجد نافذة سباق ولا فرع فشل
a1["الإجراء A: ولّد متوقّفاً"] --> a2["انتماء بـ Assign"]
a2 --> a3["ابدأ بـ Resume"]
a2 -.-> a4["فرع الفشل إلزامي"]
b1["الإجراء B: ولّد بقائمة سمات"] --> b2["منتمٍ من لحظة الولادة"]
b2 -.-> b3["لا نافذة سباق ولا فرع فشل"]
الشكل 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 إلى الابن يُبقي المقبض بعد إنهاء الأب. عندئذ لا يصير «آخر مقبض» ولا تنتهي شجرة الابن.
flowchart TB
accTitle: خطّ زمن KillOnJobClose
accDescr: سواء انتهى الأب طبيعياً أو انهار أو أُنهي قسراً، تغلق النواة كلّ مقابض الأب، وإن كان ذلك آخر مقبض Job أُغلق الـ Job وانتهت شجرة العمليات تحته جماعياً
die["اختفاء الأب (بما فيه الانهيار)"] --> close["تغلق النواة كلّ المقابض"]
close --> last{"آخر مقبض Job؟"}
last -->|"نعم"| killall["إنهاء الشجرة تحته جماعياً"]
last -->|"لا (مع وراثة)"| stay["يبقى الابن حيّاً"]
الشكل 6: شرط الإطلاق ليس «موت الأب» بل «إغلاق آخر مقبض». لذا لا تورّث المقبض إلى الابن.
اختر سياسة الاسترداد الفوري أو إبقاء مادّة التشخيص
ما يُفقَد بالإنهاء القسري ليس شغل الجهاز فقط. تُفقَد أيضاً فرصة أخذ تفريغ انهيار الابن والحفيد اللذين يُنهيان معه، والإطار الصالح الأخير، وفرصة تفريغ ملفّ قياس قيد الكتابة.
تفريغ انهيار الأب نفسه مختلف. يعالج WER الاستثناء غير المعالَج أثناء حياة الأب، لذا يمكن كتابة تفريغ الأب قبل إغلاق المقبض. ما نناقشه هنا مادّة التحليل اللاحق للابن والحفيد في جهة الإنهاء القسري.
| السياسة | الميدان المناسب | ما يُفقَد |
|---|---|---|
| مع KillOnJobClose | ميدان الفتح المزدوج للجهاز أسوأ شيء | مادّة التحليل اللاحق، العيّنة الأخيرة |
| بلا (مراقبة فقط) | ميدان التفريغ والسجلّ أصل | إن تُركت عمليات يتيمة وشغل منفذ |
بلا + عملية مراقبة تستدعي TerminateJobObject |
عندما توجد خدمة تحكّم على حدة | يصير التنفيذ مزدوجاً |
كمادّة حكم عدّد أوّلاً «ما لا يجوز إبقاؤه» و«ما تريد إبقاؤه».
ما لا يجوز إبقاؤه: فتح الكاميرا/الرقمنة، حصرية التسلسلي وUSB، جهة خادم الأنبوب المسمّى، جلسة دونغل الترخيص، الذاكرة المشتركة وملفّ القفل.
ما تريد إبقاؤه: تفريغ الانهيار، الإطار/العدّاد الصالح الأخير، فرصة إرسال أمر إعادة الجهاز إلى الجانب الآمن (أرسل قبل القتل إن أمكن).
flowchart TB
accTitle: أقتل أم أراقب فقط
accDescr: إن كان الفتح المزدوج للجهاز أسوأ شيء فأضف KillOnJobClose، وإن كان تفريغ الانهيار والإطار الأخير أصلاً فلا تضف وراقب، وإن وُجدت خدمة تحكّم أخرى فاستدعِ TerminateJobObject من هناك
q{"ماذا تحمي في لحظة السقوط؟"} -->|"تحرير الجهاز أولوية قصوى"| k["مع KillOnJobClose"]
q -->|"التفريغ والحالة الأخيرة أصل"| m["مراقبة فقط (لا تقتل)"]
q -->|"خدمة التحكّم موجودة على حدة"| t["جهة المراقبة تستدعي TerminateJobObject"]
الشكل 7: لا تختر «أقتل أم لا» بل «ماذا تحمي في لحظة السقوط». إن أردت الاثنين يصير تكوين الصفّ الثالث: جهة المراقبة تأخذ التفريغ أوّلاً ثم تطوي.
مرّر مقبض Job إلى جهة المراقبة بينما الأب حيّ
لتكوين الصفّ الثالث في الجدول، إنهاء عملية المراقبة بـ TerminateJobObject، يلزم إعداد. يجب أن تحصل جهة المراقبة على مقبض Job قبل موت الأب.
الـ Job المصنوع بإجراء هذه المقالة بلا اسم، لذا لا سبيل لتتبّعه من الخارج بعد اختفاء الأب. طريقتا التمرير التاليتان.
| الطريقة | ما تفعله بينما الأب حيّ |
|---|---|
| انسخ مقبض Job بلا اسم | مرّر المقبض إلى عملية المراقبة بـ DuplicateHandle |
| استخدم Job مسمّى | اصنعه مسمّى من البداية، ولتفتح جهة المراقبة بـ OpenJobObject وتحتفظ |
قد يتصادم الاسم عالمياً لذا ضمّن GUID فريداً ونحوه. إن نسيت هذا الإعداد وجدت جهة المراقبة شذوذ الأب دون وسيلة لإنهاء الشجرة.
أخذ التفريغ وتأمين الجهاز قبل الإنهاء القسري
لابن يُنهى بـ KillOnJobClose، كما مع TerminateProcess، لا مقدّمة. لا يحدث استثناء غير معالَج، لذا حتّى مع ضبط WER (LocalDumps) للابن لا يبقى تفريغ ذلك الإنهاء القسري. ما يلتقطه WER حالة إنهاء الابن بانهياره نفسه.
إن كان المتطلّب «استرداداً تلقائياً وتفريغاً أيضاً» فانقل فاعل الإنهاء إلى جهة المراقبة. الترتيب أخذ التفريغ بينما الهدف حيّ، وإعادة الجهاز إلى الجانب الآمن إن لزم، وأخيراً الإنهاء بـ TerminateJobObject.
flowchart TB
accTitle: طيّ يوفّق الاسترداد التلقائي والتفريغ
accDescr: عندما ترصد عملية المراقبة شذوذاً تأخذ التفريغ أوّلاً، وترسل إن لزم أمراً يعيد الجهاز إلى الجانب الآمن، وأخيراً تطوي الشجرة بـ TerminateJobObject، فتوفّق الاسترداد التلقائي والتحليل اللاحق
det["ترصد جهة المراقبة شذوذاً"] --> dmp["خذ التفريغ أوّلاً"]
dmp --> safe["أَعِد الجهاز إلى الجانب الآمن"]
safe --> term["اطوِ بـ 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.
sequenceDiagram
accTitle: تدفّق إشعار منفذ الإكمال
accDescr: يلقي الـ Job رسائل حدوث العملية وإنهائها والإنهاء غير الطبيعي والصفر إلى منفذ الإكمال، ويتلقّاها خيط مراقبة مخصَّص بـ GetQueuedCompletionStatus، ويمرّر النتيجة فقط إلى خيط الواجهة
participant J as كائن Job
participant P as منفذ الإكمال
participant W as خيط المراقبة
participant U as خيط الواجهة
J->>P: NEW / EXIT_PROCESS
J->>P: ABNORMAL_EXIT / ZERO
W->>P: انتظر حزمة الإكمال
P-->>W: الرسالة وPID
W-->>U: مرّر إشعار النتيجة فقط
الشكل 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.
flowchart TB
accTitle: مراقبة لا تعتمد على الإشعار وحده
accDescr: رسائل منفذ الإكمال لغرض الإشعار بلا ضمان وصول، لذا اجمع استطلاع معلومات المحاسبة وحفظ مقبض العملية استعداداً للتفويت وإعادة استخدام PID
n["إشعار منفذ الإكمال"] --> miss["بلا ضمان وصول (لغرض الإشعار)"]
miss --> poll["جمع استطلاع معلومات المحاسبة"]
n --> pid["يُعاد استخدام PID"]
pid --> hold["احفظ مقبض العملية"]
الشكل 10: الإشعار المسار الرئيس، والمحاسبة والمقبض تأمين. لا يقال «نرصد» إلّا باكتمال الاثنين.
يُذكر أن تنفيذاً مثل «اقتل بـ TerminateThread الخيط الذي ينتظر فراغ الـ Job» شاع سابقاً، لكن مع منفذ إكمال لا يلزم الإنهاء القسري لخيط الانتظار. نشر Raymond Chen أيضاً مقالة تعيد كتابة هذا النمط القديم إلى طريقة منفذ الإكمال.16
7. حوادث تقع فعلاً في القياس والربط بالجهاز
نطبّق الآلية حتّى الآن على ستّ حوادث تقع في ميدان الجهاز. في كلّ مثال نؤكّد السبب والتدبير إضافة إلى «ما بقي».
الحادث 1: مات الأب وحده وبقيت الكاميرا مفتوحة
في تكوين يملك SDK الشركة المصنّعة عملية مساعد لنقل الإطارات، إسقاط واجهة الأب من Task Manager يُبقي المساعد وحده. الأب المعاد تشغيله device busy عند تهيئة SDK. في بعض الميادين لا يتعافى حتّى إعادة تشغيل طاقة الجهاز.
ما بقي: عملية المساعد وفتح الكاميرا الحصري.
flowchart TB
accTitle: مات الأب وحده وبقيت الكاميرا مفتوحة
accDescr: بإنهاء الواجهة قسراً يختفي الأب لكن عملية مساعد SDK تبقى ماسكة مقبض الكاميرا، فيفشل الأب المعاد تشغيله في إعادة الفتح بـ device busy
kill9["أنه الواجهة من Task Manager"] --> dead["يختفي الأب"]
dead --> helper["يبقى مساعد SDK"]
helper --> busy["ماسك الكاميرا"]
busy --> fail["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 ابتُلع فشله.
flowchart TB
accTitle: مساران لخروج الحفيد خارج الـ Job
accDescr: مسار استخدام SDK Jobه يبقى مجال التقاط بالتداخل من Windows 8 ويفشل إن وُجدت قيود واجهة، ومسار توليد SDK حفيداً بـ breakaway لا يقوم إلّا عندما تسمح أنت ويصير الحفيد خارج الشجرة من البداية
g["يخرج الحفيد خارج الـ Job"] --> ja["(أ) SDK يستخدم Jobه"]
g --> jb["(ب) توليد بـ breakaway"]
ja --> nest["من Win8 التقاط بالتداخل"]
nest -.-> ui["يفشل إن وُجدت قيود واجهة"]
jb --> allow["لا يقوم إلّا عند السماح"]
allow -.-> out["الحفيد خارج الشجرة من البداية"]
الشكل 12: حتّى مع «الخروج» نفسه، (أ) يبقى مجال التقاط بالتداخل، و(ب) يتحدّد عدم إمكان الالتقاط لحظة السماح. يبدأ التدبير بتحديد المسار.
الحادث 3: عملية جهاز شُغِّلت من خدمة
ما ينتظره SCM عند إيقاف الخدمة الأب فقط، والابن والحفيد المولودان في الجلسة 0 خارج ولاية معالجة الإيقاف. Job+KillOnJobClose يرعى حتّى خارج هذه الولاية. تصميم التكوين الذي يعبر الخدمة والجلسة التفاعلية نفسَه يُوكَل إلى مقالة حدود المستخدم.
ما بقي: عملية متبقية في الجلسة 0، وانفجار خاطئ لحكم التشغيل المزدوج عند التشغيل التالي.
الحادث 4: ابن يسقط بعد شهر
دون تسجيل محاسبة الـ Job دورياً (PeakJobMemoryUsed وعدّادات الإدخال/الإخراج وإجمالي العمليات) لا يمكن تتبّع «مساعد أيّ جيل بدأ الانتفاخ متى» لاحقاً.17 بنية السقوط بعد شهر بتسرّب مقابض كما شُرِّحت في مقالة العطل طويل الأمد لكاميرا صناعية، ومحاسبة الـ Job مدخل ذلك التحقيق.
ما بقي: سجلّ لا يكفي لتثبيت السبب.
الحادث 5: إنهاء الأب ينتظر إنهاء الابن
انتظار WaitForExit أو إنهاء الشجرة في خيط الواجهة يجعل الأب «لا يستجيب» يوم يتجمّد الابن. أوكِل انتظار الإنهاء إلى خيط IOCP، وضع في الواجهة التقدّم وزر القطع فقط. الآلية كما في مقالة «لا يستجيب».
ما بقي: أب علّق معه.
flowchart TB
accTitle: لا تنتظر الإنهاء في خيط واجهة الأب
accDescr: انتظار إنهاء الابن في خيط الواجهة ينشر تعليق الابن إلى عدم استجابة الأب، لذا أوكِل انتظار الإنهاء إلى خيط مراقبة IOCP وضع في خيط الواجهة عرض التقدّم وزر القطع فقط
w2["انتظر إنهاء الابن في خيط الواجهة"] --> h2["يوم علّق الابن"]
h2 --> f2["الأب أيضاً لا يستجيب (معه)"]
ok2["انتظر في خيط IOCP"] --> u2["الواجهة تقدّم وقطع فقط"]
الشكل 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 |
ضبط دون معرفة هذا الفرق يُشخَّص «مساعد يختفي فور التوليد» كعطل آخر. في التشغيل الطويل الترتيب الآمن الرصد أوّلاً بحدّ إشعار، ثم تقرير الحدّ القسري بعد اتّضاح الميل.
flowchart TB
accTitle: حدّ للإشعار وحدّ للفرض
accDescr: حدّ JobObjectNotificationLimitInformation يُشعر بالتجاوز فقط وتواصل العملية الجري، وحدّ Extended Limit فرض، وحدّ الذاكرة فشل العملية، وACTIVE_PROCESS فشل التوليد أو الانتماء، وحدّ الزمن إنهاء العملية
lim2{"غرض الحدّ؟"} -->|"أريد الرصد"| ntf["حدّ إشعار: يجري حتّى عند التجاوز"]
lim2 -->|"أريد الإيقاف"| enf["حدّ قسري: رفض أو إنهاء"]
ntf --> log2["حدّد الجيل بسجلّ المحاسبة"]
enf --> die2["فشل التزام ورفض توليد وإنهاء"]
الشكل 14: حتّى مع «حدّ» نفسه الإشعار والفرض شيئان مختلفان، وفعل الفرض يختلف لكلّ حدّ. فرض حدّ قسري فجأة بلا رصد ينفجر خطأ على جبل التشغيل الطبيعي.
علاقة التحكّم في معدّل CPU والمعالجة الدورية تُوكَل إلى مقالة الوقت الحقيقي الليّن، ونكتفي هنا بتدبير «عملية الجهاز تأكل الواجهة» تقريباً.
9. التداخل وBreakaway وطرف دخل Job أصلاً
فكّر في التداخل كـ «تضمين مجموعة عمليات»
يمكن ترتيب قواعد التداخل من Windows 8 في أربع.5
- الوظيفة الأب مجموعة أوسع، والوظيفة الابن مجموعة جزئية منها (يفشل Assign بترتيب لا يقوم فيه هذا التضمين)
- القيود الرئيسة للموارد يصير الأشدّ على السلسلة هو الفعّال
- وظيفة وُضعت عليها قيود واجهة لا يمكن تداخلها
- يصل الإشعار أيضاً إلى منافذ إكمال كلّ الوظائف الأب على السلسلة (يجوز ألّا يكون لجهة الوظيفة الابن منفذ)
flowchart TB
accTitle: هرمية الوظائف المتداخلة والقيود الفعّالة
accDescr: الوظيفة الأب مجموعة أوسع والوظيفة الابن مجموعة جزئية منها، وتصير القيمة الأشدّ على السلسلة هي القيود الفعّالة للموارد الرئيسة. وظيفة وُضعت عليها قيود واجهة لا يمكن تداخلها
pj["الوظيفة الأب (مجموعة أوسع)"] --> cj["الوظيفة الابن (مجموعة جزئية)"]
cj --> pr["العملية المنتمية"]
pj -.-> eff["القيود الفعّالة = القيمة الأشدّ"]
cj -.-> eff
ui["وظيفة بقيود واجهة"] -.-> no["لا يمكن التداخل"]
الشكل 15: فكّر في التداخل كـ «تضمين مجموعة». قيود الواجهة تكسر التداخل لذا من الحصافة ألّا تضعها على Job إدارة العمر.
Breakaway مسار توليد خارج الـ Job من البداية
breakaway مسار نظامي يخرج فيه الذرّية المولودة بـ CreateProcess من الشجرة.3
| إعداد جهة الـ Job | شرط ولادة الابن خارج الـ Job |
|---|---|
JOB_OBJECT_LIMIT_BREAKAWAY_OK |
ولّد بتحديد CREATE_BREAKAWAY_FROM_JOB |
SILENT_BREAKAWAY_OK |
لا يلزم تحديد علم. يُولد كلّ ابن خارجاً |
قد يلزم لـ SDK يستخدم Jobه. لكن العملية المولودة بذلك المسار تخرج من الإنهاء الجماعي ومن المراقبة أيضاً. إن سمحت فقرّر أيضاً من يدير الوجهة التي خرجت إليها.
flowchart TB
accTitle: مسار الخروج من الشجرة بـ Breakaway
accDescr: عندما يكون BREAKAWAY_OK على الـ Job يُولد الحفيد المصنوع بـ CREATE_BREAKAWAY_FROM_JOB خارج الـ Job ويختفي من هدف الإنهاء الجماعي والمراقبة
j2["Job (مع BREAKAWAY_OK)"] --> c2["العملية الابن"]
c2 -->|"توليد معتاد"| in3["الحفيد أيضاً داخل الـ Job"]
c2 -->|"توليد بتحديد BREAKAWAY"| out3["الحفيد إلى خارج الـ Job"]
out3 --> lost["خارج هدف المراقبة والإنهاء الجماعي"]
الشكل 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: مدخل تأكيد انتمائك أو الطرف الآخر من الشيفرة18QueryInformationJobObject: سجّل دورياً Basic Accounting (إجمالي العمليات وزمن CPU) وExtended Limit (PeakJobMemoryUsedوغيره)417- أبقِ سجلّ منفذ الإكمال في ملفّ: السلسلة الزمنية لـ NEW_PROCESS / EXIT / ABNORMAL_EXIT تصير الدليل الوحيد في تحقيق بعد شهر
- لا تؤكّد البقايا بـ PID: ما يُنظَر مقبض الجهاز واسم الأنبوب وملفّ القفل. «لا تُرى عملية في Task Manager» لا يعني «حُرِّر الجهاز»
flowchart TB
accTitle: إجراء تحقيق البقايا
accDescr: أكّد الانتماء أوّلاً بـ IsProcessInJob وعلامة تبويب Job في Process Explorer، واقرأ المحاسبة بـ QueryInformationJobObject، وأخيراً احكم البقايا لا بوجود العملية بل بمقبض الجهاز واسم الأنبوب وملفّ القفل
s1["أكّد الانتماء بـ IsProcessInJob"] --> s2["علامة تبويب Job في Process Explorer"]
s2 --> s3["محاسبة بـ QueryInformationJobObject"]
s3 --> s4["حكم البقايا بمقبض الجهاز واسم الأنبوب"]
الشكل 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 ── أفضل ممارسات كائنات Job وانتشار الخروج والإدخال/الإخراج القياسي وwatchdog
- لماذا يحدث «لا يستجيب» في تطبيقات Windows ── حلقة الرسائل وآلية التعليق
- الأنابيب المسمّاة عملياً ── تصميم الاتصال بين عمليات Windows حتّى الأمن
- السقوط بعد شهر بتسرّب مقابض ── تشريح عطل تشغيل طويل لتطبيق كاميرا صناعية (الجزء الأوّل)
- إلى أيّ حدّ يمكن الوقت الحقيقي الليّن على Windows ── دليل عملي
- «الجهاز نفسه» ليس بيئة التنفيذ نفسها ── حدود المستخدم التي تفصل AppData وHKCU وDPAPI وبيانات الاعتماد
مجالات الاستشارة ذات الصلة
تتعامل شركة كومورا سوفت ذ.م.م. مع تصميم فصل عمليات تطبيقات Windows التي ترتبط بكاميرا وأجهزة قياس وتسلسلي/USB، وتحقيق أعطال شغل الجهاز مثل بقايا مساعد SDK وdevice busy، وبناء آليات مراقبة التطبيقات طويلة التشغيل والتعافي التلقائي. نرحّب بالاستشارة حتّى من حالة «إعادة تشغيل الأب فلا يُفتح الجهاز».
روابط مرجعيّة
-
Microsoft Learn, Job Objects. حول كون كائن Job كائن نواة يدير مجموعة عمليات كوحدة واحدة، وارتباط العملية الابن التي تصنعها عملية منتمية افتراضياً بالـ Job نفسه (عدا عبر Win32_Process.Create)، وعلمي قيد breakaway، والإنهاء الجماعي بـ TerminateJobObject، وطريقة إدارة شجرة العمليات في بيئة لا يمكن فيها التداخل. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, AssignProcessToJobObject function (jobapi2.h). حول تعذّر فكّ ارتباط العملية والـ Job، وكون العملية قبل Windows 7 لوظيفة واحدة وإمكان الانتماء المتعدّد (التداخل) من Windows 8، والقيود الفعّالة وانتشار breakaway في التداخل. ↩ ↩2
-
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
-
Microsoft Learn, JOBOBJECT_BASIC_ACCOUNTING_INFORMATION structure (winnt.h). حول احتفاظ الـ Job بمعلومات محاسبة مثل إجمالي العمليات وزمن CPU وعدد أخطاء الصفحة بما فيها حصّة العمليات المنتهية، وإمكان الجلب بـ QueryInformationJobObject. ↩ ↩2
-
Microsoft Learn, Nested Jobs. حول صنع الوظائف المتداخلة هرمية أب وابن (الوظيفة الابن مجموعة جزئية من عمليات الوظيفة الأب)، وتعذّر تداخل وظيفة ضُبطت عليها قيود واجهة، وصيرورة القيود الفعّالة القيمة الأشدّ على السلسلة، وإرسال الإشعار إلى كلّ منافذ إكمال سلسلة الوظائف الأب، وبدء إنهاء الهرمية من الطبقة الأدنى. ↩ ↩2
-
Microsoft Learn, Process Creation Flags. حول CREATE_SUSPENDED (توليد الخيط الأوّلي معلّقاً وعدم التنفيذ حتّى ResumeThread)، وCREATE_BREAKAWAY_FROM_JOB (يلزم JOB_OBJECT_LIMIT_BREAKAWAY_OK على Job المستدعي). ↩
-
Raymond Chen, Closing the race window between creating a suspended process and putting it in a job (The Old New Thing). حول الإجراء الكلاسيكي للتوليد بـ CREATE_SUSPENDED ثم الإدخال إلى Job، وطريقة إغلاق نافذة السباق تلك. ↩
-
Microsoft Learn, UpdateProcThreadAttribute function (processthreadsapi.h). حول إمكان تخصيص مقابض Job بالترتيب المحدَّد للعملية الابن المولَّدة بـ PROC_THREAD_ATTRIBUTE_JOB_LIST، والدعم من Windows 10 / Windows Server 2016 فما بعده. ↩
-
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. ↩
-
Raymond Chen, Destroying all child processes (and grandchildren) when the parent exits (The Old New Thing). حول تكوين إنهاء الذرّية مجتمعة عند اختفاء الأب بـ Job مع KILL_ON_JOB_CLOSE، وأهمّية عدم توريث مقبض Job. ↩
-
Microsoft Learn, TerminateJobObject function (jobapi2.h). حول إنهاء كلّ العمليات المرتبطة بالـ Job قسراً كما باستدعاء TerminateProcess لكلّ منها. ↩
-
Microsoft Learn, Job Objects - Managing Job Objects. حول صيرورة كائن Job حالة إشارة عندما تنتهي كلّ العمليات بتجاوز حدّ زمن الوظيفة، وإتلاف الـ Job عندما يُغلق آخر مقبض، وتسبيب الإغلاق إنهاء كلّ العمليات المنتمية عند تحديد KILL_ON_JOB_CLOSE. ↩
-
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
-
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 لمنفذ الإكمال. ↩
-
Microsoft Learn, Job Objects - Job Limits and Notifications. حول أفضلية ربط منفذ الإكمال بينما الـ Job غير نشط (تقليل إمكان تفويت إشعار عملية تغيّرت حالتها أثناء الربط)، وعدم ضمان تسليم الرسائل عدا الحدّ المضبوط بـ JobObjectNotificationLimitInformation، ومواصلة العملية الجري بعد التجاوز في حدّ الإشعار. ↩ ↩2 ↩3
-
Raymond Chen, Removing the TerminateThread from code that waits for a job object to empty (The Old New Thing). حول طريقة إعادة كتابة النمط القديم لقتل خيط الانتظار بـ TerminateThread إلى انتظار قائم على منفذ إكمال. ↩
-
Microsoft Learn, JOBOBJECT_EXTENDED_LIMIT_INFORMATION structure (winnt.h). حول ضبط حدّ الذاكرة لكلّ عملية ولكلّ وظيفة، وجلب ذروة الذاكرة بـ PeakProcessMemoryUsed / PeakJobMemoryUsed. ↩ ↩2
-
Microsoft Learn, IsProcessInJob function (jobapi.h). حول إمكان الحكم ما إذا كانت العملية تعمل في الـ Job المحدَّد (أو أيّ Job). ↩ ↩2 ↩3
-
Microsoft Learn, JOBOBJECT_CPU_RATE_CONTROL_INFORMATION structure (winnt.h). حول إمكان التحكّم في معدّل CPU (نسبة الدورات أو الوزن) لكلّ Job. ↩
مقالات ذات صلة
أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.
لماذا تتعطّل الوسائط ── قواعد وسائط سطر أوامر Windows
في Windows لا توجد مصفوفة وسائط؛ يصل إلى CreateProcess سلسلة واحدة ويتولّى الطرف المستقبل تقسيمها. نشرح قواعد CommandLineToArgvW وCRT و.N...
الأنابيب المسمّاة عمليّاً ── IPC القياسيّ في Windows من التصميم إلى الأمان
دليل عمليّ للأنابيب المسمّاة، وسيلة IPC القياسيّة في Windows. يغطّي وضع البايت مقابل وضع الرسالة، وخوادم متعدّدة العملاء، وأمان ACL وانتح...
واجهة مجمع مؤشّرات الترابط Win32 ── تزامن دون إنشاء مؤشّرات ترابط، عبر CreateThreadpoolWork
أتستدعي CreateThread في كلّ مكان في الشيفرة الأصليّة؟ دليل من المصادر الأوّليّة لواجهة مجمع مؤشّرات الترابط Win32: كائنات work وtimer وwa...
DllMain وقفل المحمِّل ── السبب الحقيقي لعبارة «لا تفعل شيئاً في تهيئة الـDLL»
لماذا يُحظَر استدعاء LoadLibrary أو مزامنة مؤشّرات الترابط من DllMain. نشرح من المصادر الأوّليّة كيف يسلسل قفل المحمِّل كلّ إشعار DLL، وس...
الإيقاظات الزائفة ── لماذا يستيقظ متغيّر الشرط «دون إشعار» وكيف تنتظر بشكل صحيح على Windows
يمكن لانتظار متغيّر شرط أن يستيقظ بلا إشعار (إيقاظ زائف). لماذا يسمح Windows بذلك، والانتظار الصحيح بـ while ومحمول في Win32 وC++ وC#.
أين يتصل هذا الموضوع
ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.
تطوير تطبيقات ويندوز
ندعم تطوير برامج ويندوز للأعمال، وتكامل الأجهزة، وأدوات التواصل.
الأسئلة الشائعة
أسئلة شائعة حول موضوع هذه المقالة.
- هل كائن 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 في بيئة بلا تداخل. ومع ذلك نظام التشغيل منتهي الدعم، فالترحيل أولى إن أمكن.
الملف الشخصي للمؤلف
صفحة الملف الشخصي لمؤلف المقالة.
غو كومورا
مؤسّس شركة كومورا سوفت ذ.م.م.
يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.