SSD Nodes Learn Hosting plans →
الأدلة Matt Connorبقلم Matt Connor · آخر تحديث في 2026-08-23

شرح Type= في systemd: simple وforking وnotify

تظهر الوحدة active رغم اختفاء الخادم؟ تعرّف إلى الفرق بين simple وexec وforking وoneshot وnotify، وحدد العملية الرئيسية وPID الحقيقي بدقة.

لماذا يعرض systemd أن الوحدة نشطة رغم توقف العملية

تبقى وحدة خدمة systemd في الحالة active ما دامت العملية الوحيدة التي يسمّيها systemd العملية الرئيسية قيد التشغيل، وتحدّد Type= في القسم [Service] العملية المقصودة. إذا اخترت قيمة غير صحيحة، فقد يراقب systemd غلافاً برمجياً أو عمليةً أصلية قصيرة العمر، بينما تتوقف الخدمة التي تهمك داخل الوحدة نفسها. تعكس الوحدة حالة العملية التي طُلب من systemd مراقبتها بدقة.

لن تساعدك ملاحظة إعادة التشغيل هنا. تعمل Restart= عند خروج العملية الرئيسية، لذلك لا تُنفّذ Restart=always ما دام PID الرئيسي، أي معرّف العملية، يعود إلى عملية لا تزال قيد التشغيل. أصلح Type= أولاً. وما يفعله systemd بعد خروج العملية الرئيسية فعلياً قرار منفصل، وتغطيه دليل Restart= وRestartSec=.

ما الذي يحدده Type= فعلياً

تجيب كل قيمة من قيم Type= عن سؤالين في الوقت نفسه: متى يجوز لـ systemd اعتبار هذه الوحدة قد بدأت، وما العملية التي تُعد العملية الرئيسية.

يتحكم الجواب الأول في ترتيب التشغيل. تنتظر الوحدة التي تذكر وحدتك في After= إلى أن يستدعي systemd وحدتك ويعتبرها قد بدأت. أما Type= التي تُبلغ عن «بدء التشغيل» في وقت مبكر جداً، فتسمح للوحدات التابعة بالعمل قبل أن تتمكن خدمتك من الاستجابة لها.

يتحكم الجواب الثاني في الإشراف. يضع systemd كل عملية تنشئها الوحدة في cgroup (مجموعة تحكم)، وهي ميزة في النواة تجمع العمليات بحيث يمكن تقييدها وإنهاؤها معاً. يستخدم systemctl stop هذه المجموعة لتنظيف العمليات: تكون KillMode= مضبوطة افتراضياً على control-group، لذلك يؤدي إيقاف الوحدة إلى إرسال إشارة إلى كل عملية داخلها. أما PID الرئيسي فنطاقه أضيق. فهو العملية الوحيدة التي يؤدي خروجها إلى إنهاء الوحدة، وتصبح حالة خروجها نتيجة الوحدة. يبدأ الالتباس عند التعامل مع cgroup كما لو كانت PID الرئيسية.

يشير Type=simple إلى بدء الخدمة قبل تشغيل الملف التنفيذي

Type=simple هو الإعداد الافتراضي عند تعيين ExecStart= وعدم وجود Type= أو BusName=. ينشئ systemd العملية، ويعتبر الوحدة قد بدأت فوراً، ويتعامل مع تلك العملية باعتبارها PID الرئيسي. لذلك تبدأ الوحدات التابعة مباشرة، قبل تنفيذ الملف الثنائي للخدمة.

تفسّر هذه النقطة الأخيرة مفاجأة شائعة. يؤدي الخطأ الإملائي في مسار ExecStart= إلى إنشاء مهمة بدء ناجحة، ثم يظهر الفشل بعد لحظة عند تعذّر التنفيذ. يسجّل systemd هذه الحالة برمز الخروج 203، وتسمّيها قائمته الرسمية EXEC وتعرّفها بأنها تعذّر تنفيذ الملف الثنائي للخدمة. لذلك لا يثبت عودة systemctl start دون خطأ أن الملف الثنائي موجود.

