systemd: Architektur, Komponenten und Konfiguration

systemd ist nicht ein Programm, sondern eine Familie von rund neunzig Binaries um einen gemeinsamen Kern: einen Prozess-Manager als PID 1, der jeden Dienst in eine eigene cgroup sperrt, Abhängigkeiten als Transaktion auflöst und alles Weitere über D-Bus delegiert. Dieser Artikel zeigt, wer zu dieser Familie gehört, was jedes Mitglied tut und welche Datei man anfasst, um es zu ändern.

1. Vier Ideen, aus denen alles Weitere folgt

Fast jede Eigenart von systemd lässt sich auf eine dieser vier Entscheidungen zurückführen. Wer sie kennt, kann den Rest ableiten.

Alles ist eine Unit

Dienst, Socket, Mountpunkt, Gerät, Timer, Ressourcen-Gruppe: alles wird als Objekt desselben Typsystems beschrieben, in derselben INI-Syntax, mit denselben Abhängigkeitsverben. Ein Mount ist keine Zeile in einer Tabelle mehr, sondern ein Ding mit Zustand, das andere Dinge starten kann.

Abhängigkeit ist nicht Reihenfolge

Requires= sagt was mitlaufen muss, After= sagt wann. Beides ist getrennt — und genau deshalb kann systemd alles parallel starten, was nicht ausdrücklich geordnet wurde. Die häufigste Fehlerursache in eigenen Units ist ein Requires= ohne das passende After=.

cgroups statt PID-Raten

Jede Unit bekommt ihre eigene Kernel-cgroup. Damit ist zuverlässig bekannt, welche Prozesse zu einem Dienst gehören — auch doppelt geforkte. Daraus folgen sauberes Beenden, Ressourcenlimits und Verbrauchszahlen, ohne PID-Dateien.

D-Bus als Rückgrat

PID 1 tut selbst wenig Fachliches. Uhrzeit, Hostname, Netz, Sitzungen und Namensauflösung liegen in eigenen kleinen Daemons, die eine D-Bus-Schnittstelle anbieten. Die *ctl-Werkzeuge sind bloß Clients dieser Schnittstellen.

2. Die Landschaft auf einen Blick

Links die Werkzeuge, mit denen man redet. In der Mitte der Manager, der als einziger Prozesse startet. Rechts die Fachdaemons, die selbst wieder ganz normale Units sind. Unten der cgroup-Baum, in den jeder gestartete Prozess einsortiert wird.

Architekturdiagramm: der Kernel oben, systemd als PID 1 in der Mitte, links die Steuerwerkzeuge über D-Bus, rechts die eigenen Daemons, unten der cgroup-Baum.

Der einzige Prozess, der Dienste startet, ist PID 1. Alles andere — auch systemctl — schickt nur Aufträge über D-Bus dorthin und wartet auf das Ergebnis. Deshalb bricht der Bootvorgang nicht ab, wenn dbus-daemon noch nicht läuft: PID 1 hört zusätzlich auf einem privaten Socket unter /run/systemd/private.

3. PID 1 und der Manager pro Benutzer

Dasselbe Binary läuft in zwei Rollen: einmal als Systemmanager mit UID 0, und einmal pro angemeldetem Benutzer als dessen persönlicher Manager. Beide haben eigene Unit-Suchpfade und eigene Konfigurationsdateien.

