تجاوز إلى المحتوى
← المدونة

systemd 260 أزال SysV. ماذا الآن؟

أزال الإصدار systemd 260 كلاً من sysv-generator وrc-local.service وsystemd-sysv-install. كل سكربت في /etc/init.d وكل ملف /etc/rc.local على خوادمك صار مربوطاً بعدّاد تنازلي. هذا دليل عملي لحصرها جميعاً وتحويلها إلى ملفات unit أصلية بالشكل الصحيح.

·قراءة 18 دقيقة
  • systemd
  • Linux
  • إدارة الأنظمة
  • ترحيل

طوال خمسة عشر عاماً تقريباً، كان systemd يترجم سكربتاتك في /etc/init.d إلى خدمات في كل إقلاع، بصمت، دون أن يطلب منك الانتباه إلى ذلك. وفي الإصدار 260، الصادر في مارس 2026، توقّف. حُذف systemd-sysv-generator، وحُذف systemd-rc-local-generator ومعه rc-local.service، وحُذف كذلك الخطاف systemd-sysv-install الذي كان يقف خلف systemctl enable — إزالة كاملة لا تعطيلاً. لم يتغيّر شيء في سكربتاتك. الذي تغيّر أن أحداً لم يعد يقرأها.

غلاف المقال بلوحتين: على اليسار لوحة عنوانها DELETED IN systemd 260 تعدّد systemd-sysv-generator وsystemd-rc-local-generator وrc-local.service وsystemd-sysv-install؛ وعلى اليمين لوحة عنوانها WHAT REPLACES WHAT فيها أربعة أزواج قبل/بعد: start-stop-daemon --background يصير Type=exec، و--make-pidfile يصير بلا ملف PID إطلاقاً، و--chuid acme يصير User=acme، و/etc/rc[2-5].d/S20name يصير WantedBy=multi-user.target.
ثلاثة مكوّنات أُزيلت في إصدار واحد. الترحيل نفسه ليس صعباً — الصعب أن تعرف أي السكربتات ما زالت لديك، وعلى أي أجهزة، قبل أن تخبرك بذلك ترقية تلقائية غير مراقَبة.

هذا هو الترحيل، مكتوباً لمن عليه تنفيذه على خوادم حقيقية لا لمن يتأمله من بعيد: كيف تحصر كل سكربت ملفوف عبر أسطول كامل باستخدام حقل في الـ journal لا يكاد أحد يعرف بوجوده، وماذا كان المولّد يكتب بالضبط حتى تنسخه بدل أن تخمّنه، وتحويل حقلاً بحقل لسكربت init واقعي، وخطأ Type= الذي يجعل الـ unit تُبلّغ بالنجاح بينما الديمون ميت، والأوامر التي حلّت محل init 3. كل رقم إصدار وكل توجيه هنا جرى التحقق منه في ملف NEWS الخاص بـ systemd وفي الشيفرة المصدرية عند الوسوم التي كان الكود المحذوف لا يزال موجوداً فيها، لا من الذاكرة.

الخدمة ليست failed. هي غير موجودة أصلاً

العَرَض مربك على نحو غير معتاد، لأنه غياب لا خطأ. خدمتك لا تظهر بحالة failed. هي لا تظهر إطلاقاً. يخبرك systemctl status أن الـ unit غير موجودة — Unit could not be found — بينما السكربت قابع على القرص، قابل للتنفيذ، ولم يُمَسّ منذ 2015. وهذا سلوك صحيح: السكربت لم يكن unit في أي يوم. كان مولّد يصنع واحدة في /run/systemd/generator.late عند كل إقلاع، وهذا المسار صار فارغاً الآن.[v260]

ما تراهما يعنيهأين يُعالَج
Unit x.service could not be found، بينما /etc/init.d/x موجود وقابل للتنفيذالمولّد الذي كان يخترع تلك الوحدة أُزيل. لا شيء معطوب؛ لا شيء يترجم بعد الآن.ما كان المولّد يكتبه
خدمة كانت تبدأ في كل إقلاع لسنوات لا تبدأ ببساطة، ولا يظهر شيء في الـ journalالسبب نفسه، منظوراً إليه من جهة الإقلاع لا من سطر الأوامر. لا توجد unit فاشلة لأنه لا توجد unit أصلاً.تحويل سكربت واحد
/etc/rc.local ما زال قابلاً للتنفيذ ولم يعد يعملأُزيل systemd-rc-local-generator وrc-local.service مع مولّد SysV في الدفعة نفسها.استبدال /etc/rc.local
systemctl enable على خدمة قديمة لم يعد يفعل شيئاً مفيداًsystemd-sysv-install، الخطاف الذي كان يمرّر enable/disable إلى chkconfig أو update-rc.d، ذهب.ما يجب أن تشحنه الحزمة
telinit: command not found، أو أن init 3 لا يفعل شيئاًأُزيل في 258، أي قبل ذلك بإصدارين ولسبب مختلف: مفهوم مستويات التشغيل أُسقط، لا السكربتات فحسب.init 3 وtelinit وrunlevel
وحدة محوَّلة تُبلّغ بأنها active بينما العملية اختفت — أو تُبلّغ بأنها failed والديمون يعملقيمة Type= خاطئة لديمون يتفرّع. هذا أشيع تحويل مكسور على الإطلاق.Type=forking وملفات PID
# After the upgrade, the service that has started at boot for eleven years
# is simply not there. Not failed - not there.

systemctl status acme-collector
# Unit acme-collector.service could not be found.

ls -l /etc/init.d/acme-collector
# -rwxr-xr-x 1 root root 1284 Mar 14  2015 /etc/init.d/acme-collector
#            ^ the script is still on disk, still executable, still correct.

# The script was never a service. A generator turned it into one on every
# boot, and the generator is gone. Confirm which systemd you are on:
systemctl --version | head -1
# systemd 260 (260.2-1)

# And confirm the generator really is absent rather than just failing:
ls /usr/lib/systemd/system-generators/ | grep -E 'sysv|rc-local'
# (no output on 260 and later; on 259 and earlier you get one or two lines)

# The same disappearance, seen from the other end - nothing was generated:
ls /run/systemd/generator.late/
# (empty, or missing your unit)

المولّدات تعمل قبل أن يحمّل المدير أي unit، في كل إقلاع وفي كل daemon-reload، وناتجها يعيش في tmpfs. لهذا لا يبدو شيء على القرص مختلفاً بعد الترقية، ولهذا لا يوجد ما تُصلحه: ليس هناك ملف تالف في أي مكان، بل خطوة ترجمة لم تعد تحدث. كل ما يلي يفترض أن لديك وصولاً عبر الشِل إلى الجهاز وقدرة على إعادة تشغيله مرة واحدة في النهاية. لا شيء في هذا المقال يغيّر حالة النظام قبل أن تقرّر أنت ذلك؛ أوامر الحصر تقرأ فقط.[generator]

ما الذي أُزيل، ومتى بالضبط

ثلاثة إصدارات منفصلة معنيّة بالأمر، والخلط بينها هو سبب كون كثير من النصائح المتداولة في هذا الموضوع خاطئة. سكربتات الخدمات نفسها أُعلن إهمالها (deprecation) منذ الإصدار 255. أما الإصدار 258 في سبتمبر 2025 فقد أزال شيئاً آخر: واجهات حالة النظام الموروثة من System V — /dev/initctl، وأوامر initctl وrunlevel وtelinit، والتحكم في الحالة عبر init 3، ووحدات runlevel[0-6].target، وتسجيل انتقالات مستويات التشغيل في utmp وwtmp. ثم تراجع الإصدار 259 جزئياً عن واحدة منها. أما الإصدار 260 فأزال سكربتات الخدمات نفسها.[news][v255][v258]