استخدم simple للبرنامج الذي يبقى في الواجهة ولا ينقل نفسه إلى الخلفية. ينطبق ذلك على معظم الخدمات الحديثة وعلى أي برنامج تكتبه بنفسك تقريباً.

ينتظر Type=exec بدء البرنامج فعلياً

Type=exec هو simple مع خطوة إضافية. لا يعتبر systemd الوحدة قد بدأت إلا بعد نجاح كل من عملية fork وتنفيذ الملف الثنائي. يؤدي غياب الملف الثنائي أو تعذّر حل User= إلى فشل مهمة البدء نفسها، بدلاً من الإبلاغ عن نجاح ثم الفشل بصمت بعد لحظات.

أُضيف Type=exec إلى systemd في الإصدار 240، لذلك يتوفر في كل توزيعة خوادم حالية. تأتي Ubuntu 24.04 مع systemd 255، وتأتي Debian 13 مع systemd 257، حتى أغسطس 2026. تحقّق من إصدارك باستخدام systemctl --version.

التكلفة هي خطوة مزامنة إضافية عند البدء. والفائدة هي الحصول على حالة خروج فعلية من systemctl start. بالنسبة إلى البرنامج الذي يعمل في المقدمة، استخدم exec بدلاً من simple.

Type=forking، وكيفية فقدان معرّف العملية الرئيسية

تُخبر Type=forking systemd بأن العملية المحددة في ExecStart= ستنشئ عملية فرعية ثم تنتهي عمداً. ينتظر systemd انتهاء العملية الأولى، وبعد ذلك فقط يعتبر الوحدة قد بدأت. العملية الفرعية المتبقية هي daemon. يعود هذا الأسلوب إلى عصر SysV، عندما لم تكن هناك جهة تشرف على daemon بعد عودة script الإقلاع، وكان ملف PID هو السجل الوحيد للعملية قيد التشغيل. وتُعد هذه المحدودية من الأسباب الأساسية التي تفسّر استبدال systemd لـ init scripts.

تكمن الصعوبة في تحديد الهوية. العملية التي شغّلها systemd انتهت، لذلك يجب على systemd تحديد العملية الناجية التي تمثل العملية الرئيسية. اضبط PIDFile= على الملف الذي يكتبه daemon، ويكون عادةً مساراً تحت /run، وسيقرأ systemd معرّف PID منه. يتحقق systemd أيضاً من أن PID الموجود في الملف يشير إلى عملية تنتمي فعلاً إلى هذه الخدمة، لذلك يرفض ملفاً قديماً يشير إلى عملية غير مرتبطة بدلاً من الوثوق به.

من دون PIDFile=، يُطبَّق GuessMainPID=، وتكون قيمته الافتراضية yes. لا يكون هذا التخمين موثوقاً إلا عندما تستقر الخدمة في عملية واحدة. يوضح الدليل الحد بوضوح: إذا كان daemon يتكوّن من أكثر من عملية، فقد يكون التخمين خاطئاً، ويتوقف اكتشاف الفشل عن العمل. وقد تنتهي الوحدة أيضاً بمعرّف PID رئيسي قيمته 0، ما يعني أن systemd لا يملك أي عملية للإشراف عليها.

تحتوي معظم daemons التي تنشئ عمليات فرعية أيضاً على خيار يُبقيها في المقدمة. استخدم هذا الخيار مع Type=exec واحذف السطر PIDFile=. تقليل العناصر المتحركة يعني تقليل طرق فقدان PID.

Type=oneshot للعمل الذي ينتهي

يتوقع Type=oneshot أن تبدأ العملية ثم تنتهي. لا يعيّن systemd الوحدة على أنها بدأت إلا بعد انتهاء العملية، ما يجعل oneshot النوع المناسب لأي شيء يجب على وحدة أخرى انتظاره. وهو أيضاً النوع الافتراضي الضمني عندما لا تحدد الوحدة Type= ولا ExecStart=.