RolleKonfigurationUnit-VerzeichnisseAnwenden
systemd
(PID 1)
/etc/systemd/system.conf
/etc/systemd/system.conf.d/*.conf
/etc/systemd/system/
/run/systemd/system/
/usr/lib/systemd/system/
Units: systemctl daemon-reload
system.conf: systemctl daemon-reexec
systemd --user/etc/systemd/user.conf
~/.config/systemd/user.conf
~/.config/systemd/user/
/etc/systemd/user/
/usr/lib/systemd/user/
Gestartet als user@UID.service.
Ohne Login laufen lassen:
loginctl enable-linger <user>

Zusätzlich steuerbar ist PID 1 über die Kernel-Kommandozeile: systemd.unit=, systemd.mask=, systemd.log_level=. systemctl selbst startet nichts, sondern übersetzt Unterbefehle in Methodenaufrufe auf org.freedesktop.systemd1. Mit --user, --machine= und --host= wählt man den Manager, den man anspricht.

Die Abhängigkeitsverben — der Teil, an dem eigene Units scheitern

Es gibt zwei unabhängige Achsen: wer muss mitlaufen und in welcher Reihenfolge. Wer nur die erste setzt, bekommt einen Dienst, der parallel zu seiner Voraussetzung startet und mit einer Verbindungsfehlermeldung stirbt.

DirektiveAchseBedeutung
Wants=ExistenzZieht die andere Unit mit hoch. Scheitert sie, läuft man trotzdem weiter. Der Normalfall.
Requires=ExistenzZieht mit hoch; scheitert sie, wird auch diese Unit gestoppt. Ohne After= nutzlos scharf.
Requisite=ExistenzWie Requires=, startet die andere Unit aber nicht — sie muss schon aktiv sein, sonst sofortiger Fehlschlag.
BindsTo=ExistenzWie Requires=, aber gekoppelt auch an unerwartetes Wegfallen — typisch bei .device-Units.
PartOf=ExistenzStop und Restart der anderen Unit ziehen diese mit, der Start nicht. Klassisch für Instanzen unter einem Sammel-Target.
Conflicts=ExistenzDas Starten dieser Unit stoppt die andere. So schließen sich rescue.target und der Normalbetrieb aus.
Before=
After=
ReihenfolgeRein zeitlich, ohne jede Aussage darüber, ob die andere Unit überhaupt läuft. Fast immer paarweise mit einem Existenzverb nötig.
WantedBy=
RequiredBy=
InstallationSteht in [Install] und wirkt erst bei systemctl enable — dann wird ein Symlink im .wants/-Verzeichnis des Ziels angelegt.
Diagnose statt Raten. systemctl list-dependencies foo.service zeigt den Baum nach unten, --reverse nach oben, und --after beziehungsweise --before zeigen ausschließlich die Reihenfolgekanten — genau die Ansicht, die man bei „startet zu früh“ braucht.

4. Die elf Unit-Typen

Die Dateiendung bestimmt den Typ. Jeder Typ hat neben den gemeinsamen Abschnitten [Unit] und [Install] einen eigenen Abschnitt mit den typspezifischen Direktiven.

EndungAbschnittWofürWoher es meist kommt
.service[Service]Ein Prozess oder eine Prozessgruppe mit Lebenszyklus, Restart-Politik und Sandbox.Paket oder selbst geschrieben
.socket[Socket]Lauschender Socket, der bei der ersten Verbindung den passenden Dienst startet — Socket-Aktivierung.Paket; Gegenstück heißt gleich mit .service
.target—Reiner Synchronisationspunkt ohne eigene Prozesse. Der Ersatz für Runlevel.Paket; eigene Targets sind legitim
.timer[Timer]Zeit- oder ereignisgesteuerter Auslöser für eine Unit. Der cron-Ersatz, mit Journal und Nachholen.Paket oder selbst geschrieben
.mount[Mount]Ein Mountpunkt als verwaltetes Objekt. Der Name ist der escapte Pfad.Generiert aus /etc/fstab
.automount[Automount]Mountet erst beim ersten Zugriff, mit optionalem Leerlauf-Timeout.x-systemd.automount in fstab
.swap[Swap]Swap-Gerät oder -Datei.Generiert aus /etc/fstab
.path[Path]Startet eine Unit, sobald ein Pfad existiert, sich ändert oder nicht mehr leer ist (inotify).Selbst geschrieben
.device[Unit]Spiegel eines udev-Geräts. Nicht schreibbar — entsteht durch SYSTEMD_WANTS in udev-Regeln.udev, automatisch
.slice[Slice]Knoten im cgroup-Baum, an dem Ressourcenlimits für eine ganze Gruppe von Units hängen.Eingebaut; eigene sind sinnvoll
.scope[Scope]Gruppe fremdgestarteter Prozesse, die systemd nur noch verwaltet — Login-Sitzungen, VMs, systemd-run --scope.Nur zur Laufzeit erzeugt
Templates. Ein @ vor der Endung macht eine Unit zur Vorlage: getty@.service wird mit systemctl start getty@tty3 instanziiert, und %i beziehungsweise %I in der Datei stehen für den Teil hinter dem @. Weitere Platzhalter: %n Unitname, %H Hostname, %t Laufzeitverzeichnis, %S Zustandsverzeichnis. Pfade als Instanznamen müssen mit systemd-escape -p kodiert werden.

5. Die Boot-Kette

Die obere Reihe ist echte Sequenz — jede Stufe übergibt an die nächste. Die untere Reihe sieht auch nach Sequenz aus, ist aber nur eine Ordnungsbeziehung: innerhalb jeder Stufe läuft alles parallel, was nicht ausdrücklich geordnet wurde.

Boot-Kette: UEFI, Bootloader, Kernel mit initrd, systemd im initrd und switch-root; danach die Target-Kette von local-fs bis graphical.target.

Warum das praktisch zählt: Ein Dienst, der ins Netz muss, gehört hinter network-online.target — nicht hinter network.target, das nur besagt, dass die Netzwerkverwaltung gestartet wurde. Und wer den Bootvorgang vermessen will, nimmt systemd-analyze critical-chain: das zeigt die Ordnungskette, die die Bootzeit tatsächlich bestimmt hat, statt nur die langsamsten Units aus systemd-analyze blame.

Notausstieg. An der Kernel-Kommandozeile im Bootloader: systemd.unit=rescue.target (einbenutzer, mit Dateisystemen), systemd.unit=emergency.target (nur Root, read-only), systemd.mask=<unit> für einen einzelnen Störenfried, oder init=/bin/bash, wenn systemd selbst nicht mehr hochkommt. Für Ausführlichkeit: systemd.log_level=debug und systemd.log_target=console.

6. Komponenten-Katalog mit allen Konfigurationspfaden

Alle *.conf-Dateien unter /etc/systemd/ folgen demselben Muster: Hauptdatei plus ein gleichnamiges .conf.d/-Verzeichnis daneben, dessen Schnipsel obendrauf gelegt werden.

Protokollierung, Sitzungen und Benutzer

KomponenteAufgabeKonfigurationWerkzeug
systemd-
journald
Sammelt Meldungen aus fünf Quellen (Unit-stdout/stderr, /dev/log, /dev/kmsg, native API, Audit) in einem binären, indizierten Format mit strukturierten Feldern./etc/systemd/journald.conf
/etc/systemd/journald.conf.d/
Ablage: /run/log/journal/
oder /var/log/journal/
Storage=, SystemMaxUse=,
MaxRetentionSec=,
ForwardToSyslog=
journalctl
systemd-cat
systemd-
logind
Verwaltet Sitzungen, Seats und Benutzer. Legt pro Anmeldung eine session-N.scope an, vergibt Gerätezugriff an die aktive Sitzung, behandelt Deckel- und Einschalttaste./etc/systemd/logind.conf
/etc/systemd/logind.conf.d/
HandleLidSwitch=,
HandlePowerKey=, IdleAction=,
KillUserProcesses=, RemoveIPC=
Eintritt: pam_systemd.so
loginctl
systemd-inhibit
systemd-
homed
Selbsttragende Heimatverzeichnisse: Benutzerdatensatz und Daten zusammen in einem LUKS-Abbild, das beim Login entschlüsselt wird./etc/systemd/homed.conf
/home/<user>.home
braucht nss-systemd und
pam_systemd_home.so
homectl
systemd-
userdbd
Vereinheitlicht Benutzerdatensätze als JSON — aus /etc/passwd, aus homed, aus DynamicUser=./etc/userdb/
/run/userdb/
/usr/lib/userdb/
userdbctl
Persistentes Journal ist kein Schalter, sondern ein Verzeichnis. Bei Storage=auto schreibt journald genau dann nach /var/log/journal/, wenn dieses Verzeichnis existiert. Anlegen und systemctl restart systemd-journald genügt. Umgekehrt räumt journalctl --vacuum-size=500M oder --vacuum-time=30d sofort auf, ohne auf die Rotation zu warten.

Netz, Namen und Zeit

KomponenteAufgabeKonfigurationWerkzeug
systemd-
networkd
Deklarative Netzkonfiguration. .link benennt und konfiguriert die Schnittstelle, .netdev erzeugt virtuelle Geräte (Bridge, VLAN, Bond, WireGuard), .network weist Adressen und Routen zu./etc/systemd/network/*.link
/etc/systemd/network/*.netdev
/etc/systemd/network/*.network
lexikalisch sortiert, die erste
passende [Match] gewinnt
networkctl status
networkctl reload
networkctl
reconfigure <if>
systemd-
resolved
Caching-Resolver mit Stub auf 127.0.0.53:53, pro Link eigene DNS-Server, Split-DNS über Suchdomänen, optional DNSSEC und DNS-over-TLS./etc/systemd/resolved.conf
/etc/systemd/resolved.conf.d/
/etc/resolv.conf → /run/systemd/
resolve/stub-resolv.conf
DNS=, FallbackDNS=, Domains=,
DNSSEC=, DNSOverTLS=
resolvectl status
resolvectl query
resolvectl
flush-caches
systemd-
timesyncd
Schlanker SNTP-Client — hält die lokale Uhr nach, stellt aber keine Zeit bereit und kann nicht als Server dienen. Für Zeitserver oder strenge Genauigkeit nimmt man chrony./etc/systemd/timesyncd.conf
/etc/systemd/timesyncd.conf.d/
NTP=, FallbackNTP=,
PollIntervalMinSec=
timedatectl
timesync-status
timedatectl
set-ntp true
hostnamed
localed
timedated
Drei winzige, bus-aktivierte Dienste, deren einziger Zweck es ist, klassische /etc-Dateien über eine geprüfte Schnittstelle schreibbar zu machen — damit auch unprivilegierte Werkzeuge sie über polkit ändern können./etc/hostname, /etc/machine-info
/etc/locale.conf, /etc/vconsole.conf
/etc/localtime, /etc/adjtime
hostnamectl
localectl
timedatectl

Geräte, Boot und Speicher

KomponenteAufgabeKonfigurationWerkzeug
systemd-
udevd
Hört auf Kernel-uevents, legt Gerätedateien an, vergibt Namen und Symlinks, setzt Rechte und kann Units anstossen. Die berechenbaren Schnittstellennamen wie enp3s0 kommen von hier, nicht von networkd./etc/udev/rules.d/*.rules
/usr/lib/udev/rules.d/*.rules
/etc/udev/udev.conf
/etc/udev/hwdb.d/
udevadm control
--reload
udevadm trigger
udevadm info -a -n
udevadm monitor
systemd-
boot
Minimaler EFI-Bootmanager. Kein eigenes Dateisystemwissen, keine Skriptsprache — er liest Einträge von der EFI-Partition und ruft den Kernel auf.<ESP>/loader/loader.conf
<ESP>/loader/entries/*.conf
/etc/kernel/cmdline
/etc/kernel/install.d/
bootctl status
bootctl list
kernel-install
ukify
systemd-
cryptsetup
systemd-
repart
Verschlüsselte Geräte werden aus /etc/crypttab zu generierten Units; repart vergrößert oder erzeugt Partitionen deklarativ beim Boot./etc/crypttab
/etc/veritytab
/etc/repart.d/*.conf
systemd-cryptenroll
(TPM2, FIDO2)
systemd-repart

Isolation und Ressourcen

KomponenteAufgabeKonfigurationWerkzeug
systemd-nspawn
systemd-machined
nspawn startet einen Container aus einem Verzeichnisbaum oder Image, machined führt Buch über laufende Container und VMs und macht sie für systemctl -M adressierbar./var/lib/machines/
/etc/systemd/nspawn/<name>.nspawn
systemd-nspawn@<name>.service
machinectl start
machinectl shell
systemd-oomdGreift ein, bevor der Kernel-OOM-Killer zuschlägt: beobachtet den PSI-Druck ganzer cgroups und beendet gezielt die verursachende Gruppe, nicht den Prozess mit dem höchsten Score./etc/systemd/oomd.conf
pro Unit:
ManagedOOMMemoryPressure=
ManagedOOMSwap=
oomctl
systemd-portabled
systemd-sysext
Zwei Wege, Software als Image auszuliefern: portable Dienste hängen ein Image ein und erzeugen daraus Units mit fester Sandbox; Systemerweiterungen legen sich per overlayfs über /usr, ohne es zu verändern./var/lib/portables/
/var/lib/extensions/
portablectl attach
systemd-sysext merge

Einmal-Dienste beim Boot

Diese laufen einmal durch, hinterlassen einen Zustand und beenden sich. Alle folgen demselben Muster: ein Verzeichnis mit Schnipseln unter /usr/lib/ für Pakete und unter /etc/ für den Administrator, wobei eine gleichnamige Datei in /etc/ die des Pakets vollständig ersetzt.

WerkzeugVerzeichnisseAufgabe
systemd-tmpfiles/etc/tmpfiles.d/
/usr/lib/tmpfiles.d/
Legt Verzeichnisse, Dateien, Symlinks und Gerätedateien an, setzt Rechte, räumt Altes nach Alter weg. Ersetzt die halbe rc.local.
systemd-sysusers/etc/sysusers.d/
/usr/lib/sysusers.d/
Deklariert Systembenutzer und -gruppen, die beim Boot angelegt werden, statt sie im Paket-Postinst zu erzeugen.
systemd-sysctl/etc/sysctl.d/
/usr/lib/sysctl.d/
Setzt Kernel-Parameter. /etc/sysctl.conf wird weiterhin gelesen, gilt aber als Altlast.
systemd-
modules-load
/etc/modules-load.d/
/usr/lib/modules-load.d/
Lädt Kernelmodule, die nicht automatisch kommen. Optionen dazu weiterhin in /etc/modprobe.d/.
systemd-binfmt/etc/binfmt.d/Registriert Interpreter für fremde Binärformate — die Grundlage für qemu-user und Cross-Builds.
systemd-coredump/etc/systemd/coredump.confHängt sich in kernel.core_pattern, legt Abstürze komprimiert ab und macht sie über coredumpctl auffindbar.
systemd-
random-seed
/var/lib/systemd/random-seedRettet Entropie über den Reboot — wichtig auf VMs, die sonst früh im Boot beim Zufall blockieren.
systemd-firstboot/etc/machine-idFragt beim allerersten Start Zeitzone, Locale, Hostname und root-Passwort ab — oder bekommt sie per Credential vorgesetzt.

7. Generatoren — die unsichtbare Übersetzungsschicht

Vor jedem daemon-reload ruft PID 1 alle ausführbaren Dateien in den Generator-Verzeichnissen auf. Jeder darf Units nach /run/systemd/generator*/ schreiben. So werden Formate zu Units, die nie als Unit gedacht waren — und deshalb findet man eine .mount-Unit, die man nirgends angelegt hat.