الإصدارالتاريخما الذي حدث
258سبتمبر 2025تجديد الإشارة إلى إهمال سكربتات خدمات System V — وهو إهمال أُعلن أصلاً في 255 — مع بقاء الإزالة مجدولة في 259 في ملاحظات هذا الإصدار، قبل أن تُؤجَّل لاحقاً إلى 260. وبشكل منفصل وفوري: أُزيل /dev/initctl، وأوامر initctl وrunlevel وtelinit، وتغييرات الحالة بأسلوب init 3، ووحدات runlevel[0-6].target، وتوقف تسجيل انتقالات مستويات التشغيل في utmp/wtmp. وأُسقط دعم cgroup v1 في الإصدار نفسه.
259ديسمبر 2025أُعيدت runlevel[0-6].target، لكن فقط حين تبني التوزيعة بخيار -Dcompat-sysv-interfaces=yes. والأوامر المحذوفة لم تعد. وهذا هو الإصدار الذي تشحنه Ubuntu 26.04 LTS.
260مارس 2026أُزيل دعم سكربتات خدمات System V. المحذوف: systemd-sysv-generator؛ وsystemd-rc-local-generator وrc-local.service؛ وsystemd-sysv-install. وأصبحت خيارات البناء -Drc-local= و-Dsysvinit-path= و-Dsysvrcnd-path= مهملة. وارتفع الحد الأدنى للنواة إلى 5.10، وglibc إلى 2.34، وOpenSSL إلى 3.0.
261 وما بعده2026لا تغييرات إضافية متعلقة بـ SysV؛ طبقة التوافق ببساطة غائبة. ويُتوقع أن تختفي خيارات البناء المهملة في إصدار لاحق.

التراجع الجزئي في 259 يهمّك إن كنت تدير أسطولاً مختلطاً: عادت وحدات runlevel[0-6].target، لكن بوصفها خياراً يجب على التوزيعة تفعيله وقت البناء فقط، ولم تعد الأوامر إطلاقاً. أي أن systemctl isolate runlevel3.target قد ينجح على أحد أجهزتك ويفشل على الذي يليه، تبعاً لكيفية تصريف كل توزيعة لحزمتها. لا تبنِ عادات عمل على ذلك. أما موقع خوادمك فعلياً فمسألة تحدّدها نسخة systemd التي تشحنها كل توزيعة:[v259][v260]

أين أنتsystemdسكربتات SysV وrc.local
Debian 12 bookworm252كلاهما يعمل. ولا تحذير إهمال لكل سكربت في الـ journal.
Debian 13 trixie257كلاهما يعمل، والـ journal يحذّر بالفعل لكل سكربت ملفوف.
Ubuntu 24.04 LTS255كلاهما يعمل، والـ journal يحذّر بالفعل لكل سكربت ملفوف.
Ubuntu 26.04 LTS259كلاهما يعمل، والـ journal يحذّر لكل سكربت ملفوف كما يفعل منذ 24.04. وتنص Canonical على أن هذا آخر إصدار Ubuntu بتوافقية SysV.
Ubuntu 26.10260 أو أحدثأُزيل. السكربتات تبقى على القرص ولا تُقرأ أبداً.
RHEL 9 / RHEL 10252 / 257كلاهما يعمل طوال عمر الإصدار: على RHEL 9 بصمت، وعلى RHEL 10 مع تحذير في الـ journal لكل سكربت ملفوف، وملاحظات إصدار RHEL 10 تحمل الإهمال سلفاً. التغيير يصل مع النسخة الكبرى القادمة.
التوزيعات المتدحرجة260 أو أحدثأُزيل بالفعل. تحقّق بـ systemctl --version بدل الافتراض.

التواريخ تجعل الأمر ملموساً لا نظرياً. تنص ملاحظات الإصدار من Canonical نفسها على أن Ubuntu 26.04 LTS — التي تشحن systemd 259 — هي آخر إصدار Ubuntu يحمل توافقية System V، وأن التغيير يسري في 26.10، وموعدها المقرّر أكتوبر 2026. فإن كنت على السلاسل المرحلية (interim)، أو لديك أي شيء يتبع توزيعة متدحرجة، فالموعد النهائي يبعد أسابيع لا سنوات. وإن كنت على LTS أو على توزيعة مؤسسية، فأمامك حتى ترقيتك الكبرى القادمة — وهي بالضبط اللحظة التي لا تريد أن تكتشف فيها سكربتات شِل بلا مالك.[ubuntu2604][ubuntu2610][rhel10]

احصر كل سكربت قبل أن تكتشفه الترقية نيابة عنك

ابدأ بالحصر، ونفّذه بينما المولّد ما زال يعمل، لأن المولّد نفسه هو من سيعطيك الجواب. اعتباراً من الإصدار 255، في كل مرة يلفّ فيها systemd سكربتاً يُصدر تحذيراً مهيكلاً يحمل معرّف رسالة ثابتاً، إضافة إلى حقلين مخصّصين في الـ journal: SYSVSCRIPT= ويحمل المسار، وUNIT= ويحمل الاسم الذي اخترعه له. هذا يحوّل تدقيقاً على مستوى أسطول كامل إلى استعلام واحد دقيق، بدل تخمين مركّب من ls وحسن النية.[messages][v255][journalctl]

#!/usr/bin/env bash
# Inventory every SysV script this host still depends on. Run it BEFORE the
# upgrade, on every machine, and keep the output.

echo '== 1. scripts systemd is currently wrapping =========================='
# systemd 255 and later log a structured warning for every script they wrap.
# The message ID is stable, so this is an exact list rather than a guess.
journalctl -b -o json --output-fields=SYSVSCRIPT,UNIT \
  MESSAGE_ID=a8fa8dacdb1d443e9503b8be367a6adb 2>/dev/null \
  | python3 -c 'import sys,json
for l in sys.stdin:
    d = json.loads(l)
    print("%-28s %s" % (d.get("UNIT","?"), d.get("SYSVSCRIPT","?")))'

echo '== 2. fallback for systemd 254 and older ============================='
# Older systemd wraps silently. Ask the manager which units came from a
# generator instead: generated units live under /run/systemd/generator.late
# and carry a SourcePath= pointing back at the script. This also catches
# scripts that were wrapped before the current boot's journal starts.
systemctl list-units --type=service --all --no-legend --plain \
  | awk '{print $1}' \
  | while read -r u; do
      frag=$(systemctl show -p FragmentPath --value "$u" 2>/dev/null)
      case "$frag" in
        /run/systemd/generator*)
          src=$(systemctl show -p SourcePath --value "$u" 2>/dev/null)
          [ -n "$src" ] && printf '%-28s %s\n' "$u" "$src" ;;
      esac
    done