يرتبط سلوكان تحديداً بـ oneshot. فهو النوع الوحيد الذي يقبل أكثر من سطر ExecStart= واحد، وتُنفَّذ هذه الأسطر بالترتيب. كما تكون مهلة البدء معطَّلة افتراضياً، لذلك ينتظر oneshot الذي يتوقف عن الاستجابة إلى أجل غير مسمى، ما لم تضبط TimeoutStartSec= بنفسك.

بعد انتهاء العملية، تعود الوحدة إلى الحالة غير النشطة. يحافظ RemainAfterExit=yes عليها في الحالة active من دون تشغيل أي عملية على الإطلاق. هذه هي النسخة المقصودة من السلوك المذكور في أعلى هذه الصفحة، وهي صحيحة عندما تكون مهمة الوحدة ترك حالة مستمرة بدلاً من إبقاء شيء قيد التشغيل، مثل تحميل مجموعة قواعد جدار ناري أو تشغيل مجموعة حاويات. وهذا هو النمط الذي تعتمد عليه مجموعة Docker Compose التي تعود بعد إعادة التشغيل، حيث تنفّذ الوحدة أمر compose، ثم تنتهي، وتبقى نشطة لأن الحاويات التي شغّلتها تستمر في العمل بعدها. كما أن وحدة oneshot هي ما يشغّله جدول زمني، وهو النصف الآخر من تشغيل مهمة باستخدام مؤقت systemd بدلاً من cron.

Type=notify يتيح للخدمة الإبلاغ عن جاهزيتها

Type=notify ينقل قرار الجاهزية إلى الخدمة. يُبقي systemd مهمة البدء مفتوحة إلى أن ترسل العملية READY=1 عبر Unix socket، الذي تتلقى مساره في متغير البيئة NOTIFY_SOCKET. وواجهة C هي sd_notify(3)، وتدعمها خوادم كثيرة بالفعل.

هذه هي الإجابة الدقيقة عن السؤال: «هل بدأت الخدمة؟». تُبلغ simple وexec بأن الخدمة بدأت قبل أن تقرأ إعداداتها أو تفتح socket الاستماع، لذلك قد تبدأ وحدة تعتمد عليها مبكراً جداً ويفشل اتصالها الأول. أما notify فتُبلغ بأن الخدمة بدأت في اللحظة التي تعلن فيها الخدمة نفسها أنها جاهزة.

يقبل systemd هذه الرسالة من العملية الرئيسية فقط، وهذا ما يعنيه NotifyAccess=main، وهو ما يتضمنه Type=notify. إذا جاءت الرسالة من عملية فرعية أو من عملية مساعدة، فاضبط NotifyAccess=all. يمكن لبرنامج shell النصي استدعاء systemd-notify --ready، لكن ذلك يعمل كعملية قصيرة العمر ومستقلة، ولذلك يحتاج إلى NotifyAccess=all، وقد لا يتمكن systemd من ربط الرسالة بمرسل انتهى بالفعل. وتكون الخدمة التي تتحدث بالبروتوكول بنفسها أكثر موثوقية.

هناك إعدادان مرتبطان يستحقان المعرفة. يوسّع Type=notify-reload، المتاح منذ systemd 253، المصافحة نفسها لتشمل عمليات إعادة التحميل، لذلك يعيد systemctl reload النتيجة عندما تُبلغ الخدمة بانتهاء إعادة التحميل، بدلاً من إعادتها عند إرسال الإشارة. ويطلب WatchdogSec= من الخدمة التي ترسل إشعارات أن ترسل رسالة إبقاء على قيد التشغيل وفق فاصل زمني، ويتعامل systemd مع تجاوز الموعد المحدد على أنه فشل.

Type=dbus وType=idle

ينتظر Type=dbus إلى أن تسجّل الخدمة اسماً على D-Bus، وهي حافلة الرسائل التي تستخدمها خدمات النظام وسطح المكتب للتواصل فيما بينها. ويتطلب ذلك BusName=، ويصبح النوع الافتراضي فور تعيين BusName=. استخدمه فقط لخدمة تسجّل اسماً على الحافلة فعلاً.