GeneratorÜbersetztErgebnis
systemd-fstab-
generator
/etc/fstab und die Kernel-Parameter root=, resume=.mount-, .automount- und .swap-Units
systemd-cryptsetup-
generator
/etc/crypttabsystemd-cryptsetup@<name>.service
systemd-getty-
generator
Konsolen aus /sys und console=getty@- und serial-getty@-Instanzen
systemd-gpt-auto-
generator
GPT-Partitionstypen (Discoverable Partitions)Mounts für /home, /srv, ESP, Swap — ganz ohne fstab
systemd-sysv-
generator
alte Init-Skripte in /etc/init.d/Kompatibilitäts-Units in /run/systemd/generator.late/
systemd-debug-
generator
Kernel-Parameter systemd.mask=, systemd.wants=, systemd.debug-shellMaskierungen und Zusatz-Units für genau diesen Boot
Eigene Generatoren liegen in /etc/systemd/system-generators/ und müssen ausführbar sein. Sie laufen sehr früh, ohne Netz, mit strengem Timeout und ohne die Möglichkeit, andere Units zu starten — sie dürfen ausschließlich Dateien schreiben. systemd-analyze unit-paths zeigt, wo ihre Ausgabe in der Rangfolge landet.

8. Konfigurationsmodell und Drop-ins