echo '== 3. scripts on disk with no NATIVE unit behind them ================'
# Note the test: `systemctl cat` succeeds for a generated unit too, so asking
# whether the unit exists tells you nothing while the generator is running.
# Ask where the definition lives instead.
for f in /etc/init.d/*; do
  [ -f "$f" ] && [ -x "$f" ] || continue
  n=$(basename "$f"); n=${n%.sh}
  frag=$(systemctl show -p FragmentPath --value "$n.service" 2>/dev/null)
  case "$frag" in
    /etc/systemd/system/*|/usr/lib/systemd/system/*|/lib/systemd/system/*) ;;
    *) echo "no native unit: $f" ;;
  esac
done

echo '== 4. runlevel wiring (this is what set the boot order) ============='
ls -l /etc/rc[1-5].d/S* 2>/dev/null | awk '{print $9, $10, $11}'

echo '== 5. rc.local ======================================================'
ls -l /etc/rc.local 2>/dev/null && \
  { [ -x /etc/rc.local ] && echo 'executable: it runs at boot today'; }

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

تفصيلان في ذلك الاستعلام يستحقان أن تستوعبهما جيداً. أولاً، systemd لم يلفّ يوماً إلا الملفات العادية القابلة للتنفيذ داخل مجلد init، فالسكربت المتروك بصلاحية 644 كان ميتاً أصلاً ولا ينبغي أن تبعثه. ثانياً، الـ unit الأصلية كانت تتغلب دائماً على سكربت يحمل الاسم نفسه: المولّد كان يتخطى صراحةً أي سكربت له unit حقيقية موجودة مسبقاً. هذه القاعدة الثانية هي ما يجعل الترحيل كله آمناً على مراحل — يمكنك تركيب الـ unit الجديدة والسكربت ما زال موجوداً وما زال مفعّلاً، وببساطة يتوقف الرجوع إلى السكربت. ولاحظ أخيراً أن الخطوة الأولى لا تقرأ إلا journal الإقلاع الحالي، فالمضيف الذي دُوّر تحذيره وخرج من السجل لن يظهر فيها إطلاقاً، ويبقى بحاجة إلى الخطوة الثانية.[gensrc]

ما كان المولّد يكتبه لك بصمت

قبل أن تكتب unit واحدة، خذ التي ينتجها المولّد واقرأها. ليست تقريباً خشناً؛ هي ترجمة حتمية، وهي أقرب ما ستحصل عليه من مواصفة رسمية لسلوك سكربتك عند الإقلاع. التقطها بـ systemctl cat ما دمت قادراً — بعد الترقية تختفي هذه المعلومة، وإعادة بنائها من ترويسة LSB هي المكان الذي يُدخل فيه الناس أخطاء الترتيب.[gensrc][genman]

# Capture the generated unit BEFORE you upgrade. It is the specification for
# the unit you are about to write, produced by the only thing that ever read
# your init script correctly.
systemctl cat acme-collector.service > ~/acme-collector.generated.service

# What comes out looks like this - and every line of it is decided by the
# generator source, not by convention:

# /run/systemd/generator.late/acme-collector.service
# Automatically generated by systemd-sysv-generator

[Unit]
Documentation=man:systemd-sysv-generator(8)
SourcePath=/etc/init.d/acme-collector
Description=LSB: ACME metrics collector
Before=multi-user.target
Before=multi-user.target
Before=multi-user.target
Before=graphical.target
After=remote-fs.target
After=network-online.target
After=time-sync.target
After=postgresql.service
Wants=network-online.target

[Service]
Type=forking
Restart=no
TimeoutSec=5min
IgnoreSIGPIPE=no
KillMode=process
GuessMainPID=no
RemainAfterExit=no
PIDFile=/var/run/acme-collector.pid
SuccessExitStatus=5 6
ExecStart=/etc/init.d/acme-collector start
ExecStop=/etc/init.d/acme-collector stop
ExecReload=/etc/init.d/acme-collector reload

# Yes, Before=multi-user.target really is repeated. The rc2.d, rc3.d and rc4.d
# symlinks each append it and nothing deduplicates the list. Harmless, but it
# tells you the file was machine-written and how.
#
# Note what is NOT there: no [Install] section. The generator wired the unit in
# by dropping symlinks next to it - into multi-user.target.wants for rc2-rc4,
# and into graphical.target.wants for rc5 - which is why `systemctl is-enabled`
# on one of these was never a straight answer.

والآن الجزء الذي يفاجئ معظم الناس، ويمكنك التحقق منه في مصدر المولّد نفسه. الحقلان Default-Start: وDefault-Stop: في ترويسة LSB لم يُقرآ قط. المولّد كان يحلّل فقط Provides: وRequired-Start: وShould-Start: وX-Start-Before: وX-Start-After:، وحقلَي الوصف، وتعليقات أسلوب Red Hat وهي # pidfile: و# description:. أما ربط مستويات التشغيل فكان يأتي بالكامل من روابط S??name في /etc/rc[1-5].d. فإن كنت تصون Default-Start منذ عقد، فأنت تصون تعليقاً.[lsb][exitstatus]

في سكربت initما الذي فعله المولّد بهاكتب هذا في وحدتك
Provides: nameإن سمّى خدمة، فرابط اسم بديل (alias). وإن سمّى $facility، فـ Before= مع Wants= على ذلك الهدف — في اتجاه واحد. وإن كرّر اسم الملف، فيُتجاهل.Alias= في [Install]، أو لا شيء
Required-Start: $networkAfter=network-online.target و Wants=network-online.target، لأن ذلك الهدف خامل ما لم يُسحبWants= + After=network-online.target
Required-Start: $remote_fsAfter=remote-fs.targetAfter=remote-fs.target
Required-Start: $namedAfter=nss-lookup.targetAfter=nss-lookup.target
Required-Start: $portmapAfter=rpcbind.targetAfter=rpcbind.target
Required-Start: $timeAfter=time-sync.targetAfter=time-sync.target
Required-Start: $local_fs أو $syslogلا شيء. أُسقط كلاهما: تحت systemd هما مستوفيان سلفاً قبل أن تعمل أي خدمة عادية.لا شيء
Should-Start: fooAfter=foo.service — ترتيب فقط، وليس اشتراطاً أبداًAfter=foo.service، دون Requires=
X-Start-Before: fooBefore=foo.serviceBefore=foo.service
Default-Start: / Default-Stop:لا شيء إطلاقاً. لم يُحلَّل قط. ربط مستويات التشغيل كان يأتي من روابط S?? في /etc/rc[1-5].d.WantedBy=multi-user.target في [Install]
رابط في /etc/rc2.d أو rc3.d أو rc4.dBefore=multi-user.target ورابط wants داخلهWantedBy=multi-user.target
رابط في /etc/rc5.dBefore=graphical.target ورابط wants داخلهWantedBy=graphical.target
رابط في /etc/rc1.dBefore=rescue.target مع رابط wants إليه — يعامله المولّد تماماً كما يعامل rc2–rc5نادراً جداً ما يكون هذا ما تريده؛ اتركه
# pidfile: /path (أسلوب Red Hat)PIDFile=/path مع RemainAfterExit=no. وبغياب سطر pidfile نهائياً: RemainAfterExit=yesاحذف كليهما. استخدم Type=exec بدلاً منهما
وجود ### BEGIN INIT INFOSuccessExitStatus=5 6 — رمزا LSB لـ «غير مركّب» و«غير مهيّأ»فقط إن كنت ما زلت تخرج بهذين الرمزين
سطر استعمال يحتوي |reload} أو ما يشبههExecReload=/etc/init.d/x reload. وبلا سطر استعمال، لا فعل reload.ExecReload=/bin/kill -HUP $MAINPID

خريطتان في ذلك الجدول تستحقان نظرة ثانية، لأنهما المصدر المعتاد لجملة «كان يعمل تحت SysV ويتسابق الآن». الرمز $network لم يصبح network.target؛ صار network-online.target، وأضاف المولّد After= وWants= معاً لأن ذلك الهدف لا يفعل شيئاً ما لم يسحبه أحد إلى الداخل فعلياً. في المقابل، $local_fs و$syslog لم يُخطّطا إلى أي شيء — أُسقطا، لسبب وجيه: تحت systemd كلاهما مضمون سلفاً قبل أن تبدأ أي خدمة عادية. انسخ هذه القرارات إلى unit الخاصة بك؛ ولا ترتجل قرارات جديدة.[special]

تحويل سكربت واحد، حقلاً بحقل

إليك سكربتاً واقعياً — لا نموذجاً تعليمياً. فيه ترويسة LSB، وتعليق ملف PID بأسلوب Red Hat، واستدعاء start-stop-daemon يدفع العملية إلى الخلفية ويكتب ملف الـ PID نيابة عن الديمون، وفعل reload، وسطر الاستعمال الذي هو السبب الوحيد لأن المولّد أصدر ExecReload= من الأساس. معظم السكربتات الحقيقية في الميدان أسوأ من هذا.

#!/bin/sh
### BEGIN INIT INFO
# Provides:          acme-collector
# Required-Start:    $remote_fs $network $time
# Required-Stop:     $remote_fs $network
# Should-Start:      postgresql
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: ACME metrics collector
### END INIT INFO
# pidfile: /var/run/acme-collector.pid

DAEMON=/opt/acme/bin/collector
PIDFILE=/var/run/acme-collector.pid
RUNAS=acme
OPTS="--config /etc/acme/collector.conf"

case "$1" in
  start)
    start-stop-daemon --start --quiet --background --make-pidfile \
        --pidfile "$PIDFILE" --chuid "$RUNAS" --exec "$DAEMON" -- $OPTS
    ;;
  stop)
    start-stop-daemon --stop --quiet --pidfile "$PIDFILE" --retry 30
    rm -f "$PIDFILE"
    ;;
  reload)
    kill -HUP "$(cat "$PIDFILE")"
    ;;
  restart)
    "$0" stop; sleep 1; "$0" start
    ;;
  *)
    echo "Usage: $0 {start|stop|restart|reload}" >&2
    exit 2
    ;;
esac
exit 0

وإليك ما يحل محله. اقرأ التعليقات لا التوجيهات: القرارات المثيرة للاهتمام هي تلك المتعلقة بما لا يجب ترحيله. التفرّع (forking) يذهب. ملف الـ PID اليدوي يذهب. وغلاف start-stop-daemon يذهب، لأن كل ما كان يفعله — إسقاط الامتيازات، والدفع إلى الخلفية، وتتبّع العملية — صار توجيهاً واحداً الآن.[service][exec]

# /etc/systemd/system/acme-collector.service
#
# Everything above the blank line inside [Service] is the translation of the
# init script. Everything below it is what the wrapper could never give you.

[Unit]
Description=ACME metrics collector
Documentation=https://example.internal/acme/collector
# $network in the old header became network-online.target - and that target
# only does something if a unit actively pulls it in, hence the Wants=.
Wants=network-online.target
After=network-online.target
# "Should-Start: postgresql" was ordering only, never a requirement. Keep it
# that way: After= without Requires= means a database outage does not cascade.
After=postgresql.service

[Service]
Type=exec
User=acme
Group=acme
# The single most important line of the migration: stop the daemon forking.
# Almost every daemon has a flag for this - --foreground, -f, -D, --no-detach,
# --nodaemon. Find it and the PID file problem disappears with it.
ExecStart=/opt/acme/bin/collector --config /etc/acme/collector.conf --foreground
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5s
TimeoutStopSec=30s

# /var/run/acme-collector.pid was in /run all along; let systemd own it.
RuntimeDirectory=acme-collector
StateDirectory=acme-collector
ConfigurationDirectory=acme

NoNewPrivileges=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectSystem=strict
ProtectHome=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
RestrictNamespaces=yes
RestrictSUIDSGID=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes
SystemCallArchitectures=native
SystemCallFilter=@system-service
# Empty means "no capabilities at all". If the daemon binds a port below 1024,
# use CapabilityBoundingSet=CAP_NET_BIND_SERVICE plus the matching
# AmbientCapabilities= instead of dropping this line.
CapabilityBoundingSet=

[Install]
WantedBy=multi-user.target
في السكربتفي الوحدةملاحظة
start-stop-daemon --background أو &Type=exec وخيار المقدمة الخاص بالديمون نفسههذا هو بيت القصيد. لا تدفع شيئاً إلى الخلفية.
--make-pidfile، echo $! > …احذفهsystemd يعرف الـ PID لأنه هو من فرّع العملية.
--chuid، su - user -c، runuserUser= وGroup=ينطبق على كل سليل، لا على الأول فقط.
mkdir -p /run/x; chown …RuntimeDirectory=xيُنشأ قبل البدء، ويُزال بعد التوقف، وبالمالك الصحيح.
mkdir -p /var/lib/xStateDirectory=xيبقى عبر عمليات إعادة التشغيل؛ واستخدم CacheDirectory=/LogsDirectory= لبقية الحالات.
ulimit -n 65535LimitNOFILE=65535استدعاء ulimit في السكربت كان ينطبق على الشِل، وأحياناً لا يصل إلى الديمون.
export FOO=barEnvironment= أو EnvironmentFile=EnvironmentFile=-/etc/default/x يبقي ملفات التهيئة القائمة عاملة.
nice -n 10، ioniceNice=10، IOSchedulingClass=أو اذهب أبعد بـ CPUWeight= وIOWeight=.
cd /opt/xWorkingDirectory=/opt/x
>> /var/log/x.log 2>&1احذفهالمخرج يذهب إلى الـ journal موسوماً باسم الوحدة. ولا تدوير تكتبه.
sleep 5; check_if_upType=notify في الديمون، أو فحص صحة في وحدة منفصلةاستدعاء sleep داخل فعل start سباقٌ بفتيل أطول لا أكثر.
restart) stop; sleep 1; startاحذفهsystemctl restart موجود وينتظر كما ينبغي.
كتلة status)احذفهsystemctl status وis-active يبلّغان بالحالة الحقيقية.
trap / تنظيف عند التوقفExecStop=، والأفضل معالجة SIGTERM داخل الديمونTimeoutStopSec= يحدّ مدة الانتظار قبل SIGKILL.

وهذه هي الخريطة بصيغتها العامة، للأنماط التي ستصادفها فعلاً. حيثما يقول صف «احذفه»، فهذا ليس تبسيطاً: السلوك يوفّره المدير نفسه، وإعادة تنفيذه في الشِل هي الطريق إلى شيئين يتصارعان على العملية ذاتها.[unit]

# Cut over on a system that still has the generator, so you can roll back by
# doing nothing. Order matters here.

# 1. Check the file before systemd ever loads it.
sudo systemd-analyze verify /etc/systemd/system/acme-collector.service

# 2. Install it and reload. The native unit now WINS over the script: the
#    generator skips any script that already has a unit of the same name.
sudo systemctl daemon-reload

# 3. Prove which one is live before you touch the running process.
systemctl show -p FragmentPath -p SourcePath --value acme-collector.service
# /etc/systemd/system/acme-collector.service
#                       <- empty SourcePath = no longer generated. Good.

# 4. Restart through systemd, not through the script.
sudo systemctl restart acme-collector.service
systemctl status acme-collector.service --no-pager

# 5. Enable it explicitly. The generator's implicit wiring is not inherited.
sudo systemctl enable acme-collector.service
systemctl is-enabled acme-collector.service   # -> enabled

# 6. Retire the old wiring. Do NOT delete the script yet - move it aside, so
#    a rollback is one mv away for the length of the change window.
sudo rm -f /etc/rc[0-6].d/[SK]??acme-collector
sudo mv /etc/init.d/acme-collector /root/retired-init.d-acme-collector

# 7. The only real test: reboot, then read the boot rather than the status.
sudo systemctl reboot
journalctl -b -u acme-collector.service
systemd-analyze blame | head -20

نفّذ التبديل بهذا الترتيب وسيكلّفك التراجع أمر mv واحداً. أهم خطوة على الإطلاق هي الثالثة: systemctl show -p FragmentPath -p SourcePath يخبرك بلا لبس أي التعريفين هو الحيّ. وجود SourcePath غير فارغ يعني أنك ما زلت تنظر إلى الغلاف المولَّد، وأن ملفك الجديد فيه تعارض في الاسم، أو خطأ نحوي، أو أنه في المجلد الخطأ.[systemctl]

استبدال /etc/rc.local دون توريث أسوأ عاداته

يستحق /etc/rc.local قسماً خاصاً به لأنه الشيء الذي يؤجّله الناس، ولأن وحدة التوافق التي يفقدها كانت تحمل دلالات غريبة فعلاً يجدر بك معرفتها قبل أن تنسخها إلى الأمام. الوحدة الحقيقية rc-local.service كانت تستخدم Type=forking مع GuessMainPID=no وRemainAfterExit=yes وTimeoutSec=infinity، ومرتّبة بعد network.target فقط، ولا تسحب نفسها إلا إن كان الملف قابلاً للتنفيذ. وصفحة الدليل الرسمية نفسها تكلّفت التحذير من أن هذا الترتيب لا يعني أن الشبكة صالحة للاستعمال، ومن أن الأمر كله وُجد للتوافق مع أنظمة System V بعينها لا بوصفه تصميماً يوصي به أحد.[rclocalunit][rclocalman]

# /etc/rc.local was never one thing. Read yours first and split it: a mount,
# a sysctl, a firewall rule and a background daemon are four different units,
# and only the last one belongs in a service.

# --- 1. the direct replacement, when it really is one script ---------------
# /etc/systemd/system/local-startup.service
[Unit]
Description=Local startup commands (former /etc/rc.local)
ConditionFileIsExecutable=/usr/local/sbin/local-startup
# The old rc-local.service used After=network.target, which upstream's own
# manual page warns does not mean the network is usable. If your script talks
# to the network, use network-online.target and pull it in.
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/sbin/local-startup
# The original had TimeoutSec=infinity. Keeping that means a hung script hangs
# the boot forever with no message. Pick a number you would accept waiting.
TimeoutStartSec=90s

[Install]
WantedBy=multi-user.target

# --- 2. the pieces that should not be a service at all ---------------------
# sysctl:      /etc/sysctl.d/90-local.conf
# modules:     /etc/modules-load.d/local.conf
# files/dirs:  /etc/tmpfiles.d/local.conf
# mounts:      /etc/fstab or a .mount unit
# periodic:    a .timer, not a sleep loop

# --- 3. install it --------------------------------------------------------
sudo install -m 0755 /etc/rc.local /usr/local/sbin/local-startup
sudo systemctl daemon-reload
sudo systemctl enable --now local-startup.service
systemctl status local-startup.service --no-pager
# On SELinux systems relabel the script, exactly as the old generator's manual
# page told you to do for /etc/rc.local:
command -v restorecon >/dev/null && sudo restorecon -v /usr/local/sbin/local-startup

عادتان تستحقان الكسر وأنت هناك. لا تُبقِ على TimeoutSec=infinity: فهي تحوّل أمراً معلّقاً إلى إقلاع لا ينتهي أبداً، بصمت، وبلا unit فاشلة تشير إليها. ولا تُبقِ كل شيء في سكربت واحد. ملف rc.local النموذجي هو أربعة أشياء غير مترابطة وُضعت معاً لأن الخطاف المتاح كان واحداً فقط — إعداد sysctl، ونقطة تركيب، وقاعدة جدار ناري، وشيء كان يجب أن يكون ديموناً منذ البداية. وكوحدات منفصلة يمكن ترتيبها وإعادة محاولتها وتعطيلها باستقلال، وحين ينكسر أحدها تعرف أيّها انكسر.[special]

Type=forking وملفات PID والديمونات التي تقاومك

هذا هو الخطأ الذي يكلّفك ليلة كاملة، وهو لا يعلن عن نفسه. إن كان الديمون يتفرّع والوحدة تقول Type=simple، فسيتتبّع systemd العملية الأم، ويراها تخرج خلال أجزاء من الألف من الثانية، ثم إما يقتل الأبناء الناجين بوصفهم شاردين، أو يعلن نجاح الإقلاع بينما لا شيء يعمل. وإن كان الديمون يتفرّع والوحدة تقول Type=forking لكن ملف الـ PID يصل متأخراً، فسيفقد systemd أثر العملية الرئيسية ويُبلّغ عن MainPID=0: تبدو الوحدة نشطة، ولا يعمل Restart= أبداً، ويترك الإطفاء عمليات خلفه.[daemon][service]

# The failure mode nobody warns you about: the unit reports "active" and the
# daemon is dead, or reports "failed" while the daemon is happily running.
# Both come from getting Type= wrong.

# --- diagnosis -------------------------------------------------------------
systemctl show -p Type -p MainPID -p PIDFile -p ControlGroup --value myd.service
# forking
# 0                <- systemd has no main process: it lost the daemon
# /var/run/myd.pid
# /system.slice/myd.service

# If MainPID is 0 while the process exists, systemd is not supervising it.
# Restart=, watchdogs, and clean shutdown are all silently not working.
systemd-cgls -u myd.service      # who is actually inside the unit's cgroup

# --- fix 1 (preferred): stop forking --------------------------------------
# Type=exec: systemd considers the unit started once the binary has been
# executed. No PID file, no race, no double-fork, no GuessMainPID guesswork.
#   ExecStart=/usr/sbin/myd --foreground
#   Type=exec

# --- fix 2 (best, if the daemon supports it): tell systemd when you are up --
# Type=notify with sd_notify(READY=1) from the daemon, or Type=notify-reload
# if it can also signal that a reload finished. These - together with Type=dbus
# for D-Bus services - are the only readiness protocols precise enough that
# After= on a dependent unit means "the port is actually accepting".
#   Type=notify
#   NotifyAccess=main

# --- fix 3 (last resort): keep forking, but do it correctly ---------------
# PIDFile= must be an absolute path under /run, and the daemon must write it
# BEFORE the parent exits. Anything else is a race you will lose under load.
#   Type=forking
#   PIDFile=/run/myd/myd.pid
#   RuntimeDirectory=myd

# --- do not do this -------------------------------------------------------
# Type=simple with a daemon that forks. systemd will track the parent, see it
# exit immediately, and either kill the children or declare success while the
# service never started. This is the single most common broken conversion.

العلاج الدائم ليس ملف PID أذكى، بل أن تتوقف عن التفرّع أساساً. تكاد كل ديمونات العشرين سنة الماضية تملك خيار سطر أوامر للبقاء في المقدمة (foreground)، وتوجيه المصدر نفسه للديمونات الحديثة هو استعمالها. عندها يزيل Type=exec صنف المشكلة بأكمله، ويذهب Type=notify أبعد: فهو — ومعه notify-reload، وType=dbus لخدمات D-Bus — الطريقة الوحيدة الموثوقة لجعل After= على وحدة تابعة يعني «أن المقبس يقبل الاتصالات فعلاً» بدل «أن الملف التنفيذي جرى تشغيله». وما عدا ذلك ترتيب مبني على الأمل.[incompat]

init 3 وtelinit وrunlevel ذهبت هي الأخرى

هذا هو الجزء من التغيير الذي يتجاوز الخوادم إلى ذاكرة الأصابع وإلى كتيبات التشغيل. أزال الإصدار 258 عقدة الجهاز /dev/initctl وأوامر initctl وrunlevel وtelinit، وأزال دعم تغيير الحالة عبر init 3، وأزال وحدات runlevel[0-6].target، وتوقف عن تسجيل انتقالات مستويات التشغيل في utmp وwtmp — لأن مفهوم مستوى التشغيل لم يعد موجوداً. ثم أعاد الإصدار 259 الأهداف فقط، وخلف خيار البناء -Dcompat-sysv-interfaces=yes حصراً؛ ولم يُعِد الأوامر.[v258][v259]

SysVsystemdملاحظة
init 3، telinit 3systemctl isolate multi-user.targetالأمر telinit هو وحده الذي أُزيل في 258. أما init فما زال موجوداً — فهو العملية رقم 1 — لكن التحكم في حالة النظام عبره هو ما أُزيل، فلم يعد init 3 يفعل شيئاً.
init 5systemctl isolate graphical.target
init 1، telinit Ssystemctl isolate rescue.targetوemergency.target ينزل أعمق من ذلك.
init 0 / init 6systemctl poweroff / systemctl reboot
runlevelsystemctl get-default، systemctl list-units --type=targetالأمر أُزيل؛ والهدف ليس مستوى تشغيل.
مستوى التشغيل الافتراضي في /etc/inittabsystemctl set-default multi-user.targetملف /etc/inittab لم يُقرأ منذ سنوات.
chkconfig x on، update-rc.d x defaultssystemctl enable x.serviceيحتاج قسم [Install] داخل الوحدة.
chkconfig x off، update-rc.d -f x removesystemctl disable x.service
(لا مقابل له)systemctl mask x.serviceيجعل الوحدة غير قابلة للتشغيل حتى كاعتمادية.
chkconfig --list، service --status-allsystemctl list-unit-files --type=service
service x startsystemctl start x.serviceغالباً ما يبقى service موجوداً كطبقة توافق؛ لا تعتمد عليه.
# v258 removed /dev/initctl and the initctl, runlevel and telinit commands,
# and with them `init 3`. v259 brought the runlevel[0-6].target ALIASES back,
# but only if the distribution builds with -Dcompat-sysv-interfaces=yes, and
# the commands did not come back at all. Do not build habits on them.

# What runlevel am I in?  ->  what is the system aiming at?
systemctl get-default                      # the boot target
systemctl list-units --type=target --state=active

# init 3 / telinit 3     ->
sudo systemctl isolate multi-user.target
# init 5                 ->
sudo systemctl isolate graphical.target
# init 1 / telinit 1 / S ->
sudo systemctl isolate rescue.target
# init 0                 ->
sudo systemctl poweroff
# init 6                 ->
sudo systemctl reboot

# Change the default "runlevel" permanently (this replaces /etc/inittab):
sudo systemctl set-default multi-user.target

# chkconfig foo on / update-rc.d foo defaults ->
sudo systemctl enable foo.service
# chkconfig foo off / update-rc.d -f foo remove ->
sudo systemctl disable foo.service
# ...and the one with no SysV equivalent, for a unit that must never start:
sudo systemctl mask foo.service

# chkconfig --list / service --status-all ->
systemctl list-unit-files --type=service
systemctl list-units --type=service --all

أحد هذه البدائل ليس له مقابل في SysV إطلاقاً، ويستحق أن تتعلّمه لذاته. الأمر systemctl mask يجعل الوحدة غير قابلة للتشغيل، بأي وسيلة، بما في ذلك كاعتمادية لشيء آخر — وأقرب ما وصلت إليه SysV يوماً هو حذف السكربت وأنت تتمنى ألا يعيده مدير الحزم. إنه الأداة الصحيحة لإحالة خدمة إلى التقاعد وأنت لست مستعداً لإزالتها بعد، ولضمان ألا يُحيي زميل حسن النية سكربتاً خارجاً من الخدمة.[systemctl]

ما تكسبه ولم يمنحه لك الغلاف يوماً

من الإنصاف الاعتراف بأن هذا الترحيل عمل مفروض عليك ولا ميزة في نهايته. فخذ التعويض إذن، لأنه حقيقي ومجاني: سكربت init الملفوف لم يكن يستطيع استعمال أي منه. الغلاف كان يشغّل سكربت الشِل، وسكربت الشِل كان يشغّل الديمون؛ ولم يكن لدى systemd أدنى فكرة عن ماهية العملية، ولا يستطيع إعادة تشغيلها بثقة، ولا يستطيع تقييدها إطلاقاً. أما الـ unit الأصلية فتحصل على كل ما يلي بإضافة أسطر:[exec][resctl]

  • حلقة إشراف حقيقية. Restart=on-failure مع RestartSec=، مدعومة بـ StartLimitIntervalSec= بحيث تتوقف حلقة الانهيار بدل أن تشلّ نواة معالج كاملة. الغلاف المولَّد كان يثبّت Restart=no لكل سكربت أنتجه في حياته.
  • عزل نظام الملفات. ProtectSystem=strict يجعل نظام الملفات كله للقراءة فقط عدا ما تسمّيه أنت، وPrivateTmp=yes يمنح الخدمة /tmp خاصاً بها، وStateDirectory= وRuntimeDirectory= وConfigurationDirectory= تنشئ المجلدات التي كان سكربتك ينشئها يدوياً بـ mkdir -p، وتملكها، وتنظّفها.
  • خفض امتيازات يصمد فعلاً. User= وNoNewPrivileges=yes وCapabilityBoundingSet= صريحة تحل محل su وstart-stop-daemon --chuid، وخلافاً لهما تنطبق على كل عملية تولّدها الخدمة، إلى الأبد، بما فيها تلك التي يحاول ديمون مخترَق توليدها.
  • تقييد نداءات النظام وفضاءات الأسماء. SystemCallFilter=@system-service وRestrictAddressFamilies= وRestrictNamespaces= وMemoryDenyWriteExecute=yes هي سياسة seccomp تكتبها في أربعة أسطر. ولم يكن لها مقابل في الشِل بأي ثمن.
  • محاسبة موارد لكل خدمة. MemoryMax= وCPUQuota= وTasksMax= وIOWeight= تنطبق على cgroup الخاصة بالوحدة، أي على الديمون وكل ما يتفرّع عنه — وهو تحديداً ما لم يستطع سكربت قائم على ملف PID احتواءه يوماً.
  • سجلات تصل إلى مكان ما. المخرج القياسي والخطأ القياسي يذهبان إلى الـ journal واسم الوحدة ملحق بهما، فيعمل journalctl -u دون أن يعرف الديمون ما هو syslog أصلاً، ودون أن يملأ ملف سجل قسماً بأكمله بصمت لأن أحداً لم يكتب قاعدة تدوير.

لكن لا تضف ذلك كله على العمياء. شغّل systemd-analyze security على الوحدة، وتعامل مع الدرجة بوصفها قائمة مهام لا شهادة تقدير: فهي تسمّي كل توجيه لم تضبطه وما الذي سيكسبه لك. شدّ الخناق على خطوات صغيرة، وأعد التشغيل، واقرأ الـ journal — فنمط الفشل في التقسية المفرطة هو خدمة تعمل بشكل مثالي ثم تعجز عن فتح ملف بعد ثلاث ساعات، وهذه ورطة أشدّ بكثير من خدمة ترفض الإقلاع من البداية.[analyze]

تحقّق من الترحيل بدل أن تتمنّاه

لا تتحقق بالنظر إلى systemctl status ورؤية اللون الأخضر. الغلاف الملفوف كان يظهر أخضر هو الآخر. تحقّق من الأشياء الأربعة التي اختلفت فعلاً: أن التعريف ملف تتحكم فيه أنت لا ملف مولَّد، وأن لا SourcePath يعود بك إلى سكربت init، وأن الوحدة مفعّلة صراحةً لا بأسلاك متبقية من الماضي، وأن systemd يعرف معرّف العملية الرئيسية.[analyze]

#!/usr/bin/env bash
# Run after the cut-over and again after the first reboot. Non-zero exit means
# something in the migration is not finished.
set -u
rc=0
units=("$@")   # e.g. ./verify.sh acme-collector.service local-startup.service

for u in "${units[@]}"; do
  echo "--- $u"

  # 1. Is it a real file, not a generated one?
  frag=$(systemctl show -p FragmentPath --value "$u")
  case "$frag" in
    /etc/systemd/system/*|/usr/lib/systemd/system/*|/lib/systemd/system/*) ;;
    *) echo "  FAIL still generated or missing: '$frag'"; rc=1 ;;
  esac

  # 2. No SourcePath = no init script behind it.
  src=$(systemctl show -p SourcePath --value "$u")
  [ -z "$src" ] || { echo "  FAIL SourcePath=$src"; rc=1; }

  # 3. Syntactically valid, with no warnings.
  systemd-analyze verify "$frag" || { echo "  FAIL verify"; rc=1; }

  # 4. Enabled explicitly, not by leftover generator wiring.
  [ "$(systemctl is-enabled "$u")" = enabled ] \
    || { echo "  FAIL not enabled"; rc=1; }

  # 5. Actually supervised: a Type= that forks and loses its child shows
  #    MainPID=0 while looking perfectly healthy.
  mp=$(systemctl show -p MainPID --value "$u")
  ty=$(systemctl show -p Type --value "$u")
  [ "$ty" = oneshot ] || [ "$mp" != 0 ] \
    || { echo "  FAIL Type=$ty but MainPID=0"; rc=1; }

  # 6. Report the sandbox score. Not pass/fail - a number to improve.
  systemd-analyze security "$u" | tail -1
done

# 7. Nothing anywhere is still being generated from an init script.
if ls /run/systemd/generator.late/*.service >/dev/null 2>&1; then
  for g in /run/systemd/generator.late/*.service; do
    grep -q '^SourcePath=/etc/init.d/' "$g" && { echo "STILL WRAPPED: $g"; rc=1; }
  done
fi

# 8. Nothing failed at boot for an ordering reason you introduced.
systemctl --failed --no-legend --no-pager
exit $rc

ثم أعد التشغيل، لأن صنف العلل الذي يُدخله هذا الترحيل بأكمله هو علل الترتيب، وعلل الترتيب غير مرئية على نظام يعمل وكل اعتمادياته قائمة سلفاً. اقرأ journalctl -b -u لكل وحدة محوَّلة، وsystemd-analyze blame للإقلاع ككل. فالخدمة التي كانت تبدأ تحت رابط مرقّم عند الموضع 20 وصارت تبدأ مبكراً أكثر مما ينبغي لن تفشل — بل ستعيد المحاولة، أو ستبدأ بتهيئة فارغة، أو ستربط منفذاً قبل أن تحصل الواجهة على عنوان.

إن كنت تشحن برمجيات إلى خوادم الآخرين

إن كنت توزّع برمجيات يركّبها آخرون، فقد توقف هذا عن كونه اختيارياً منذ مدة. المولّد كان يسجّل تحذيراً لكل سكربت يلفّه منذ 255، موجّهاً إليك مباشرة: please update package to include a native systemd unit file. وعلى systemd 260 لم يعد ذلك التحذير موجوداً، لأنه لم يبقَ ما يُحذَّر منه — تُركَّب الحزمة والسكربت معها، ثم لا شيء يشغّله.[gensrc]

ما يجب أن تشحنه الحزمة الآن هو ملف unit في /usr/lib/systemd/system/ بقسم [Install] سليم، لأن المكوّن الثالث المحذوف، systemd-sysv-install، كان الخطاف الذي يسمح لـ systemctl enable بالسقوط إلى chkconfig أو update-rc.d في حالة الخدمات القائمة على سكربتات. وبدونه صار تفعيل خدمة مسألة روابط رمزية يصفها [Install] لا غير. أبقِ سكربت init في الحزمة إن كنت ما زلت تدعم توزيعات أقدم — الـ unit الأصلية والسكربت يتعايشان، والوحدة تفوز حيثما كان systemd حديثاً بما يكفي ليهمّ.[unit]

الترتيب الذي تنفّذ به هذا كله

لا توجد نسخة من هذا السيناريو يكون فيها التأجيل أرخص. العمل متناسب مع عدد السكربتات، وهذا العدد لا ينكمش، والنافذة الزمنية يحدّدها جدول إصدارات شخص آخر. ما يتغيّر هو مقدار المهلة التي تحصل عليها، ولذلك فالقرار الأول هو تحديد أي هذه الحالات أنت فيها:[ubuntu2604]

الحالةكم من الوقت أمامكافعل هذا
توزيعة متدحرجة، أو systemd 260 مركّب بالفعللا وقت — الأمر انتهى سلفاًاحصر من النسخ الاحتياطية أو من إدارة التهيئة، ثم حوّل. السكربتات على القرص خاملة لكنها ما زالت تخبرك بما كان موجوداً.
سلسلة Ubuntu المرحلية (26.04 ← 26.10)أسابيعاحصر والتقط الوحدات المولَّدة الآن، بينما 259 ما زال يعمل. وحوّل قبل إصدار أكتوبر.
Ubuntu 26.04 LTS مع البقاء على LTSحتى الـ LTS التاليةلا طوارئ، لكن الـ journal يحذّر بالفعل. حوّل عند سنوح الفرصة، وابدأ بكل ما لا مالك له.
Debian stable، أو RHEL 9 أو 10حتى النسخة الكبرى القادمةالمولّد حاضر ويعمل، وعلى Debian 13 وRHEL 10 يحذّر في الـ journal لكل سكربت يلفّه. نفّذ الحصر على أي حال — تكلفته أمر واحد، وهو يتقادم بشكل جيد.
أنت تُحزّم برمجيات للآخرينلا وقتاشحن ملف unit بقسم [Install] من الآن. ترقيات مستخدميك ليست تحت سيطرتك.
جهاز مغلق أو وكيل مورّد لا تستطيع تعديلهمثل المضيف نفسهاكتب الوحدة بنفسك ونفّذ mask على سكربت المورّد، أو أثِر الأمر معه قبل أن يتركك إصداره القادم عالقاً.
  1. احصر الأسطول كله هذا الأسبوع، باستخدام استعلام الـ journal أعلاه، واحفظ الناتج. عمودان — اسم المضيف ومسار السكربت — يكفيان لتقدير حجم العمل ولالتقاط الأجهزة التي لا يتذكرها أحد.
  2. التقط الوحدة المولَّدة لكل سكربت بـ systemctl cat إلى مجلد تحتفظ به. عملية قراءة فقط، تستغرق دقائق، وبعد الترقية لا يمكن استرجاع هذه المعلومة.
  3. افرز إلى ثلاث أكوام: احذف، استبدل، أعد الكتابة. نسبة مفاجئة مما ستجده هي خدمة لشيء أُحيل إلى التقاعد قبل سنوات. حذفه ترحيل كامل ولا يستغرق وقتاً على الإطلاق.
  4. حوّل السهل أولاً — أي شيء مُحزَّم رسمياً من المنبع يكاد يقيناً أن تكون له unit رسمية، وتركيب الحزمة الحالية أسرع وأصحّ من كتابة واحدة بنفسك.
  5. نفّذ التبديل والمولّد ما زال موجوداً، خدمة واحدة في كل مرة، على نظام ما زال السكربت حاضراً فيه. عندها يكون التراجع حذف ملف واحد وإعادة تحميل، لا استرجاع نسخة احتياطية تحت الضغط.
  6. أعد التشغيل واقرأ الإقلاع، لكل مضيف، قبل أن تعتبر شيئاً منتهياً. ثم نفّذ mask على الوحدات المتقاعدة كي لا يعيدها شيء.

نافذة الصيانة نفسها تتسع عادةً لبقية الأمر: ترقية خادم Ubuntu من 24.04 إلى 26.04 يغطي ترقية التوزيعة التي ستسحب المولّد من تحت قدميك، ومؤقتات systemd في مواجهة cron هو النصف الآخر من التنظيف ذاته، إذ إن الجهاز الذي يحوي سكربتات SysV يحوي غالباً جداول crontab كان ينبغي أن تكون وحدات هي الأخرى، وما الذي ينكسر عند الانتقال إلى Docker Engine 29 يتناول بيئة تشغيل الحاويات التي غيّرت افتراضاتها في المدة نفسها ولأسباب متصلة.

أسئلة شائعة

هل أستطيع ببساطة إعادة تركيب sysv-generator على systemd 260؟

لا، والحيل التي تبدو وكأنها قد تنجح أسوأ من تنفيذ الترحيل نفسه. المولّد حُذف من شجرة المصدر، ولم يُعطَّل خلف خيار، فلا توجد حزمة تركّبها ولا خيار تضبطه — وخيارات meson الثلاثة المهملة الباقية (-Drc-local=، -Dsysvinit-path=، -Dsysvrcnd-path=) تؤثر في المسارات فقط، لا في وجود الكود. يمكنك نظرياً نسخ ملف المولّد التنفيذي من بناء 259 إلى /usr/lib/systemd/system-generators/، لكنك ستشغّل مولّداً غير مصان مقابل مدير لم يُختبر معه قط، وسيُستبدل بصمت مع تحديث systemd التالي. كتابة ملف الـ unit تستغرق وقتاً أقل من تنقيح ذلك مرة واحدة.

هل ما زال سكربت init يعمل إن شغّلته يدوياً؟

نعم. لم يتغير شيء في السكربت؛ هو سكربت شِل عادي، و/etc/init.d/x start سيفعل تماماً ما كان يفعله دائماً. الذي ذهب هو الترجمة التلقائية إلى خدمة، ما يعني: لا يبدأ عند الإقلاع، وsystemctl لا يعرف بوجوده، ولا شيء يشرف على العملية أو يعيد تشغيلها، وإطفاء الجهاز لن يوقفها بشكل نظيف. وهذه التركيبة أسوأ مما تبدو، لأن سكربتاً ما زلت تستطيع تشغيله يدوياً لا يبدو مكسوراً — فيلتف الناس حوله بمدخلة @reboot في crontab وتهدأ المشكلة سنة كاملة.

كيف أعرف أي خوادمي متأثرة دون الدخول إليها جميعاً؟

استعلم الـ journal عن التحذير المهيكل. اعتباراً من systemd 255، ينتج كل سكربت ملفوف مدخلة سجل تحمل MESSAGE_ID=a8fa8dacdb1d443e9503b8be367a6adb وحقلين مخصّصين هما SYSVSCRIPT= وUNIT=. فإن كنت تشحن الـ journals مركزياً، فذلك استعلام واحد عبر الأسطول كله. وإن لم تكن، فشغّل journalctl -b MESSAGE_ID=a8fa8dacdb1d443e9503b8be367a6adb عبر أي أداة تنفّذ الأوامر على مضيفات كثيرة. أما على systemd 254 وما قبله فاللفّ صامت، فارجع إلى سؤال كل unit عن FragmentPath وSourcePath، كما يفعل سكربت الحصر أعلاه.

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

القيمة Type=simple تعتبر الخدمة بدأت بمجرد أن يفرّع systemd، أي قبل أن يكون الملف التنفيذي قد نُفّذ بنجاح بالضرورة. أما Type=exec فينتظر حتى يُنفَّذ الملف التنفيذي فعلاً، ما يلتقط ملفاً مفقوداً أو User= خاطئاً بوصفه فشلاً في البدء بدل خروج فوري غامض — وهي القيمة الافتراضية الأفضل لديمون يعمل في المقدمة. وType=forking مخصّصة للديمونات التي تدفع نفسها إلى الخلفية وتتوقع PIDFile=؛ وهي ما كان يستعمله sysv-generator لأنه لم يملك خياراً آخر. وType=notify ينتظر أن يستدعي الديمون sd_notify(READY=1)، وهي الوحيدة بين الأربع التي تعني معها اعتمادية After= أن الخدمة جاهزة فعلاً. حوّل إلى Type=exec؛ واستخدم Type=notify إن كان الديمون يدعمها.

هل ذهب /etc/rc.local فعلاً أم أنه مهمل فقط؟

ذهب، على systemd 260 وما بعده. أُزيل systemd-rc-local-generator ومعه وحدة rc-local.service التي كان يسحبها. سيظل الملف قابعاً في /etc، وقابلاً للتنفيذ، ولن يستدعيه شيء أبداً. ولاحظ أن المسار كان إعداداً وقت التصريف ويختلف بين التوزيعات — بعضها كان يستعمل /etc/rc.d/rc.local — فتحقّق مما هُيّئ به مولّدك فعلياً قبل أن تفترض أنك عثرت عليها كلها.

وحدتي تقول active (running) لكن العملية غير موجودة. ما الخطأ الذي ارتكبته؟

على الأرجح Type=forking مع ملف PID لم يستطع systemd قراءته في اللحظة الصحيحة، أو Type=simple على ديمون يتفرّع. شغّل systemctl show -p Type -p MainPID -p PIDFile --value yourunit.service: إن كان MainPID يساوي 0 والديمون يعمل، فقد فقد systemd أثره ولا ينطبق أي من الإشراف. والعلاج أن توقف الديمون عن التفرّع — ابحث عن خيار سطر الأوامر الذي يبقيه في المقدمة واستخدم Type=exec. وإن كنت عاجزاً عن ذلك حقاً، فتأكد أن PIDFile= مسار مطلق تحت /run، وأن الديمون يكتبه قبل خروج العملية الأم.

هل أحتفظ بسكربت init بعد كتابة الوحدة؟

احتفظ به خلال نافذة التغيير، ثم أزله. ما دام الاثنان موجودين، تفوز الـ unit الأصلية — فالمولّد كان يتخطى صراحةً أي سكربت له unit بالاسم نفسه — فوجودهما معاً آمن ويجعل التراجع تافهاً. لكن إن تركته هناك بشكل دائم صار لديك تعريفان للخدمة ذاتها، أحدهما فخ لمن سينقّح هذا في الثالثة فجراً. انقله إلى مجلد للمتقاعدين بعد أن يُعاد إقلاع المضيف بنظافة، واحذف روابط /etc/rc*.d في الوقت نفسه.

هل يلزمني القلق من هذا على RHEL أو Debian stable؟

ليس بشكل عاجل، لكن نفّذ الحصر على أي حال. تشحن RHEL 10 وDebian 13 كلتاهما systemd 257، فكلتاهما ما زالت تملك المولّد، وكلتاهما تطبع بالفعل تحذير الإهمال لكل سكربت الذي وصل في 255 — وملاحظات إصدار Red Hat نفسها تنص سلفاً على أن دعم سكربتات خدمات System V مهمل وسيُزال. والتغيير يصل مع النسخة الكبرى التالية لكل منهما. وسبب التحرك الآن أن خطوة الحصر أمر واحد للقراءة فقط، وهي أسهل بكثير والمولّد يعمل ويخبرك بما يلفّه، والبديل هو اكتشاف سكربت بلا مالك أثناء ترقية كبرى، وهو أسوأ وقت ممكن لتطبيق الهندسة العكسية على سكربت زميل لك.

ما الذي يحل محل systemd-sysv-install؟

لا شيء، لأنه لم يبقَ له ما يفعله. كان الخطاف الذي يسمح لـ systemctl enable وdisable وis-enabled بالسقوط إلى أداة التوزيعة الخاصة — chkconfig على أنظمة Red Hat، وupdate-rc.d على أنظمة Debian — حين تكون الخدمة المسمّاة سكربتاً لا unit. ومع إزالة دعم السكربتات، صار تفعيل خدمة مسألة روابط رمزية يصفها قسم [Install] في الوحدة، لا غير. فإن كانت حزمتك تعتمد على ذلك السقوط، فهي الآن تحتاج ملف unit حقيقياً بقسم [Install] حقيقي.

هل يمكنني تحويل سكربت آلياً بدل التحويل اليدوي؟

جزئياً، وأفضل محوّل هو ذاك الذي يوشك أن يُزال: الأمر systemctl cat yourservice.service على نظام ما زال يملك المولّد يعطيك ترجمة صحيحة آلياً للاعتماديات، وهي الجزء الذي يسهل الخطأ فيه. أما ما لا تستطيع أي أداة أن تفعله نيابة عنك فهو الجزء المهم — أن تقرّر أن الديمون يجب أن يتوقف عن التفرّع، وأن ملف الـ PID يجب أن يختفي، وأن start-stop-daemon --chuid يصبح User=، وأي توجيهات العزل (sandboxing) آمنة لهذا الحمل تحديداً. تعامل مع الوحدة المولَّدة بوصفها المواصفة، واكتب الوحدة الحقيقية انطلاقاً منها.

طبقة التحكم في الموارد تحرّكت في الإصدارات نفسها: أزال systemd 258 دعم cgroup v1 نهائيًا، فصار كل خادم يقلع على التسلسل الموحّد سواء طلب أحد ذلك أم لا. الانتقال من cgroup v1 إلى cgroup v2 يغطي الجرد، وجدول التحويل ملفًا بملف، والترجمتين اللتين تغيّران السلوك لا التسمية فقط.

المصادر

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

  1. systemd — NEWS: the upstream changelog, and the only authoritative statement of what was removed in which release. Everything dated in this article was checked against it
  2. systemd v260 release notes — "Support for System V service scripts has been removed", with the three components that went with it: systemd-sysv-generator, systemd-rc-local-generator plus rc-local.service, and systemd-sysv-install
  3. systemd v259 release notes — the release that restored runlevel[0-6].target behind the new -Dcompat-sysv-interfaces=yes build option, and the version shipped by Ubuntu 26.04 LTS
  4. systemd v258 release notes — the removal of /dev/initctl and of the initctl, runlevel and telinit commands, the removal of the runlevel[0-6].target units, and the removal of cgroup v1
  5. systemd v255 release notes — where support for System V service scripts was first declared deprecated, and the release whose sysv-generator already logs the structured per-script warning this article uses for the inventory
  6. systemd-sysv-generator source at v259 — the last tag where the code still exists. It is the definitive answer to what the generator read, what it ignored, and exactly which directives it wrote
  7. systemd-sysv-generator(8) — the manual page, including the statement that the wrapper units are always ordered after basic.target and that compatibility was never 100%
  8. systemd-rc-local-generator(8) — the rc.local compatibility generator, its warning that rc-local.service is ordered after network.target and that this does not mean the network works, and the SELinux note about restorecon
  9. rc-local.service at v259 — the actual unit that ran /etc/rc.local, so you can see what semantics you are replacing rather than guessing them
  10. systemd exit-status.h — the LSB start-verb exit codes, and the origin of the SuccessExitStatus=5 6 line the generator emitted for scripts with an LSB header
  11. systemd sd-messages.h — the catalogue of structured log message IDs, including SD_MESSAGE_SYSV_GENERATOR_DEPRECATED, which is what makes the journal-based inventory in this article possible
  12. systemd — Incompatibilities: the upstream list of the places where SysV behaviour and systemd behaviour genuinely differ, worth reading before you assume a script will behave the same way
  13. Linux Standard Base — Init Script Actions: the specification that defines the LSB header fields and the exit codes an init script is supposed to return
  14. systemd.service(5) — Type=, Restart=, PIDFile=, ExecReload=, RemainAfterExit= and the rest of the service options used in the unit files below
  15. systemd.unit(5) — Wants=, After=, Before=, the Condition family and the [Install] section that replaces update-rc.d and chkconfig
  16. systemd.exec(5) — User=, RuntimeDirectory=, StateDirectory= and the whole sandboxing vocabulary, none of which a wrapped init script could ever use
  17. systemd.special(7) — what multi-user.target, graphical.target, network.target and network-online.target actually mean, and why After=network.target does not mean the network is up
  18. systemd.generator(7) — how generators work and where their output lands, which is why the units you are about to lose live under /run/systemd/generator.late
  19. systemctl(1) — enable, mask, cat, show, list-unit-files and isolate: the commands that replace chkconfig, update-rc.d, service and telinit
  20. systemd-analyze(1) — verify, security and blame: the three subcommands that turn this migration from a guess into something you can check
  21. journalctl(1) — matching on MESSAGE_ID= and on arbitrary structured fields, which is how the inventory command in this article finds every wrapped script
  22. daemon(7) — the difference between a SysV-style forking daemon and a new-style daemon, and upstream's own recommendation to stop forking
  23. systemd.resource-control(5) — the per-unit cgroup accounting and limits that come for free once a service is a real unit
  24. Ubuntu 26.04 LTS release notes — the section stating that 26.04 LTS is the last release with System V compatibility in systemd, that the change takes effect in 26.10, and that the release ships systemd 259
  25. Ubuntu 26.10 release schedule — the release date that turns "eventually" into a deadline for anyone tracking the interim series
  26. Red Hat Enterprise Linux 10 release notes — the systemd rebase to 257, the statement that System V service script support is deprecated and will be removed, and the move of default configuration files under /usr/lib/systemd
  27. Debian trixie systemd package — the version currently in Debian stable, for readers deciding how much runway they actually have

Was this useful?