يتصرف Type=idle مثل simple، لكنه يؤخر تشغيل البرنامج إلى أن تُرسَل المهام الموضوعة في قائمة الانتظار، بحد أقصى قدره خمس ثوانٍ. وُجد هذا النوع لمنع تداخل مخرجات وحدة التحكم أثناء الإقلاع مع رسائل الحالة. وهو ليس أداة لترتيب التشغيل، ولا ينبغي استخدامه مع خدمة عادية.

لماذا يتركك wrapper script مع systemd وهو يتابع PID غير الصحيح

إليك البنية التي تُنتج العَرَض الأصلي.

[Service]
Type=simple
ExecStart=/opt/app/run.sh
#!/bin/bash
export APP_ENV=production
/opt/app/bin/server --config /etc/app.yaml &
/opt/app/bin/exporter --port 9101

يسجّل systemd الـshell بوصفه PID الرئيسي. يبقى الـshell قيد التشغيل ما دام exporter يعمل في الواجهة الأمامية. إذا توقّف server، فلن يلاحظ الـshell ذلك، ولذلك يظل PID الرئيسي قيد التشغيل، وتظل الوحدة في حالة active، ولا يجد Restart= شيئاً يتعامل معه. تبقى العمليتان في cgroup الخاصة بالوحدة طوال الوقت، ولذلك يواصل systemctl stop التنظيف بصورة صحيحة. الذي تعطل هو الإشراف، لا التنظيف.

يعتمد الإصلاح على عدد العمليات الطويلة التشغيل التي تملكها الوحدة فعلياً.

إذا كانت هناك عملية واحدة، فاستبدل الـshell بها.

#!/bin/bash
export APP_ENV=production
exec /opt/app/bin/server --config /etc/app.yaml

يستبدل exec الـshell بالبرنامج المحدد ويحافظ على PID نفسه، ولذلك يصبح PID الذي سجّله systemd تابعاً للـdaemon. والأفضل من ذلك إزالة الـwrapper. ينقل Environment= وEnvironmentFile= المتغيرات، وينفّذ ExecStartPre= خطوة الإعداد، وبذلك يستطيع systemd تشغيل الـdaemon مباشرة ومعرفة PID الخاص به بحكم البنية.

إذا كانت هناك عمليتان، فلن يمثّل أي PID واحد الوحدة. افصلهما إلى وحدتين، ورتّبهما باستخدام After= وWants=. تخصيص وحدة لكل عملية هو الترتيب الذي يشرف عليه systemd جيداً، وهو الطريقة الوحيدة التي تحصل بها كل عملية على سلوك إعادة تشغيل خاص بها.

ما الذي يغيّره ExitType=cgroup

أُضيف ExitType= في systemd 250. الإعداد الافتراضي هو main: تُعدّ الوحدة متوقفة عند خروج العملية الرئيسية. مع ExitType=cgroup، تُعدّ الوحدة قيد التشغيل ما دامت أي عملية في cgroup الخاص بها ما تزال حية.

[Service]
Type=simple
ExitType=cgroup
ExecStart=/opt/app/launcher

يحل هذا إعداداً محدداً واحداً. إذا شغّل launcher العمل الفعلي ثم خرج، فسيجعل systemd، في حالة ExitType=main، الوحدة متوقفة ويُنهي العمليات المتبقية. أما مع ExitType=cgroup، فتتبع الوحدة المجموعة بأكملها بدلاً من ذلك.

يجب أن تعرف ما الذي لا يحله هذا الإعداد. يحافظ ExitType=cgroup على بقاء الوحدة نشطة ما دامت عملية واحدة على الأقل حية، لذلك تظل الوحدة التي تستضيف daemonين نشطة بعد توقف أحدهما. إنه يحل حالة launcher. لكنه لا يحوّل وحدة واحدة إلى مشرف على عدة عمليات مستقلة. كذلك لا يمكن دمج ExitType= مع Type=oneshot.