Das ganze Modell beruht auf einer Trennung: /usr/lib/ gehört der Distribution und wird bei jedem Update überschrieben, /etc/ gehört dem Administrator und wird nie angefasst, /run/ gilt nur bis zum Neustart. Dazu kommt ein zweiter Mechanismus, der oft übersehen wird — Drop-ins.

Konfigurations-Präzedenz: links die drei Verzeichnisse für die Unit-Datei, von denen nur das höchstpriorisierte gilt; rechts die drei Drop-in-Verzeichnisse, die alle zusammengeführt werden.

Die zwei Mechanismen verhalten sich gegensätzlich, und das ist die häufigste Verwirrung: bei der Unit-Datei gewinnt genau eine und verdeckt die anderen vollständig; bei Drop-ins werden alle gefundenen Schnipsel aus allen drei Ebenen angewandt, sortiert nach ihrem Dateinamen. Trägt ein Drop-in denselben Dateinamen wie eines aus /usr/lib/, ersetzt die Fassung aus /etc/ es punktgenau.

Der Weg, den man in der Praxis nimmt

# Nie die Paketdatei editieren - ein Update überschreibt sie kommentarlos.
# Stattdessen ein Drop-in anlegen; der Editor öffnet eine leere Datei:
systemctl edit nginx.service          # -> /etc/systemd/system/nginx.service.d/override.conf
systemctl edit --drop-in=limits nginx.service   # eigener Dateiname statt override.conf
systemctl edit --runtime nginx.service          # nur bis zum Reboot, nach /run
systemctl edit --full nginx.service             # doch die ganze Datei - Kopie nach /etc

