SSD Nodes Learn 🎉 VPS من $5.50/شهر
الأدلة Matt Connorبقلم Matt Connor

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

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

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

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

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

ما الذي يقرره Type= فعلياً

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

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

تتحكم الإجابة الثانية في الإشراف. يضع systemd كل عملية تنشئها الوحدة داخل cgroup (مجموعة تحكم)، وهي ميزة في النواة تجمع العمليات لكي يمكن تقييدها وإنهاؤها معاً. يستخدم systemctl stop هذا الـcgroup لتنظيف العمليات: إذ تكون 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، اعتباراً من August 2026. تحقّق من الإصدار لديك باستخدام systemctl --version.

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

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

يُخبر Type=forking systemd بأن العملية المحددة في ExecStart= ستنشئ عملية فرعية ثم تنتهي عمداً. ينتظر systemd انتهاء العملية الأولى، ثم يعتبر الوحدة قيد التشغيل. أما العملية الفرعية المتبقية فهي الـdaemon.

تكمن الصعوبة في تحديد الهوية. فالعملية التي شغّلها 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 بأن الخدمة بدأت قبل أن تقرأ إعداداتها أو تفتح listening socket، لذلك قد تبدأ وحدة تعتمد عليها مبكراً جداً ويفشل اتصالها الأول. أما notify فيبلّغ بأن الخدمة بدأت في اللحظة التي تعلن فيها الخدمة نفسها أنها جاهزة.

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

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

Type=dbus وType=idle

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

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

لماذا يترك البرنامج النصي الملتف 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 الصدفة بصفتها PID الرئيسي. تبقى الصدفة قيد التشغيل ما دام exporter يعمل في الواجهة الأمامية. إذا توقف server، فلن تلاحظ الصدفة ذلك، ولذلك يظل PID الرئيسي قيد التشغيل، وتظل الوحدة في حالة active، ولا يملك Restart= ما يتصرف بناءً عليه. تبقى العمليتان داخل cgroup الخاصة بالوحدة طوال الوقت، ولذلك يواصل systemctl stop تنفيذ التنظيف بصورة صحيحة. الذي تعطل هو الإشراف، لا التنظيف.

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

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

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

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

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

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

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

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

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

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

يُستخدم cgroup أيضاً لتسجيل استهلاك الموارد، لذلك تنطبق حدود مثل MemoryMax= وCPUQuota= على كل process أنشأته الوحدة، بصرف النظر عما يقوله Type= عن main 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.

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

FAQ

لماذا تبقى وحدة systemd في الحالة النشطة بعد توقف العملية؟

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

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

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

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

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

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

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

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

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