يُستخدم cgroup أيضاً للمحاسبة على الموارد، لذلك تنطبق حدود مثل MemoryMax= وCPUQuota= على كل عملية أنشأتها الوحدة، بصرف النظر عما يقوله Type= عن PID الرئيسي. يتناول تقييد ذاكرة الخدمة ووحدة المعالجة المركزية باستخدام systemd هذا الجانب.

كيفية معرفة العملية التي يراقبها systemd فعلياً

نفّذ هذه الخطوات على الوحدة التي تعمل على تصحيحها، بالترتيب. اقرأ ما حمّله systemd، ثم اقرأ ما يتتبعه، ثم قارن ذلك بجدول العمليات.

systemctl cat app.service
systemctl show -p Type,ExitType,GuessMainPID,PIDFile,MainPID,RemainAfterExit app.service

يطبع systemctl cat ملف الوحدة مع كل ملفات drop-in المنطبقة عليها، ولذلك تقرأ ما حمّله systemd بدلاً من الملف الذي تتذكر أنك عدّلته. يطبع systemctl show القيم الفعلية، بما في ذلك القيم الافتراضية التي لم تدوّنها. سجّل قيمة MainPID قبل المتابعة.

systemd-cgls --unit=app.service
ps -o pid,ppid,stat,etime,args -p "$(systemctl show -p MainPID --value app.service)"

يسرد systemd-cgls كل عملية في cgroup الخاصة بالوحدة. يصف السطر ps العملية الوحيدة التي يشرف عليها systemd. اقرأ السطرين معاً. تعني قيمة MainPID تساوي 0 أن systemd لا يملك عملية لمراقبتها. إذا أشارت MainPID إلى shell بينما تضم cgroup أيضاً الـdaemon، فهذه هي حالة الغلاف المذكورة أعلاه. إذا احتوت cgroup على عمليات أكثر مما توقعت، فهذا يعني وجود launcher أو daemon يعمل بالتفرع.

systemctl status app.service
journalctl -u app.service -b

يطبع systemctl status سطر الحالة وشجرة cgroup معاً، ولذلك يجيب غالباً عن السؤالين دفعة واحدة. يعرض journalctl -u، المقيّد بعملية الإقلاع الحالية باستخدام -b، أحداث البدء والإيقاف التي سجّلها systemd للوحدة، مع رموز الخروج التي رآها. إذا كتب الـdaemon في ملف سجل خاص به بدلاً من journal، فاقرأ ذلك الملف أيضاً، لأن systemd لا يستطيع تسجيل إلا ما يصل إليه.

عند تغيير Type=، أعد التحميل ثم أعد التشغيل.

systemd-analyze verify /etc/systemd/system/app.service
sudo systemctl daemon-reload
sudo systemctl restart app.service

يفحص systemd-analyze verify الملف ويبلّغ عن الإعدادات التي لا يستطيع قبولها. يجعل daemon-reload systemd يعيد قراءة ملفات الوحدات من القرص. لا ينطبق تغيير Type= على وحدة قيد التشغيل، ولذلك فإن إعادة التشغيل مطلوبة وليست اختيارية.

اختبر التغيير بعد ذلك. خذ PID للعملية التي تهمك فعلياً من systemd-cgls وأنهِها. شغّل systemctl is-active app.service مباشرة بعد ذلك. إذا كان Type= صحيحاً، تغادر الوحدة الحالة النشطة. وإذا بقيت نشطة، فهذا يعني أن systemd ما زال يراقب شيئاً آخر.

ما Type= في خدمة systemd الذي ينبغي أن تستخدمه

  • برنامج يعمل في الواجهة الأمامية: Type=exec.
  • برنامج يدعم إشعار الجاهزية: Type=notify، واستخدم notify-reload أيضاً إذا كان يؤكد عمليات إعادة التحميل.
  • خدمة daemon تُصرّ على الانتقال إلى الخلفية: Type=forking مع PIDFile=، أو استخدم خيار تشغيلها في الواجهة الأمامية مع Type=exec.
  • برنامج نصي ينفّذ مهمة ثم يخرج: Type=oneshot، وأضف RemainAfterExit=yes عندما يكون الهدف ترك حالة مستمرة.
  • مشغّل يخرج بينما تواصل العمليات التابعة له العمل: Type=simple مع ExitType=cgroup.