# Ergebnis kontrollieren
systemctl cat nginx.service          # Basisdatei + alle Drop-ins, in Reihenfolge
systemctl show nginx.service -p ExecStart -p MemoryMax
systemd-analyze verify /etc/systemd/system/eigen.service

# Was hat systemd überhaupt geladen und woher?
systemd-analyze unit-paths            # alle Suchpfade in Rangfolge
systemd-analyze cat-config systemd/journald.conf   # dasselbe für *.conf.d

# Nach jeder Änderung an einer Unit-Datei:
systemctl daemon-reload                # neu einlesen
systemctl restart nginx.service        # erst das wendet sie auf den Prozess an
Die Falle bei Listen. Direktiven, die mehrfach vorkommen dürfen — ExecStart=, Environment=, After=, ReadWritePaths= — ergänzen im Drop-in, sie ersetzen nicht. Wer einen anderen Startbefehl will, schreibt erst ExecStart= ohne Wert, um die Liste zu leeren, und dann die neue Zeile. Bei Type=oneshot sind mehrere ExecStart= übrigens ausdrücklich erlaubt und laufen nacheinander.

9. Eine Unit-Datei, Zeile für Zeile

Drei Abschnitte, jeder mit einer klaren Zuständigkeit: [Unit] beschreibt Beziehungen zu anderen Units, der typspezifische Abschnitt beschreibt das Verhalten, [Install] beschreibt nur, was beim enable passieren soll.

# /etc/systemd/system/backup-export.service

