systemd 260 entfernt SysV. Was nun?
systemd 260 hat den sysv-generator, rc-local.service und systemd-sysv-install gelöscht. Jedes /etc/init.d-Skript und jede /etc/rc.local auf Ihren Servern hat damit ein Ablaufdatum. So finden Sie sie und wandeln sie sauber in systemd Units um.
- systemd
- Linux
- Systemadministration
- Migration
Rund fünfzehn Jahre lang hat systemd Ihre /etc/init.d-Skripte bei jedem einzelnen Bootvorgang klaglos in Dienste übersetzt, ohne dass Sie es je bemerken mussten. Mit Version 260, veröffentlicht im März 2026, ist damit Schluss. Der systemd-sysv-generator, der systemd-rc-local-generator samt rc-local.service sowie der Hook systemd-sysv-install hinter systemctl enable wurden ersatzlos gelöscht. An Ihren Skripten hat sich nichts geändert. Geändert hat sich, dass sie niemand mehr liest.
![Zweigeteiltes Schaubild zur Migration von SysV nach systemd. Links eine Tafel mit der Überschrift „DELETED IN systemd 260“ und den vier entfernten Komponenten systemd-sysv-generator, systemd-rc-local-generator, rc-local.service und systemd-sysv-install. Rechts eine Tafel mit der Überschrift „WHAT REPLACES WHAT“ und vier Vorher-Nachher-Paaren: start-stop-daemon --background wird zu Type=exec, --make-pidfile wird ersatzlos gestrichen, --chuid acme wird zu User=acme und /etc/rc[2-5].d/S20name wird zu WantedBy=multi-user.target.](/og/migrate-sysv-init-to-systemd-units.png)
Das hier ist die Migration, geschrieben für jemanden, der sie auf echten Servern durchführen muss statt sie aus der Ferne zu bewundern: wie Sie jedes eingebundene Skript über ein Journal-Feld inventarisieren, von dem kaum jemand weiß, dass es existiert; was der Generator genau geschrieben hat, damit Sie es abschreiben statt raten können; eine feldweise Umwandlung eines realistischen Init-Skripts; der Type=-Fehler, bei dem eine Unit Erfolg meldet, während der Daemon längst tot ist; und die Befehle, die init 3 ersetzt haben. Jede Versionsnummer und jede Direktive hier wurde gegen die NEWS-Datei von systemd und gegen den Quellcode an den Git-Tags geprüft, an denen der inzwischen entfernte Code noch existierte – nicht gegen das Gedächtnis.
Die Unit ist nicht failed. Es gibt sie nicht
Das Symptom ist ungewöhnlich verwirrend, weil es eine Abwesenheit ist und kein Fehler. Ihr Dienst erscheint nicht als failed. Er erscheint überhaupt nicht. systemctl status meldet, dass die Unit nicht gefunden werden konnte – während das Skript ausführbar auf der Platte liegt, unverändert seit 2015. Das ist korrektes Verhalten: Das Skript war nie eine Unit. Ein Generator hat bei jedem Bootvorgang eine in /run/systemd/generator.late fabriziert, und dieses Verzeichnis ist jetzt leer.[v260]
| Was Sie sehen | Was es bedeutet | Wo es behandelt wird |
|---|---|---|
Unit x.service could not be found, obwohl /etc/init.d/x existiert und ausführbar ist | Der Generator, der diese Unit früher erfunden hat, wurde entfernt. Es ist nichts kaputt; es übersetzt nur nichts mehr. | Was der Generator geschrieben hat |
| Ein Dienst, der jahrelang bei jedem Boot startete, startet einfach nicht, und im Journal steht nichts | Dieselbe Ursache, gesehen vom Bootvorgang statt von der Kommandozeile. Es gibt keine failed Unit, weil es keine Unit gibt. | Ein Skript umwandeln |
/etc/rc.local ist weiterhin ausführbar und läuft trotzdem nicht mehr | systemd-rc-local-generator und rc-local.service wurden zusammen mit dem SysV-Generator entfernt. | /etc/rc.local ersetzen |
systemctl enable bewirkt bei einem alten Dienst nichts Sinnvolles mehr | systemd-sysv-install, der Hook, der enable/disable an chkconfig oder update-rc.d weiterreichte, ist verschwunden. | Was ein Paket ausliefern muss |
telinit: command not found, oder init 3 tut nichts | Entfernt in 258, zwei Versionen früher und aus einem anderen Grund: Verworfen wurde das Konzept der Runlevel, nicht nur die Skripte. | init 3, telinit und runlevel |
| Eine umgewandelte Unit meldet active, aber der Prozess ist weg – oder meldet failed, während der Daemon läuft | Das Type= passt nicht zu einem Daemon, der forkt. Das ist die häufigste fehlerhafte Umwandlung. | Type=forking und PID-Dateien |
# 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)Generatoren laufen, bevor der Manager irgendeine Unit lädt – bei jedem Boot und bei jedem daemon-reload –, und ihre Ausgabe liegt im tmpfs. Deshalb sieht auf der Platte nach dem Upgrade nichts anders aus, und deshalb gibt es auch nichts zu reparieren: Es existiert keine defekte Datei, sondern nur ein Übersetzungsschritt, der nicht mehr stattfindet. Alles Folgende setzt voraus, dass Sie eine Shell auf der Maschine haben und sie am Ende einmal neu starten können. Nichts in diesem Artikel verändert den Systemzustand, bevor Sie es entscheiden; die Inventarisierungsbefehle lesen ausschließlich.[generator]
Was entfernt wurde – und wann genau
Beteiligt sind vier verschiedene Releases, und ihre Vermischung ist der Grund, warum so viele Ratschläge zu diesem Thema falsch sind. Für abgekündigt erklärt wurden die Dienstskripte bereits in Version 255; seitdem protokolliert der Generator für jedes eingebundene Skript eine strukturierte Warnung. Version 258 vom September 2025 tat etwas ganz anderes: Sie entfernte die System-V-Schnittstellen für den Systemzustand – /dev/initctl, die Befehle initctl, runlevel und telinit, die Zustandssteuerung per init 3, die Units runlevel[0-6].target sowie das Protokollieren von Runlevel-Übergängen in utmp und wtmp. Version 259 machte einen Teil davon rückgängig. Version 260 entfernte die Dienstskripte selbst.[news][v255][v258]
| Release | Datum | Was passiert ist |
|---|---|---|
| 258 | September 2025 | Die bereits in 255 ausgesprochene Abkündigung der SysV-Dienstskripte wird bekräftigt; die Entfernung ist an dieser Stelle noch für 259 vorgesehen und wird erst später auf 260 verschoben. Davon getrennt und sofort wirksam: /dev/initctl, die Befehle initctl, runlevel und telinit, Zustandswechsel im Stil von init 3 sowie die Units runlevel[0-6].target werden allesamt entfernt, und Runlevel-Übergänge werden nicht mehr in utmp/wtmp protokolliert. Im selben Release fällt die Unterstützung für cgroup v1 weg. |
| 259 | Dezember 2025 | runlevel[0-6].target wiederhergestellt, aber nur, wenn die Distribution mit -Dcompat-sysv-interfaces=yes baut. Die entfernten Befehle kehren nicht zurück. Diese Version liefert Ubuntu 26.04 LTS aus. |
| 260 | März 2026 | Unterstützung für System-V-Dienstskripte entfernt. Gelöscht: systemd-sysv-generator; systemd-rc-local-generator und rc-local.service; systemd-sysv-install. Die Build-Optionen -Drc-local=, -Dsysvinit-path= und -Dsysvrcnd-path= gelten als abgekündigt. Mindestkernel steigt auf 5.10, glibc auf 2.34, OpenSSL auf 3.0. |
| 261 und später | 2026 | Keine weiteren SysV-bezogenen Änderungen; die Kompatibilitätsschicht fehlt schlicht. Die abgekündigten Build-Optionen dürften in einem künftigen Release verschwinden. |
Die Teilrücknahme in 259 ist relevant, wenn Sie einen gemischten Serverbestand betreuen: Die Units runlevel[0-6].target kamen zurück, aber nur als Option, die eine Distribution zur Buildzeit aktivieren muss – und die Befehle kamen gar nicht zurück. systemctl isolate runlevel3.target kann also auf der einen Maschine funktionieren und auf der nächsten nicht, je nachdem, wie die jeweilige Distribution ihr Paket übersetzt hat. Bauen Sie darauf keine Gewohnheiten auf. Wo Ihre Server tatsächlich stehen, ist eine Frage der ausgelieferten systemd-Version:[v259][v260]
| Wo Sie stehen | systemd | SysV-Skripte und rc.local |
|---|---|---|
| Debian 12 bookworm | 252 | Beides funktioniert. Keine Abkündigungswarnung pro Skript im Journal. |
| Debian 13 trixie | 257 | Beides funktioniert, und das Journal warnt bereits für jedes eingebundene Skript. |
| Ubuntu 24.04 LTS | 255 | Beides funktioniert, und das Journal warnt bereits für jedes eingebundene Skript. |
| Ubuntu 26.04 LTS | 259 | Beides funktioniert, und das Journal warnt für jedes eingebundene Skript – wie schon seit 24.04. Canonical erklärt dies zum letzten Ubuntu mit SysV-Kompatibilität. |
| Ubuntu 26.10 | 260 oder neuer | Entfernt. Skripte bleiben auf der Platte liegen und werden nie gelesen. |
| RHEL 9 / RHEL 10 | 252 / 257 | Beides funktioniert über die Laufzeit des Release hinweg. RHEL 9 schweigt dazu; RHEL 10 warnt im Journal für jedes eingebundene Skript, und die RHEL-10-Release-Notes enthalten die Abkündigung bereits. Die Änderung kommt mit der nächsten Hauptversion. |
| Rolling-Distributionen | 260 oder neuer | Bereits entfernt. Prüfen Sie es mit systemctl --version, statt es anzunehmen. |
Die Termine machen das konkret statt theoretisch. Canonicals eigene Release Notes halten fest, dass Ubuntu 26.04 LTS – ausgeliefert mit systemd 259 – das letzte Ubuntu-Release mit System-V-Kompatibilität ist und die Änderung mit 26.10 wirksam wird. Dieses Release ist für Oktober 2026 terminiert. Wenn Sie die Interim-Serie fahren oder irgendetwas betreiben, das einer Rolling-Distribution folgt, liegt die Frist in Wochen und nicht in Jahren. Sind Sie auf einem LTS oder einer Enterprise-Distribution, haben Sie bis zum nächsten Major-Upgrade Zeit – also genau bis zu dem Moment, in dem Sie am wenigsten Lust haben, herrenlose Shell-Skripte zu entdecken.[ubuntu2604][ubuntu2610][rhel10]
Finden Sie jedes Skript, bevor das Upgrade es findet
Machen Sie die Inventur zuerst, und machen Sie sie, solange der Generator noch läuft – denn der Generator selbst liefert Ihnen die Antwort. Ab Version 255 gibt systemd bei jedem eingebundenen Skript eine strukturierte Warnung aus, mit einer stabilen Message-ID und zwei eigenen Journal-Feldern: SYSVSCRIPT= mit dem Pfad und UNIT= mit dem Namen, den es erfunden hat. Damit wird aus einem flottenweiten Audit eine einzige exakte Abfrage statt einer Schätzung aus ls und Hoffnung.[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'; }Führen Sie die Inventur auf jedem Host aus, nicht auf einem repräsentativen. Nach meiner Erfahrung sind die Skripte, die bis 2026 überleben, nie diejenigen, die ein Configuration-Management-Repository kennt – es sind das Einzelstück von einem externen Dienstleister, der Agent einer Vendor-Appliance, der Backup-Wrapper, der älter ist als das aktuelle Team. Genau diese Hosts prüft niemand, und genau bei diesen Diensten fällt das Fehlen erst eine Woche später auf.
Beachten Sie dabei, dass Schritt 1 ausschließlich das Journal des aktuellen Bootvorgangs auswertet – auf einem Host, dessen Warnung inzwischen aus dem Journal rotiert ist, bleibt Schritt 2 unverzichtbar. Zwei Details dieser Abfrage sollten Sie verinnerlichen. systemd hat ausschließlich ausführbare reguläre Dateien im Init-Verzeichnis eingebunden – ein Skript mit Modus 644 war also schon vorher tot, und Sie sollten es nicht wiederbeleben. Und eine native Unit hat ein gleichnamiges Skript immer geschlagen: Der Generator übersprang ausdrücklich jedes Skript, für das bereits eine echte Unit existierte. Diese zweite Regel macht die gesamte Migration inkrementell durchführbar – Sie können die neue Unit installieren, während das Skript noch vorhanden und noch aktiviert ist, und das Skript wird einfach nicht mehr herangezogen.[gensrc]
Was der Generator still für Sie geschrieben hat
Bevor Sie eine einzige Unit schreiben, nehmen Sie sich die vom Generator erzeugte und lesen Sie sie. Sie ist keine grobe Annäherung, sondern eine deterministische Übersetzung – und damit das Nächste an einer Spezifikation dessen, was das Bootverhalten Ihres Skripts tatsächlich war. Sichern Sie sie mit systemctl cat, solange das noch geht: Nach dem Upgrade ist diese Information verloren, und genau bei der Rekonstruktion aus dem LSB-Header entstehen die Reihenfolgefehler.[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.Und jetzt der Teil, der die meisten überrascht und den Sie im Quellcode des Generators selbst nachprüfen können: Default-Start: und Default-Stop: im LSB-Header wurden nie ausgewertet. Der Generator liest ausschließlich Provides:, Required-Start:, Should-Start:, X-Start-Before:, X-Start-After:, die beiden Beschreibungsfelder sowie die Kommentare # pidfile: und # description: im Red-Hat-Stil. Die Runlevel-Verdrahtung kam vollständig aus den S??name-Symlinks in /etc/rc[1-5].d. Wenn Sie Default-Start seit einem Jahrzehnt pflegen, pflegen Sie einen Kommentar.[lsb][exitstatus]
| Im Init-Skript | Was der Generator daraus machte | Das schreiben Sie in Ihre Unit |
|---|---|---|
Provides: name | Benennt es einen Dienst, einen Alias-Symlink. Benennt es eine $facility, Before= plus Wants= auf dieses Target – einseitig. Wiederholt es den Dateinamen, ignoriert. | Alias= in [Install], oder nichts |
Required-Start: $network | After=network-online.target und Wants=network-online.target, weil dieses Target untätig bleibt, solange es niemand hereinzieht | Wants= + After=network-online.target |
Required-Start: $remote_fs | After=remote-fs.target | After=remote-fs.target |
Required-Start: $named | After=nss-lookup.target | After=nss-lookup.target |
Required-Start: $portmap | After=rpcbind.target | After=rpcbind.target |
Required-Start: $time | After=time-sync.target | After=time-sync.target |
Required-Start: $local_fs oder $syslog | Nichts. Beide fielen weg: Unter systemd sind sie bereits erfüllt, bevor irgendein gewöhnlicher Dienst läuft. | Nichts |
Should-Start: foo | After=foo.service – nur Reihenfolge, nie eine Anforderung | After=foo.service, ohne Requires= |
X-Start-Before: foo | Before=foo.service | Before=foo.service |
Default-Start: / Default-Stop: | Überhaupt nichts. Nie ausgewertet. Die Runlevel-Verdrahtung kam aus den S??-Symlinks in /etc/rc[1-5].d. | WantedBy=multi-user.target in [Install] |
Symlink in /etc/rc2.d, rc3.d, rc4.d | Before=multi-user.target und ein wants-Symlink dorthin | WantedBy=multi-user.target |
Symlink in /etc/rc5.d | Before=graphical.target und ein wants-Symlink dorthin | WantedBy=graphical.target |
Symlink in /etc/rc1.d | Before=rescue.target und ein wants-Symlink dorthin – der Generator behandelt rc1.d genau wie rc2.d bis rc5.d | Fast nie das, was Sie wollen; weglassen |
# pidfile: /pfad (Red-Hat-Stil) | PIDFile=/pfad und RemainAfterExit=no. Ohne jede pidfile-Zeile: RemainAfterExit=yes | Beides streichen. Stattdessen Type=exec |
### BEGIN INIT INFO vorhanden | SuccessExitStatus=5 6 – die LSB-Codes für „nicht installiert“ und „nicht konfiguriert“ | Nur, wenn Sie diese Codes noch zurückgeben |
Eine Usage-Zeile mit |reload} oder Ähnlichem | ExecReload=/etc/init.d/x reload. Keine Usage-Zeile, kein reload-Verb. | ExecReload=/bin/kill -HUP $MAINPID |
Zwei Facility-Zuordnungen in dieser Tabelle verdienen einen zweiten Blick, weil sie die übliche Ursache für „unter SysV lief es, jetzt gibt es ein Race“ sind. $network wurde nicht zu network.target, sondern zu network-online.target, und der Generator setzte sowohl After= als auch Wants=, weil dieses Target nichts tut, solange es niemand aktiv hereinzieht. $local_fs und $syslog hingegen wurden auf gar nichts abgebildet – sie fielen weg, mit der vernünftigen Begründung, dass unter systemd beides ohnehin garantiert ist, bevor irgendein normaler Dienst startet. Übernehmen Sie diese Entscheidungen in Ihre Unit; erfinden Sie keine neuen.[special]
Ein Skript umwandeln, Feld für Feld
Hier ein realistisches Skript – kein Spielzeug. Es hat einen LSB-Header, einen PID-Datei-Kommentar im Red-Hat-Stil, einen start-stop-daemon-Aufruf, der den Prozess in den Hintergrund schickt und die PID-Datei stellvertretend für den Daemon schreibt, ein reload-Verb und jene Usage-Zeile, die der einzige Grund ist, weshalb der Generator überhaupt ein ExecReload= erzeugt hat. Die meisten echten Skripte da draußen sind schlechter als dieses.
#!/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 0Und hier das, was es ersetzt. Lesen Sie eher die Kommentare als die Direktiven: Die interessanten Entscheidungen sind die darüber, was nicht mitgenommen wird. Das Forking fällt weg. Die manuelle PID-Datei fällt weg. Der start-stop-daemon-Wrapper fällt weg, weil alles, was er getan hat – Rechte abgeben, in den Hintergrund gehen, den Prozess verfolgen –, heute eine Direktive ist.[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| Im Skript | In der Unit | Anmerkung |
|---|---|---|
start-stop-daemon --background oder & | Type=exec und der Vordergrund-Schalter des Daemons | Der eigentliche Punkt. Schicken Sie nichts in den Hintergrund. |
--make-pidfile, echo $! > … | ersatzlos streichen | systemd kennt die PID, weil es den Prozess selbst geforkt hat. |
--chuid, su - user -c, runuser | User= und Group= | Gilt für jeden Nachkommen, nicht nur für den ersten. |
mkdir -p /run/x; chown … | RuntimeDirectory=x | Wird vor dem Start angelegt, nach dem Stopp entfernt, mit korrektem Eigentümer. |
mkdir -p /var/lib/x | StateDirectory=x | Übersteht Neustarts; für die übrigen Fälle CacheDirectory=/LogsDirectory=. |
ulimit -n 65535 | LimitNOFILE=65535 | Ein ulimit im Skript galt der Shell, manchmal nicht dem Daemon. |
export FOO=bar | Environment= oder EnvironmentFile= | EnvironmentFile=-/etc/default/x hält vorhandene Konfigdateien funktionsfähig. |
nice -n 10, ionice | Nice=10, IOSchedulingClass= | Oder gehen Sie weiter mit CPUWeight= und IOWeight=. |
cd /opt/x | WorkingDirectory=/opt/x | |
>> /var/log/x.log 2>&1 | ersatzlos streichen | Die Ausgabe geht ins Journal, markiert mit der Unit. Keine Rotation zu schreiben. |
sleep 5; check_if_up | Type=notify im Daemon, oder ein Health-Check in einer eigenen Unit | Ein sleep im Start-Verb ist ein Race mit längerer Zündschnur. |
restart) stop; sleep 1; start | ersatzlos streichen | systemctl restart existiert und wartet korrekt. |
status)-Block | ersatzlos streichen | systemctl status und is-active melden den echten Zustand. |
trap / Aufräumen beim Stoppen | ExecStop=, besser noch SIGTERM im Daemon behandeln | TimeoutStopSec= begrenzt die Wartezeit bis zum SIGKILL. |
Die Zuordnung in allgemeiner Form, für die Idiome, die Ihnen tatsächlich begegnen. Wo in einer Zeile „ersatzlos streichen“ steht, ist das keine Vereinfachung: Das Verhalten liefert der Manager, und es in der Shell nachzubauen ist der Weg, auf dem zwei Instanzen um denselben Prozess kämpfen.[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 -20Stellen Sie in dieser Reihenfolge um, dann kostet ein Rollback ein einziges mv. Der mit Abstand wichtigste Schritt ist der dritte: systemctl show -p FragmentPath -p SourcePath sagt Ihnen unmissverständlich, welche der beiden Definitionen aktiv ist. Ein nicht leerer SourcePath bedeutet, dass Sie noch auf den generierten Wrapper schauen und Ihre neue Datei entweder einen Namenskonflikt hat, einen Syntaxfehler enthält oder im falschen Verzeichnis liegt.[systemctl]
/etc/rc.local ersetzen, ohne die schlechteste Angewohnheit mitzunehmen
/etc/rc.local verdient einen eigenen Abschnitt, weil es das ist, was alle aufschieben, und weil die Kompatibilitäts-Unit, die dabei verloren geht, wirklich eigenartige Semantik hatte, die man kennen sollte, bevor man sie fortschreibt. Der echte rc-local.service nutzte Type=forking mit GuessMainPID=no, RemainAfterExit=yes und TimeoutSec=infinity, war lediglich nach network.target einsortiert und zog sich nur dann selbst herein, wenn die Datei ausführbar war. Die Upstream-Manpage warnte ausdrücklich, dass diese Einsortierung nicht bedeutet, dass das Netzwerk benutzbar ist, und dass das Ganze der Kompatibilität mit bestimmten System-V-Systemen diente und nicht als Design gedacht war, das irgendjemand empfiehlt.[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-startupZwei Angewohnheiten sollten Sie bei dieser Gelegenheit ablegen. Behalten Sie TimeoutSec=infinity nicht: Es verwandelt einen hängenden Befehl in einen Bootvorgang, der niemals fertig wird – lautlos, ohne failed Unit, auf die man zeigen könnte. Und behalten Sie nicht alles in einem Skript. Eine rc.local besteht meist aus vier unzusammenhängenden Dingen, die nur deshalb zusammenkamen, weil es genau einen Hook gab – ein sysctl, ein Mount, eine Firewall-Regel und etwas, das eigentlich ein Daemon hätte sein müssen. Als getrennte Units lassen sie sich unabhängig einsortieren, wiederholen und deaktivieren, und wenn eines davon kaputtgeht, wissen Sie, welches.[special]
Type=forking, PID-Dateien und Daemons, die sich wehren
Das ist der Fehler, der eine Nacht kostet, und er kündigt sich nicht an. Forkt der Daemon und die Unit sagt Type=simple, verfolgt systemd den Elternprozess, sieht ihn nach Millisekunden beenden und tötet dann entweder die überlebenden Kinder als Streuner oder erklärt den Start für erfolgreich, während nichts läuft. Forkt der Daemon und die Unit sagt Type=forking, die PID-Datei kommt aber zu spät, verliert systemd den Hauptprozess und meldet MainPID=0: Die Unit sieht aktiv aus, Restart= greift nie, und beim Herunterfahren bleiben Prozesse zurück.[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.Die dauerhafte Lösung ist keine klügere PID-Datei, sondern gar nicht mehr zu forken. Nahezu jeder in den letzten zwanzig Jahren geschriebene Daemon hat einen Schalter, um im Vordergrund zu bleiben, und die Upstream-Empfehlung für Daemons neuen Stils lautet, ihn zu benutzen. Type=exec beseitigt dann die gesamte Problemklasse, und Type=notify geht weiter: Es ist – zusammen mit notify-reload beziehungsweise Type=dbus für D-Bus-Dienste – der einzige verlässliche Weg, ein After= in einer abhängigen Unit „der Socket nimmt tatsächlich Verbindungen an“ heißen zu lassen und nicht „die Binärdatei wurde ausgeführt“. Alles andere ist eine hoffnungsvolle Reihenfolge.[incompat]
init 3, telinit und runlevel sind ebenfalls weg
Dieser Teil der Änderung reicht über die Server hinaus in das Muskelgedächtnis und in die Runbooks. Version 258 entfernte den Geräteknoten /dev/initctl sowie die Befehle initctl, runlevel und telinit, strich die Unterstützung für Zustandswechsel per init 3, entfernte die Units runlevel[0-6].target und beendete das Protokollieren von Runlevel-Übergängen in utmp und wtmp – weil das Konzept eines Runlevels schlicht nicht mehr existiert. Version 259 stellte die Targets hinter der Build-Option -Dcompat-sysv-interfaces=yes wieder her; die Befehle stellte sie nicht wieder her.[v258][v259]
| SysV | systemd | Anmerkung |
|---|---|---|
init 3, telinit 3 | systemctl isolate multi-user.target | telinit wurde in 258 entfernt. init gibt es weiterhin – es ist PID 1; entfallen ist die Zustandssteuerung darüber. |
init 5 | systemctl isolate graphical.target | |
init 1, telinit S | systemctl isolate rescue.target | emergency.target geht noch eine Stufe tiefer. |
init 0 / init 6 | systemctl poweroff / systemctl reboot | |
runlevel | systemctl get-default, systemctl list-units --type=target | Der Befehl wurde entfernt; ein Target ist kein Runlevel. |
Standard-Runlevel in /etc/inittab | systemctl set-default multi-user.target | /etc/inittab wird seit Jahren nicht mehr gelesen. |
chkconfig x on, update-rc.d x defaults | systemctl enable x.service | Erfordert einen [Install]-Abschnitt in der Unit. |
chkconfig x off, update-rc.d -f x remove | systemctl disable x.service | |
| (kein Gegenstück) | systemctl mask x.service | Macht die Unit unstartbar, selbst als Abhängigkeit. |
chkconfig --list, service --status-all | systemctl list-unit-files --type=service | |
service x start | systemctl start x.service | service existiert oft noch als Shim; verlassen Sie sich nicht darauf. |
# 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 --allEiner dieser Ersätze hat überhaupt kein SysV-Gegenstück und lohnt sich schon für sich genommen. systemctl mask macht eine Unit unstartbar – durch alles, auch als Abhängigkeit von etwas anderem. Das Nächste, was SysV je dazu hatte, war, das Skript zu löschen und zu hoffen, dass der Paketmanager es nicht wieder hinlegt. Es ist das richtige Werkzeug, um einen Dienst stillzulegen, den Sie noch nicht deinstallieren wollen, und um sicherzustellen, dass ein ausgemustertes Skript nicht von einem gutmeinenden Kollegen wiederbelebt wird.[systemctl]
Was Sie gewinnen und der Wrapper nie geboten hat
Man sollte ehrlich sagen: Diese Migration ist aufgezwungene Arbeit, an deren Ende kein neues Feature steht. Nehmen Sie deshalb die Entschädigung mit, denn sie ist real und kostenlos – ein eingebundenes Init-Skript konnte nichts davon nutzen. Der Wrapper führte Ihr Shell-Skript aus, und Ihr Shell-Skript führte den Daemon aus; systemd hatte keine Ahnung, was der Prozess war, konnte ihn nicht zuverlässig neu starten und ihn überhaupt nicht einschränken. Eine native Unit bekommt all das durch das Hinzufügen von Zeilen:[exec][resctl]
- Eine echte Überwachungsschleife.
Restart=on-failurezusammen mitRestartSec=, abgesichert durchStartLimitIntervalSec=, damit eine Absturzschleife irgendwann aufhört, statt einen Kern dauerhaft zu belegen. Der generierte Wrapper setzte für jedes je erzeugte Skript festRestart=no. - Isolation im Dateisystem.
ProtectSystem=strictmacht das gesamte Dateisystem schreibgeschützt, bis auf das, was Sie benennen,PrivateTmp=yesgibt dem Dienst ein eigenes/tmp, undStateDirectory=,RuntimeDirectory=sowieConfigurationDirectory=erzeugen, besitzen und räumen jene Verzeichnisse auf, die Ihr Skript früher von Hand permkdir -pangelegt hat. - Rechtebeschränkung, die tatsächlich hält.
User=,NoNewPrivileges=yesund ein explizitesCapabilityBoundingSet=ersetzensuundstart-stop-daemon --chuid– und anders als diese gelten sie für jeden Prozess, den der Dienst startet, dauerhaft, einschließlich derer, die ein kompromittierter Daemon zu starten versucht. - Einschränkung von Syscalls und Namespaces.
SystemCallFilter=@system-service,RestrictAddressFamilies=,RestrictNamespaces=undMemoryDenyWriteExecute=yessind eine seccomp-Policy, die Sie in vier Zeilen schreiben. Ein Shell-Äquivalent dazu gab es zu keinem Preis. - Ressourcenkontrolle pro Dienst.
MemoryMax=,CPUQuota=,TasksMax=undIOWeight=gelten für die cgroup der Unit – also für den Daemon und alles, was er forkt. Genau das konnte ein Skript auf PID-Datei-Basis niemals einfangen. - Logs, die irgendwo ankommen. Standardausgabe und Standardfehler gehen mit angehängtem Unit-Namen ins Journal, sodass
journalctl -ufunktioniert, ohne dass der Daemon wissen muss, was syslog ist – und ohne dass eine Logdatei still eine Partition füllt, weil niemand eine Rotationsregel geschrieben hat.
Fügen Sie das aber nicht blind alles hinzu. Lassen Sie systemd-analyze security über die Unit laufen und behandeln Sie das Ergebnis als Aufgabenliste statt als Note: Es benennt jede Direktive, die Sie nicht gesetzt haben, und was sie bringen würde. Ziehen Sie in kleinen Schritten an, starten Sie neu und lesen Sie das Journal – der Fehlermodus übertriebener Härtung ist ein Dienst, der einwandfrei startet und drei Stunden später eine Datei nicht öffnen kann. Das ist erheblich unangenehmer als ein Dienst, der gleich gar nicht erst startet.[analyze]
Migration prüfen statt hoffen
Prüfen Sie nicht, indem Sie auf systemctl status schauen und Grün sehen. Ein eingebundenes Skript zeigte auch Grün. Prüfen Sie die vier Dinge, die tatsächlich anders waren: dass die Definition eine Datei ist, die Sie kontrollieren, und keine generierte; dass kein SourcePath auf ein Init-Skript zurückverweist; dass die Unit explizit aktiviert ist und nicht durch übrig gebliebene Verdrahtung; und dass systemd die PID des Hauptprozesses kennt.[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 $rcStarten Sie danach neu, denn die gesamte Fehlerklasse, die diese Migration einführt, sind Reihenfolgefehler – und Reihenfolgefehler sind auf einem laufenden System unsichtbar, auf dem jede Abhängigkeit ohnehin schon oben ist. Lesen Sie journalctl -b -u für jede umgestellte Unit und systemd-analyze blame für den Bootvorgang insgesamt. Ein Dienst, der früher unter einem nummerierten Symlink an Position 20 startete und nun zu früh startet, wird nicht scheitern – er wird es erneut versuchen, oder mit leerer Konfiguration starten, oder binden, bevor eine Schnittstelle eine Adresse hat.
Wenn Sie Software auf fremde Server ausliefern
Wenn Sie Software vertreiben, die andere Leute installieren, ist das schon länger nicht mehr optional. Der Generator protokollierte seit 255 für jedes eingebundene Skript eine Warnung, die sich direkt an Sie richtete: please update package to include a native systemd unit file. Unter systemd 260 gibt es diese Warnung nicht mehr, weil es nichts mehr zu warnen gibt – das Skript wird installiert, und danach führt es niemand aus.[gensrc]
Was ein Paket jetzt ausliefern muss, ist eine Unit-Datei in /usr/lib/systemd/system/ mit einem ordentlichen [Install]-Abschnitt. Denn die dritte entfernte Komponente, systemd-sysv-install, war genau der Hook, über den systemctl enable bei skriptbasierten Diensten auf chkconfig oder update-rc.d durchreichen konnte. Ohne ihn ist das Aktivieren eines Dienstes ausschließlich eine Frage der Symlinks, die [Install] beschreibt. Behalten Sie das Init-Skript im Paket, wenn Sie noch ältere Distributionen unterstützen – eine native Unit und ein Skript können koexistieren, und die Unit gewinnt überall dort, wo systemd neu genug ist, um relevant zu sein.[unit]
Die Reihenfolge, in der Sie vorgehen sollten
Es gibt keine Variante, in der es später billiger wird. Der Aufwand ist proportional zur Anzahl der Skripte, und die schrumpft nicht; das Zeitfenster bestimmt der Release-Kalender von jemand anderem. Was variiert, ist die Vorwarnzeit – die erste Entscheidung ist deshalb, in welcher dieser Situationen Sie sich befinden:[ubuntu2604]
| Situation | Wie viel Zeit Sie haben | Das ist zu tun |
|---|---|---|
| Rolling-Distribution oder systemd 260 bereits installiert | Keine – es ist schon weg | Aus Backups oder Configuration Management inventarisieren, dann umwandeln. Die Skripte auf der Platte sind wirkungslos, verraten Ihnen aber noch, was existierte. |
| Ubuntu Interim-Serie (26.04 → 26.10) | Wochen | Jetzt inventarisieren und die generierten Units sichern, solange 259 noch läuft. Vor dem Oktober-Release umwandeln. |
| Ubuntu 26.04 LTS, Verbleib auf LTS | Bis zum nächsten LTS | Kein Notfall, aber das Journal warnt bereits. Bei Gelegenheit umwandeln, beginnend mit allem Herrenlosen. |
| Debian stable, RHEL 9 oder 10 | Bis zur nächsten Hauptversion | Der Generator ist vorhanden und arbeitet; auf Debian 13 und RHEL 10 warnt er bereits pro Skript. Machen Sie die Inventur trotzdem – sie kostet einen Befehl und altert gut. |
| Sie paketieren Software für andere | Keine | Liefern Sie ab sofort eine Unit-Datei mit [Install]-Abschnitt aus. Die Upgrades Ihrer Anwender liegen nicht in Ihrer Hand. |
| Eine Appliance oder ein Vendor-Agent, den Sie nicht ändern können | Wie beim Host | Schreiben Sie die Unit selbst und maskieren Sie das Skript des Herstellers – oder sprechen Sie ihn an, bevor sein nächstes Release Sie hängen lässt. |
- Inventarisieren Sie diese Woche den gesamten Bestand mit der Journal-Abfrage von oben und speichern Sie die Ausgabe. Zwei Spalten – Hostname und Skriptpfad – genügen, um den Aufwand abzuschätzen und die Maschinen zu erwischen, an die sich niemand erinnert.
- Sichern Sie für jedes Skript die generierte Unit mit
systemctl catin ein Verzeichnis, das Sie aufbewahren. Das ist ein rein lesender Vorgang, er dauert Minuten, und nach dem Upgrade ist die Information nicht wiederherstellbar. - Sortieren Sie auf drei Stapel: löschen, ersetzen, neu schreiben. Ein überraschend großer Anteil dessen, was Sie finden, ist ein Dienst für etwas, das vor Jahren stillgelegt wurde. Ihn zu löschen ist eine vollständige Migration und kostet keine Zeit.
- Wandeln Sie die einfachen Fälle zuerst um – alles, was ohnehin upstream paketiert ist, hat mit hoher Wahrscheinlichkeit eine offizielle Unit, und das aktuelle Paket zu installieren ist schneller und korrekter, als selbst eine zu schreiben.
- Führen Sie die Umstellung durch, solange der Generator noch existiert, Dienst für Dienst, auf einem System, auf dem das Skript noch vorhanden ist. Ein Rollback heißt dann: eine Datei entfernen und neu laden – und nicht unter Druck ein Backup zurückspielen.
- Neu starten und den Bootvorgang lesen, pro Host, bevor Sie irgendetwas davon als erledigt bezeichnen. Danach maskieren Sie die ausgemusterten Units, damit sie nichts zurückholt.
In dasselbe Wartungsfenster fällt üblicherweise auch der Rest: das Server-Upgrade von Ubuntu 24.04 auf 26.04 behandelt das Distributions-Upgrade, das Ihnen den Generator unter den Füßen wegnimmt, systemd-Timer im Vergleich zu cron ist die andere Hälfte derselben Aufräumaktion – denn eine Maschine mit SysV-Skripten hat fast immer auch Crontabs, die eigentlich Units sein sollten –, und was beim Umstieg auf Docker Engine 29 bricht befasst sich mit der Container-Laufzeitumgebung, die im selben Zeitraum und aus verwandten Gründen ihre eigenen Voreinstellungen geändert hat.
Häufige Fragen
Kann ich den sysv-generator unter systemd 260 einfach nachinstallieren?
Nein – und die Umgehungen, die so aussehen, als könnten sie funktionieren, sind schlimmer als die Migration selbst. Der Generator wurde aus dem Quellbaum gelöscht und nicht hinter einem Schalter deaktiviert; es gibt also kein Paket zum Installieren und keine Option zum Setzen. Die drei verbliebenen abgekündigten meson-Optionen (-Drc-local=, -Dsysvinit-path=, -Dsysvrcnd-path=) beeinflussen nur Pfade, nicht das Vorhandensein des Codes. Sie könnten prinzipiell die Generator-Binärdatei aus einem 259-Build nach /usr/lib/systemd/system-generators/ kopieren – dann betreiben Sie aber einen ungewarteten Generator gegen einen Manager, mit dem er nie getestet wurde, und das nächste systemd-Update überschreibt ihn stillschweigend. Die Unit-Datei zu schreiben geht schneller, als das einmal zu debuggen.
Funktioniert mein Init-Skript noch, wenn ich es von Hand aufrufe?
Ja. Am Skript hat sich nichts geändert; es ist ein gewöhnliches Shell-Skript, und /etc/init.d/x start tut genau das, was es immer tat. Weg ist die automatische Übersetzung in einen Dienst – und das heißt: Es startet nicht beim Booten, systemctl weiß nichts davon, niemand überwacht oder startet den Prozess neu, und das Herunterfahren der Maschine beendet ihn nicht sauber. Diese Kombination ist schlimmer, als sie klingt, denn ein Skript, das sich von Hand noch aufrufen lässt, fühlt sich nicht kaputt an – also behilft man sich mit einem @reboot-Eintrag in der crontab, und das Problem ist für ein Jahr leise.
Wie finde ich heraus, welche meiner Server betroffen sind, ohne mich auf allen anzumelden?
Fragen Sie das Journal nach der strukturierten Warnung ab. Ab systemd 255 erzeugt jedes eingebundene Skript einen Logeintrag mit MESSAGE_ID=a8fa8dacdb1d443e9503b8be367a6adb und zwei eigenen Feldern, SYSVSCRIPT= und UNIT=. Wenn Sie Journals zentral einsammeln, ist das eine einzige Abfrage über den gesamten Bestand. Falls nicht, lassen Sie journalctl -b MESSAGE_ID=a8fa8dacdb1d443e9503b8be367a6adb über das laufen, womit Sie Befehle auf vielen Hosts ausführen. Unter systemd 254 und älter erfolgt die Einbindung lautlos; dort fragen Sie stattdessen jede Unit nach ihrem FragmentPath und SourcePath, so wie es das Inventarskript oben tut.
Was ist der Unterschied zwischen Type=simple, Type=exec, Type=forking und Type=notify?
Type=simple betrachtet den Dienst als gestartet, sobald systemd geforkt hat – also bevor die Binärdatei überhaupt erfolgreich ausgeführt wurde. Type=exec wartet, bis die Binärdatei tatsächlich ausgeführt wurde, wodurch eine fehlende Datei oder ein falsches User= als Startfehler auffällt statt als rätselhafter Sofortabbruch – für einen Vordergrund-Daemon der bessere Standard. Type=forking ist für Daemons gedacht, die sich selbst in den Hintergrund schicken, und erwartet ein PIDFile=; genau das benutzte der sysv-generator, weil er keine Wahl hatte. Type=notify wartet, bis der Daemon sd_notify(READY=1) aufruft – als einziger der vier bedeutet hier eine After=-Abhängigkeit, dass der Dienst wirklich bereit ist. Wandeln Sie nach Type=exec um; nehmen Sie Type=notify, wenn der Daemon es unterstützt.
Ist /etc/rc.local wirklich weg oder nur abgekündigt?
Weg, ab systemd 260. systemd-rc-local-generator und die von ihm hereingezogene Unit rc-local.service wurden beide entfernt. Die Datei liegt weiterhin in /etc, weiterhin ausführbar, und niemand ruft sie je auf. Beachten Sie, dass der Pfad eine Compile-Zeit-Einstellung war und zwischen Distributionen variierte – manche nutzten /etc/rc.d/rc.local –, prüfen Sie also, womit Ihr Generator tatsächlich konfiguriert war, bevor Sie annehmen, alle gefunden zu haben.
Meine Unit meldet active (running), aber den Prozess gibt es nicht. Was habe ich falsch gemacht?
Mit hoher Wahrscheinlichkeit Type=forking mit einer PID-Datei, die systemd im entscheidenden Moment nicht lesen konnte – oder Type=simple bei einem Daemon, der forkt. Führen Sie systemctl show -p Type -p MainPID -p PIDFile --value ihrdienst.service aus: Steht MainPID auf 0, während der Daemon läuft, hat systemd ihn aus den Augen verloren und keine der Überwachungsfunktionen greift. Die Lösung ist, das Forken zu unterbinden – finden Sie den Vordergrund-Schalter und nehmen Sie Type=exec. Wenn das wirklich nicht geht, stellen Sie sicher, dass PIDFile= ein absoluter Pfad unterhalb von /run ist und der Daemon ihn schreibt, bevor der Elternprozess sich beendet.
Soll ich das Init-Skript behalten, nachdem ich die Unit geschrieben habe?
Behalten Sie es während des Umstellungsfensters, entfernen Sie es danach. Solange beides existiert, gewinnt die native Unit – der Generator übersprang ausdrücklich jedes Skript, für das bereits eine gleichnamige Unit existierte –, das Nebeneinander ist also gefahrlos und macht ein Rollback trivial. Lassen Sie es aber dauerhaft liegen, haben Sie zwei Definitionen desselben Dienstes, von denen eine eine Falle für denjenigen ist, der das um drei Uhr nachts debuggt. Verschieben Sie es in ein Ausmusterungsverzeichnis, sobald der Host sauber neu gestartet ist, und entfernen Sie bei derselben Gelegenheit die Symlinks in /etc/rc*.d.
Muss ich mir unter RHEL oder Debian stable Sorgen machen?
Nicht dringend, machen Sie die Inventur aber trotzdem. RHEL 10 und Debian 13 liefern beide systemd 257 aus, beide haben also noch den Generator – und beide geben bereits die skriptweise Abkündigungswarnung aus, die mit 255 kam. Red Hats eigene Release Notes halten zusätzlich fest, dass die Unterstützung für System-V-Dienstskripte abgekündigt ist und entfernt wird. Die Änderung kommt jeweils mit der nächsten Hauptversion. Der Grund, jetzt zu handeln: Der Inventurschritt ist ein einziger, rein lesender Befehl, er ist erheblich einfacher, solange der Generator läuft und Ihnen sagt, was er einbindet, und die Alternative ist, ein herrenloses Skript mitten in einem Major-Upgrade zu entdecken – dem denkbar schlechtesten Zeitpunkt, um die Shell eines Kollegen zu reverse-engineeren.
Was ersetzt systemd-sysv-install?
Nichts, weil es nichts mehr zu tun gibt. Es war der Hook, über den systemctl enable, disable und is-enabled an das Werkzeug der Distribution durchreichten – chkconfig auf Red-Hat-Systemen, update-rc.d auf Debian-Systemen –, wenn der genannte Dienst ein Skript war und keine Unit. Ohne Skriptunterstützung ist das Aktivieren eines Dienstes vollständig eine Frage der Symlinks, die der [Install]-Abschnitt der Unit beschreibt. Hat sich Ihr Paket auf dieses Durchreichen verlassen, braucht es jetzt eine echte Unit-Datei mit einem echten [Install]-Abschnitt.
Kann ich ein Skript automatisch umwandeln statt von Hand?
Teilweise – und der beste Konverter ist genau der, der gleich entfernt wird: systemctl cat ihrdienst.service auf einem System, das den Generator noch hat, liefert Ihnen eine mechanisch korrekte Übersetzung der Abhängigkeiten, also genau den Teil, den man leicht falsch macht. Was Ihnen kein Werkzeug abnimmt, ist der Teil, auf den es ankommt: die Entscheidung, dass der Daemon aufhören soll zu forken, dass die PID-Datei verschwinden soll, dass aus start-stop-daemon --chuid ein User= wird, und welche Sandbox-Direktiven für diese konkrete Last unbedenklich sind. Behandeln Sie die generierte Unit als Spezifikation und schreiben Sie die echte daraus.
Die Ebene der Ressourcenkontrolle hat sich in denselben Releases bewegt: systemd 258 hat cgroup v1 ersatzlos entfernt, sodass jeder Host nun die vereinheitlichte Hierarchie bootet, ob das jemand wollte oder nicht. die Migration von cgroup v1 auf cgroup v2 behandelt die Bestandsaufnahme, die Umrechnung Datei für Datei und die beiden Übersetzungen, die das Verhalten ändern und nicht nur die Schreibweise.
Quellen
Jede Versionsnummer, jede Direktive und jeder Standardwert in diesem Artikel wurde aus den unten verlinkten Quellen gelesen und nicht aus anderer Berichterstattung übernommen. Wo der Code inzwischen gelöscht wurde, verweist der Link auf den letzten Git-Tag, an dem er noch existierte – damit Sie es selbst nachprüfen können.
- 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
- 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
- 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
- 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
- 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
- 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
- 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%
- 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
- 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
- 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
- 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
- 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
- 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
- systemd.service(5) — Type=, Restart=, PIDFile=, ExecReload=, RemainAfterExit= and the rest of the service options used in the unit files below
- systemd.unit(5) — Wants=, After=, Before=, the Condition family and the [Install] section that replaces update-rc.d and chkconfig
- systemd.exec(5) — User=, RuntimeDirectory=, StateDirectory= and the whole sandboxing vocabulary, none of which a wrapped init script could ever use
- 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
- 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
- systemctl(1) — enable, mask, cat, show, list-unit-files and isolate: the commands that replace chkconfig, update-rc.d, service and telinit
- systemd-analyze(1) — verify, security and blame: the three subcommands that turn this migration from a guess into something you can check
- journalctl(1) — matching on MESSAGE_ID= and on arbitrary structured fields, which is how the inventory command in this article finds every wrapped script
- daemon(7) — the difference between a SysV-style forking daemon and a new-style daemon, and upstream's own recommendation to stop forking
- systemd.resource-control(5) — the per-unit cgroup accounting and limits that come for free once a service is a real unit
- 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
- Ubuntu 26.10 release schedule — the release date that turns "eventually" into a deadline for anyone tracking the interim series
- 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
- Debian trixie systemd package — the version currently in Debian stable, for readers deciding how much runway they actually have
War das hilfreich?