إذا لم تكن متأكداً من Type= الذي تحتاج إليه خدمة daemon تابعة لجهة خارجية، فاقرأ ملف الوحدة المضمّن في الحزمة أولاً. يعرض تشغيل systemctl cat على وحدة وفّعتها التوزيعة قيمة Type= التي اختارها المشروع الأصلي، وقد اختبر هذا الاختيار عدد من الأشخاص يفوق عدد من اختبروا اختيارك.

FAQ

لماذا تبقى وحدة systemd في حالة active بعد انتهاء العملية؟

لأن العملية التي يتعامل معها systemd على أنّها العملية الرئيسية لا تزال حية. يراقب systemd معرّف PID واحداً لكل خدمة، ويختاره وفقاً لـ Type=، بدلاً من مراقبة كل عملية في cgroup الخاصة بالوحدة. السبب المعتاد هو تشغيل برنامج تغليف نصي باستخدام Type=simple: تكون الصدفة هي PID الرئيسي، لذلك تبقى الوحدة في حالة active عندما تنتهي خدمة بدأتها الصدفة في الخلفية. شغّل systemctl show -p MainPID app.service، ثم اعرض cgroup الخاصة بالوحدة باستخدام systemd-cgls --unit=app.service، وقارن بين النتيجتين.

ما الفرق بين Type=simple وType=exec؟

يعتبر Type=simple الوحدة قد بدأت فور إنشاء systemd للعملية، وقبل تنفيذ الملف الثنائي. لذلك يؤدي تحديد مسار خاطئ في ExecStart= إلى نجاح مهمة البدء، ثم إلى فشل لاحق. ينتظر Type=exec نجاح التنفيذ، ولذلك تبلغ مهمة البدء عن الفشل نفسها. يتعامل كلا النوعين مع العملية نفسها باعتبارها PID الرئيسي. يحتاج Type=exec إلى systemd 240 أو إصدار أحدث.

هل ما زلت بحاجة إلى PIDFile= مع Type=forking؟

نعم، كلما كانت الخدمة تكتب هذا الملف. من دونه، يعود systemd إلى GuessMainPID=، وهو تخمين لا يكون موثوقاً إلا لخدمة تستقر في عملية واحدة. عندما يكون التخمين خاطئاً أو يتعذر إجراؤه، يتوقف اكتشاف الفشل وإعادة التشغيل التلقائية لهذه الوحدة. وجّه PIDFile= إلى المسار الدقيق الذي تكتب فيه الخدمة الملف، ويكون عادةً ضمن /run.

متى ينبغي أن أستخدم RemainAfterExit=yes؟

استخدمه عندما يكون الغرض من الوحدة تغيير حالة النظام، لا إبقاء عملية قيد التشغيل. تنتهي وحدة Type=oneshot التي تحمّل قواعد الجدار الناري أو تبدأ مجموعة حاويات فور اكتمال عملها. ومن دون RemainAfterExit=yes، تنتقل الوحدة إلى الحالة inactive، فلا يبقى لدى systemctl stop شيء لإيقافه، ولا توجد طريقة لتشغيل عملية تنظيف باستخدام ExecStop=. عند استخدامه، تبقى الوحدة في الحالة active من دون عمليات، وهذا هو السلوك المقصود هنا.

هل يتطلب تغيير Type= تنفيذ daemon-reload؟

نعم، ويتطلب أيضاً إعادة تشغيل الوحدة. يجعل systemctl daemon-reload systemd يعيد قراءة ملفات الوحدات من القرص، لكن المثيل قيد التشغيل يحتفظ بـ Type= الذي بدأ به. شغّل sudo systemctl daemon-reload ثم sudo systemctl restart app.service قبل الاختبار، وإلا فستظل تراقب سلوك الإشراف القديم.