[Unit]
Description=Nächtlicher Export nach S3
Documentation=https://wiki.intern/backup-export
After=network-online.target postgresql.service   # Reihenfolge ...
Wants=network-online.target                       # ... und wer mitkommen soll
Requires=postgresql.service                       # harte Kopplung: DB weg => wir auch
ConditionPathExists=/srv/export                   # Condition: still überspringen ...
AssertPathIsDirectory=/srv/export                 # ... Assert: laut scheitern
StartLimitIntervalSec=300
StartLimitBurst=3                                 # mehr als 3 Fehlstarts in 5 min => Aufgabe

[Service]
Type=oneshot                # simple | exec | forking | oneshot | notify | dbus | idle
User=backup
Group=backup
WorkingDirectory=/srv/export
EnvironmentFile=-/etc/default/backup-export        # führendes "-": darf fehlen
Environment="LC_ALL=C"
ExecStartPre=/usr/local/bin/export-check
ExecStart=/usr/local/bin/export-run --target=s3
ExecStopPost=/usr/local/bin/export-cleanup         # läuft auch nach Fehlschlag
TimeoutStartSec=30min
Restart=on-failure                                 # no | on-failure | always | on-abnormal
RestartSec=30

# Verzeichnisse, die systemd anlegt, aufräumt und rechtemässig dem User gibt:
RuntimeDirectory=backup-export        # /run/backup-export
StateDirectory=backup-export          # /var/lib/backup-export
LogsDirectory=backup-export           # /var/log/backup-export
CacheDirectory=backup-export          # /var/cache/backup-export

# Sandbox - Details im Abschnitt "Härten"
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
NoNewPrivileges=yes
ReadWritePaths=/srv/export

[Install]
WantedBy=multi-user.target        # wirkt erst bei "systemctl enable"

Der zugehörige Timer — der cron-Ersatz

# /etc/systemd/system/backup-export.timer
# aktivieren mit: systemctl enable --now backup-export.timer

[Unit]
Description=Startet den Export täglich um 02:30

[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true              # verpasste Läufe nachholen (Maschine war aus)
RandomizedDelaySec=15min     # entzerrt viele Hosts mit derselben Zeit
AccuracySec=1min             # Standard ist 1min, nicht sekundengenau
Unit=backup-export.service   # optional; sonst gleicher Name mit .service

[Install]
WantedBy=timers.target
Warum Timer statt cron. Der Lauf landet in einer eigenen cgroup und im Journal (journalctl -u backup-export zeigt jeden Lauf), er kann Abhängigkeiten haben, er lässt sich mit systemctl start backup-export.service unverändert von Hand auslösen, und systemctl list-timers zeigt für alle Timer den letzten und den nächsten Termin. Ausdrücke prüft man vorher mit systemd-analyze calendar "Mon *-*-* 03:00".

Type= richtig wählen — die häufigste Ursache für hängende Starts

Type=systemd hält die Unit für gestartet, sobald ...Passt zu
simple... der Prozess geforkt wurde. Kein Warten, keine Prüfung.Vordergrundprozesse, wenn nichts von ihnen abhängt
exec... execve() erfolgreich war. Fängt wenigstens „Binary fehlt“ ab.bessere Vorgabe als simple
notify... der Dienst selbst READY=1 über sd_notify() meldet.alles, worauf andere Units warten müssen
forking... der Elternprozess sich beendet hat. Braucht meist PIDFile=.alte Daemons, die sich selbst in den Hintergrund legen
oneshot... der Prozess fertig ist. Mit RemainAfterExit=yes gilt die Unit danach als aktiv.Skripte, Migrationen, Einrichtungsschritte
dbus... der in BusName= genannte Name auf dem Bus erscheint.bus-aktivierte Dienste
idle... wie simple, wartet aber bis zu 5 s auf ruhige Konsole.nur Kosmetik beim Boot

10. cgroups: Buchführung und Grenzen

Weil ohnehin jede Unit ihre eigene cgroup hat, kosten Limits nichts extra — sie sind ein paar Zeilen in derselben Datei. Der Baum ist hierarchisch: was am Slice hängt, gilt für alle Units darin gemeinsam.

DirektiveWirkungTypischer Einsatz
MemoryHigh=Weiche Grenze: der Kernel bremst und räumt aggressiv auf, tötet aber nicht.Erste Wahl — Dienst wird langsam statt tot
MemoryMax=Harte Grenze; darüber greift der OOM-Killer innerhalb der cgroup.Als letzte Sicherung über MemoryHigh=
CPUQuota=Absolute Obergrenze, 200% entspricht zwei vollen Kernen.Backup- und Indexläufe deckeln
CPUWeight=Relatives Gewicht (Vorgabe 100) — wirkt nur bei Konkurrenz.Wichtiges bevorzugen, ohne zu verschenken
IOWeight=
IOReadBandwidthMax=
Anteil beziehungsweise absolute Obergrenze an Datenträger-Durchsatz, pro Gerät.Ein Rsync, das den Host lahmlegt
TasksMax=Obergrenze an Prozessen und Threads.Fork-Bomben und Runaway-Worker
Slice=Hängt die Unit unter ein anderes Slice, dessen Limits dann gemeinsam gelten.Mehrere Dienste zusammen deckeln
# Sehen, wer was verbraucht
systemd-cgls                          # der Baum, mit Prozessen
systemd-cgtop                         # wie top, aber pro Unit statt pro Prozess
systemctl status nginx.service        # zeigt Memory, Tasks, CPU der cgroup

# Grenze setzen, ohne eine Datei zu öffnen - schreibt ein Drop-in nach /etc/...d/
systemctl set-property nginx.service MemoryHigh=2G MemoryMax=3G
systemctl set-property --runtime backup.service CPUQuota=40%   # nur bis Reboot

# Ein eigenes Slice für eine Gruppe von Diensten:
#   /etc/systemd/system/batch.slice
#     [Slice]
#     CPUQuota=300%
#     MemoryMax=8G
#   ... dann in jeder Unit:  Slice=batch.slice

# Einmalig etwas eingesperrt laufen lassen, ohne Unit-Datei
systemd-run --scope -p MemoryMax=1G -p CPUQuota=50% ./import.sh
systemd-run --unit=nachtlauf --on-calendar="*-*-* 03:00" /usr/local/bin/job
Zahlen gibt es nur, wenn mitgezählt wird. Auf älteren Systemen ist DefaultMemoryAccounting= und DefaultCPUAccounting= in /etc/systemd/system.conf nicht überall aktiv — dann bleibt systemctl status bei der Speicherangabe stumm. Sobald eine Grenze gesetzt ist, wird für diese Unit ohnehin gezählt.

11. Härten ohne Container

Dieselben Kernel-Mechanismen, die Container ausmachen — Namespaces, Capabilities, seccomp, Bind-Mounts — stehen jeder Unit als Direktiven zur Verfügung. Ein Dienst lässt sich damit deutlich enger führen als in vielen Container-Setups, ohne ein Image zu bauen.

DirektiveWirkung
ProtectSystem=strictDas gesamte Dateisystem wird nur lesbar eingehängt, außer /dev, /proc, /sys. Schreibrechte gibt es nur zurück über ReadWritePaths=.
ProtectHome=yes/home, /root und /run/user sind leer. read-only als mildere Variante.
PrivateTmp=yesEigenes /tmp und /var/tmp im Namespace — beseitigt eine ganze Klasse von Symlink-Angriffen.
PrivateDevices=yesNur noch Pseudo-Geräte, kein Zugriff auf echte Hardware.
PrivateNetwork=yesEigener Netzwerk-Namespace mit nur Loopback. Hart, aber für lokale Batch-Jobs ideal.
NoNewPrivileges=yesKein setuid-Aufstieg mehr, für den Dienst und alle seine Kinder. Sollte fast überall stehen.
CapabilityBoundingSet=Weißliste der verbleibenden Capabilities, zum Beispiel nur CAP_NET_BIND_SERVICE für Port 80.
SystemCallFilter=
@system-service
seccomp-Filter über vordefinierte Syscall-Gruppen; mit ~@privileged gezielt ausschließen.
RestrictAddressFamilies=Nur die gebrauchten Socket-Familien, meist AF_INET AF_INET6 AF_UNIX.
ProtectKernelTunables=
ProtectKernelModules=
ProtectKernelLogs=
/proc/sys und /sys nur lesbar, kein Modulladen, kein dmesg.
DynamicUser=yesUID nur für die Laufzeit, kein Eintrag in /etc/passwd, automatisch mit privaten StateDirectory= und PrivateTmp=.
IPAddressDeny=any
IPAddressAllow=
Paketfilter pro Unit über eBPF — Netzzugriff auf konkrete Ziele einschränken, unabhängig von der Firewall.
# Bewertung der eigenen Units - 0 = uneinnehmbar, 10 = völlig offen
systemd-analyze security                      # alle Dienste, sortiert
systemd-analyze security nginx.service        # zeilenweise, mit Begründung je Direktive

# Vorgehen: schrittweise zuschrauben, nach jedem Schritt neu starten und prüfen.
# Fehlschläge stehen im Journal - bei seccomp meist als "Operation not permitted".
journalctl -u nginx -b --since "5 min ago" -p warning
Reihenfolge in der Praxis. Erst NoNewPrivileges=, PrivateTmp= und ProtectSystem=full — die brechen fast nie etwas. Dann ProtectSystem=strict mit passenden ReadWritePaths=. Zuletzt Capabilities und Syscall-Filter, denn dort scheitert es am ehesten und die Meldungen sind am unspezifischsten. Alles davon gehört in ein Drop-in, nicht in die Paketdatei.

12. Werkzeugkasten

BereichBefehle
Zustand
systemctl
systemctl status <unit>
systemctl list-units --failed
systemctl is-active / is-enabled / is-failed   (skripttauglich)
systemctl reset-failed
systemctl list-unit-files --state=enabled
systemctl list-timers --all  ·  systemctl list-sockets
Journal
journalctl
journalctl -u <unit> -b  ·  -b -1 (voriger Boot)
journalctl --since "-2h" --until "10:00"
journalctl -p err  ·  -k (nur Kernel)  ·  -f (folgen)
journalctl -o json-pretty  ·  _PID=  ·  _UID=
journalctl --disk-usage  ·  --vacuum-time=30d  ·  --verify
Analyse
systemd-analyze
systemd-analyze time  ·  blame  ·  critical-chain
systemd-analyze plot > boot.svg
systemd-analyze dot | dot -Tsvg > units.svg
systemd-analyze verify <datei>  ·  security <unit>
systemd-analyze calendar  ·  timespan  ·  condition
systemd-analyze unit-paths  ·  cat-config
Ad hoc
systemd-run
systemd-run --unit=test /pfad/cmd
systemd-run --scope -p MemoryMax=1G cmd
systemd-run --on-active=90s cmd
systemd-run --on-calendar="*:0/15" cmd
systemd-run --user --scope cmd
(lange Laeufe ueberleben so das Abbrechen der SSH-Verbindung)

13. Praxis auf Debian- und Proxmox-Systemen

Zwei Pfade, ein Verzeichnis

/lib/systemd/system ist seit dem usr-merge nur noch ein Symlink auf /usr/lib/systemd/system. Ältere Anleitungen nennen den alten Pfad — das ist dasselbe Verzeichnis, nicht ein zweiter Suchpfad.

/etc/default lebt weiter

Debian-Pakete binden ihre alten /etc/default/<dienst>-Dateien meist per EnvironmentFile=-/etc/default/... ein. Änderungen dort wirken also — aber nur, wenn die Unit die Variablen auch tatsächlich benutzt. systemctl cat verrät es.

Alte Init-Skripte funktionieren

Was in /etc/init.d/ liegt, wird vom sysv-Generator in eine Unit übersetzt und taucht in systemctl auf. Diese Units liegen in /run/systemd/generator.late/ und lassen sich nicht editieren — man schreibt stattdessen eine echte Unit in /etc/, die sie verdrängt.

Netz wird nicht von networkd verwaltet

Auf Debian und Proxmox macht das weiterhin ifupdown mit /etc/network/interfaces, auf Desktops NetworkManager. networkd ist installiert, aber ab Werk deaktiviert. Vor dem Umstellen prüfen, wer gerade zuständig ist — zwei Verwalter auf einer Schnittstelle enden im Flattern.

resolv.conf ist ein Symlink

Wenn resolved läuft, steht in /etc/resolv.conf nur 127.0.0.53. Die echten Server zeigt resolvectl status. Manuelles Editieren wird beim nächsten Neustart überschrieben — die Server gehören in resolved.conf oder ans Interface.

Proxmox bootet über systemd-boot

Bei Installation auf ZFS mit UEFI verwaltet proxmox-boot-tool die EFI-Partitionen und schreibt die systemd-boot-Einträge — bootctl und /etc/kernel/cmdline direkt anzufassen, geht an diesem Werkzeug vorbei. Der richtige Weg ist proxmox-boot-tool refresh.

Ablauf, wenn eine Unit nicht startet

systemctl status foo.service            # 1. Zustand, Exit-Code, letzte 10 Journal-Zeilen
journalctl -u foo.service -b --no-pager  # 2. der ganze Verlauf dieses Boots
systemctl cat foo.service               # 3. was ist wirklich konfiguriert, inkl. Drop-ins
systemd-analyze verify foo.service      # 4. Syntax und unbekannte Direktiven
systemctl list-dependencies foo.service --after   # 5. wartet sie auf das Richtige?
systemctl show foo.service -p Conditions -p ExecMainStatus  # 6. still übersprungen?

# Läuft der Dienst von Hand, aber nicht als Unit, ist es fast immer die Sandbox
# oder die Umgebung. Zum Gegenprüfen mit identischer Umgebung starten:
systemd-run --pty --same-dir --wait --collect --service-type=exec \
  --property=User=backup /usr/local/bin/export-run --target=s3
Maßgeblich ist immer das Handbuch der eingesetzten Version. Direktiven und Voreinstellungen unterscheiden sich zwischen den systemd-Versionen. man systemd.directives ordnet jede einzelne Direktive der passenden Handbuchseite zu; die wichtigsten Seiten sind man systemd.unit, man systemd.service, man systemd.exec und man systemd.resource-control.
War diese Antwort hilfreich?
  • 0 Benutzer fanden dies hilfreich