SSD Nodes Learn 🎉 VPS ab $5.50/Monat
Anleitungen Matt ConnorVon Matt Connor

systemd Type=: simple, exec, forking oder notify?

Ihre Unit ist aktiv, obwohl der Daemon beendet ist? Erfahren Sie, wie Type= die Main-PID bestimmt und wann simple, exec, forking, oneshot oder notify passt.

Warum meldet systemd eine Unit als aktiv, obwohl der Prozess beendet wurde

Eine systemd-Service-Unit bleibt active, solange der eine Prozess lebt, den systemd als Hauptprozess betrachtet. Type= im Abschnitt [Service] legt fest, um welchen Prozess es sich handelt. Wenn Sie den falschen Wert wählen, überwacht systemd am Ende einen Shell-Wrapper oder einen kurzlebigen übergeordneten Prozess, während der relevante Daemon innerhalb derselben Unit beendet wird. Die Unit beschreibt den Zustand des Prozesses, den systemd überwachen soll.

Das Ändern der Restart-Richtlinie hilft hier nicht. Restart= wird aktiv, wenn der Hauptprozess beendet wird. Daher wird Restart=always nicht ausgelöst, solange die Main-PID (Prozesskennung) zu einem Prozess gehört, der noch läuft. Korrigieren Sie zuerst Type=. Was systemd nach dem tatsächlichen Beenden des Hauptprozesses tun soll, ist eine separate Entscheidung. Sie wird im Leitfaden zu Restart= und RestartSec= behandelt.

Was entscheidet Type= tatsächlich?

Jeder Wert von Type= beantwortet zwei Fragen zugleich: Wann darf systemd diese Unit als gestartet betrachten, und welcher Prozess ist der Hauptprozess?

Die erste Antwort steuert die Reihenfolge. Eine Unit, die Ihre Unit in After= nennt, wartet, bis systemd Ihre Unit als gestartet meldet. Eine Type=, die zu früh „gestartet“ meldet, lässt abhängige Units laufen, bevor Ihr Dienst Anfragen beantworten kann.

Die zweite Antwort steuert die Überwachung. systemd ordnet jeden Prozess, den eine Unit startet, einer cgroup (control group) zu. Dabei handelt es sich um eine Kernel-Funktion, die Prozesse gruppiert, damit sie gemeinsam begrenzt und beendet werden können. Über die cgroup führt systemctl stop die Bereinigung durch: KillMode= ist standardmäßig auf control-group gesetzt. Beim Stoppen einer Unit wird daher jedem darin enthaltenen Prozess ein Signal gesendet. Die Haupt-PID ist enger definiert. Sie bezeichnet den einzelnen Prozess, dessen Beenden die Unit beendet und dessen Exit-Status zum Ergebnis der Unit wird. Die Verwirrung beginnt, wenn die cgroup so behandelt wird, als wäre sie die Haupt-PID.

Type=simple meldet den Start, bevor die Binärdatei ausgeführt wird

Type=simple ist der Standardwert, wenn ExecStart= gesetzt ist und weder Type= noch BusName= vorhanden ist. systemd erstellt den Prozess, betrachtet die Unit sofort als gestartet und behandelt diesen Prozess als Haupt-PID. Nachfolgende Units starten unmittelbar, noch bevor die Service-Binärdatei ausgeführt wurde.

Dieses Detail erklärt eine häufige Überraschung. Ein Tippfehler im Pfad von ExecStart= führt trotzdem zu einem erfolgreichen Startjob. Der Fehler tritt erst einen Moment später auf, wenn die Ausführung fehlschlägt. systemd protokolliert diesen Fall mit dem Exit-Code 203. In der eigenen Tabelle bezeichnet systemd ihn als EXEC und definiert ihn als Fehler beim Ausführen der Service-Binärdatei. Eine erfolgreiche Rückkehr von systemctl start ohne Fehler beweist daher nicht, dass Ihre Binärdatei vorhanden ist.

Verwenden Sie simple für ein Programm, das im Vordergrund bleibt und sich nicht selbst in den Hintergrund verschiebt. Das gilt für die meisten modernen Daemons und für praktisch jedes Programm, das Sie selbst schreiben.

Type=exec wartet, bis das Programm tatsächlich gestartet wurde

Type=exec entspricht simple mit einem zusätzlichen Schritt. systemd betrachtet die Unit erst als gestartet, wenn sowohl der Fork als auch die Ausführung der Binärdatei erfolgreich waren. Eine fehlende Binärdatei oder ein User=, das nicht aufgelöst werden kann, lässt den Startauftrag nun selbst fehlschlagen, statt zunächst einen Erfolg zu melden und kurz darauf unbemerkt zu scheitern.

Type=exec wurde mit systemd 240 eingeführt und ist daher in jeder aktuellen Server-Distribution verfügbar. Ubuntu 24.04 wird mit systemd 255 ausgeliefert, Debian 13 mit systemd 257, Stand August 2026. Prüfen Sie Ihre Version mit systemctl --version.

Der Nachteil ist ein zusätzlicher Synchronisationsschritt beim Start. Der Vorteil ist ein korrekter Exit-Status von systemctl start. Bei einem Vordergrundprogramm sollten Sie exec gegenüber simple bevorzugen.

Type=forking und wie die Haupt-PID verloren geht

Type=forking teilt systemd mit, dass der Prozess in ExecStart= einen Kindprozess erzeugt und anschließend absichtlich beendet wird. systemd wartet, bis dieser erste Prozess beendet ist, und betrachtet die Unit erst danach als gestartet. Der zurückbleibende Kindprozess ist der Daemon.

Die Schwierigkeit besteht darin, den Prozess eindeutig zu identifizieren. Der von systemd gestartete Prozess ist beendet. Daher muss systemd ermitteln, welcher der verbliebenen Prozesse der Hauptprozess ist. Setzen Sie PIDFile= auf die Datei, in die der Daemon seine PID schreibt. Dabei handelt es sich normalerweise um einen Pfad unter /run. systemd liest die PID aus dieser Datei. Außerdem prüft systemd, ob die darin angegebene PID zu einem Prozess gehört, der bereits diesem Dienst zugeordnet ist. Eine veraltete Datei, die einen anderen Prozess angibt, wird daher abgelehnt und nicht als vertrauenswürdig behandelt.

Ohne PIDFile= gilt GuessMainPID=. Der Standardwert ist yes. Diese Erkennung ist nur zuverlässig, wenn der Dienst in einem einzelnen Prozess weiterläuft. Das Handbuch beschreibt die Einschränkung eindeutig: Besteht der Daemon aus mehreren Prozessen, kann die Erkennung fehlschlagen. Dann funktioniert auch die Erkennung von Fehlern nicht mehr. Eine Unit kann außerdem eine Haupt-PID von 0 erhalten. Das bedeutet, dass systemd überhaupt keinen Prozess überwachen kann.

Die meisten Daemons, die sich forken, bieten auch eine Option, mit der sie im Vordergrund bleiben. Verwenden Sie diese Option zusammen mit Type=exec und löschen Sie die Zeile PIDFile=. Weniger beteiligte Komponenten bedeuten weniger Möglichkeiten, die PID zu verlieren.

Type=oneshot für Arbeiten, die abgeschlossen werden

Type=oneshot erwartet, dass der Prozess ausgeführt wird und beendet wird. systemd markiert die Unit erst nach ihrem Ende als gestartet. Dadurch ist oneshot die passende Form für alles, worauf eine andere Unit warten muss. Dies ist auch der implizite Standard, wenn eine Unit weder Type= noch ExecStart= angibt.

Zwei Verhaltensweisen sind spezifisch für oneshot. Nur dieser Typ akzeptiert mehr als eine ExecStart=-Zeile. Diese Zeilen werden in der angegebenen Reihenfolge ausgeführt. Außerdem ist das Start-Timeout standardmäßig deaktiviert. Ein hängender oneshot wartet daher unbegrenzt, sofern Sie TimeoutStartSec= nicht selbst festlegen.

Nach dem Ende des Prozesses wechselt die Unit wieder in den Zustand inaktiv. RemainAfterExit=yes hält sie active, obwohl überhaupt kein Prozess läuft. Das ist die beabsichtigte Variante des Symptoms am Anfang dieser Seite. Sie ist korrekt, wenn die Aufgabe der Unit darin bestand, einen Zustand zu hinterlassen, statt einen laufenden Prozess aufrechtzuerhalten: etwa beim Laden eines Firewall-Regelwerks oder beim Starten eines Container-Stacks. Dieses Muster steht hinter einem Docker-Compose-Stack, der nach einem Reboot wieder gestartet wird. Die Unit führt den Compose-Befehl aus, beendet sich und bleibt aktiv, weil die gestarteten Container länger als die Unit laufen. Eine oneshot-Unit wird außerdem durch einen Zeitplan ausgelöst. Das ist die andere Hälfte von dem Ausführen eines Jobs mit einem systemd-Timer statt mit cron.

Type=notify meldet, wann der Dienst bereit ist

Type=notify verlagert die Entscheidung auf den Dienst. systemd hält den Startauftrag offen, bis der Prozess READY=1 über einen Unix-Socket sendet. Den Pfad erhält der Prozess über die Umgebungsvariable NOTIFY_SOCKET. Die C-Schnittstelle ist sd_notify(3), und viele Server unterstützen sie bereits.

Dies ist die genaue Antwort auf die Frage „ist der Dienst gestartet?“. simple und exec melden den Dienst als gestartet, bevor er seine Konfiguration gelesen oder seinen Listening-Socket geöffnet hat. Dadurch kann eine abhängige Unit zu früh starten und ihre erste Verbindung fehlschlagen. notify meldet den Dienst genau in dem Moment als gestartet, in dem der Dienst selbst seine Bereitschaft signalisiert.

systemd akzeptiert diese Meldung nur vom Hauptprozess. Das bedeutet NotifyAccess=main, und Type=notify setzt diese Einstellung voraus. Wenn die Meldung von einem Kindprozess oder einem Hilfsprozess kommt, setzen Sie NotifyAccess=all. Ein Shell-Skript kann systemd-notify --ready aufrufen. Dieser Aufruf läuft jedoch als eigener kurzlebiger Prozess. Deshalb benötigt er NotifyAccess=all, und systemd kann eine Meldung möglicherweise nicht mehr zuordnen, wenn der Absender bereits beendet wurde. Ein Dienst, der das Protokoll selbst unterstützt, ist zuverlässiger.

Zwei verwandte Einstellungen sollten Sie kennen. Type=notify-reload ist seit systemd 253 verfügbar und erweitert denselben Handshake auf Reloads. Dadurch kehrt systemctl reload erst zurück, wenn der Dienst den Abschluss des Reloads meldet, und nicht bereits nach dem Senden des Signals. WatchdogSec= fordert einen Dienst mit Benachrichtigungsunterstützung auf, in einem bestimmten Intervall eine Keepalive-Meldung zu senden. systemd wertet eine verpasste Frist als Fehler.

Type=dbus und Type=idle

Type=dbus wartet, bis der Dienst einen Namen auf D-Bus übernimmt. D-Bus ist der Nachrichtenbus, über den System- und Desktop-Dienste miteinander kommunizieren. Dafür ist BusName= erforderlich. Sobald BusName= gesetzt ist, wird Type=dbus zum Standardwert. Verwenden Sie Type=dbus nur für einen Dienst, der tatsächlich einen Busnamen registriert.

Type=idle verhält sich wie simple. Das Starten des Programms wird jedoch verzögert, bis wartende Aufträge verteilt wurden. Dafür gilt eine Begrenzung von fünf Sekunden. Dieser Typ verhindert, dass sich Konsolenausgaben beim Booten mit Statusmeldungen überschneiden. Er dient nicht zur Festlegung der Startreihenfolge und gehört nicht zu einem normalen Dienst.

Warum ein Wrapper-Skript systemd an der falschen PID festhält

Diese Struktur erzeugt das ursprüngliche Symptom.

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

systemd registriert die Shell als Haupt-PID. Die Shell bleibt aktiv, während exporter im Vordergrund läuft. Wenn server beendet wird, bemerkt die Shell das nicht. Die Haupt-PID ist daher weiterhin aktiv, die Unit bleibt active, und Restart= kann nichts unternehmen. Beide Prozesse befinden sich währenddessen vollständig in der cgroup der Unit, daher bereinigt systemctl stop weiterhin korrekt. Die Überwachung ist fehlerhaft, nicht die Bereinigung.

Die Lösung hängt davon ab, wie viele dauerhaft laufende Prozesse die Unit tatsächlich enthält.

Wenn es nur einer ist, ersetzen Sie die Shell durch diesen Prozess.

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

exec ersetzt die Shell durch das angegebene Programm und behält dieselbe PID. Die von systemd registrierte PID gehört damit zum Daemon. Noch besser ist es, den Wrapper zu entfernen. Environment= und EnvironmentFile= stellen die Variablen bereit, und ExecStartPre= führt den Einrichtungsschritt aus. Dadurch kann systemd den Daemon direkt starten und seine PID von Anfang an korrekt ermitteln.

Wenn es zwei sind, repräsentiert keine einzelne PID die Unit. Teilen Sie die Prozesse auf zwei Units auf und ordnen Sie sie mit After= und Wants=. Eine Unit pro Prozess ist die Struktur, die systemd zuverlässig überwachen kann. Nur so erhält jeder Prozess sein eigenes Neustartverhalten.

Welche Änderungen ExitType=cgroup bewirkt

ExitType= wurde in systemd 250 hinzugefügt. Der Standardwert ist main: Die Unit gilt als beendet, sobald der Hauptprozess beendet wird. Mit ExitType=cgroup gilt die Unit so lange als aktiv, wie ein beliebiger Prozess in ihrer Cgroup läuft.

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

Damit wird ein bestimmtes Problem gelöst. Ein Launcher, der die eigentliche Arbeit startet und anschließend beendet wird, würde unter ExitType=main dazu führen, dass systemd die Unit als beendet betrachtet und die verbliebenen Prozesse beendet. Mit ExitType=cgroup folgt die Unit stattdessen der gesamten Prozessgruppe.

Wichtig ist, was dadurch nicht gelöst wird. ExitType=cgroup hält eine Unit aktiv, solange mindestens ein Prozess läuft. Eine Unit mit zwei Daemons bleibt daher aktiv, nachdem einer davon beendet wurde. Der Fall mit dem Launcher wird behoben. ExitType=cgroup macht aus einer Unit jedoch keinen Supervisor für mehrere unabhängige Prozesse. ExitType= kann außerdem nicht mit Type=oneshot kombiniert werden.

Auch die Ressourcenabrechnung erfolgt über die Cgroup. Daher gelten Limits wie MemoryMax= und CPUQuota= für jeden von der Unit gestarteten Prozess, unabhängig davon, was Type= über die Haupt-PID aussagt. Dieser Aspekt wird unter Begrenzen von Speicher- und CPU-Nutzung eines Dienstes mit systemd erläutert.

Welchen Prozess systemd tatsächlich überwacht

Arbeiten Sie diese Schritte der Reihe nach an der Unit durch, die Sie debuggen. Lesen Sie zuerst, was systemd geladen hat. Lesen Sie anschließend, was systemd überwacht. Vergleichen Sie das Ergebnis danach mit der Prozesstabelle.

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

systemctl cat gibt die Unit-Datei zusammen mit jedem dafür geltenden Drop-in aus. Sie lesen damit die Konfiguration, die systemd geladen hat, und nicht die Datei, an deren Bearbeitung Sie sich erinnern. systemctl show gibt die effektiven Werte einschließlich der Standardwerte aus, die Sie nicht explizit eingetragen haben. Notieren Sie den Wert von MainPID, bevor Sie fortfahren.

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

systemd-cgls listet jeden Prozess in der cgroup der Unit auf. Die Zeile ps beschreibt den einzelnen Prozess, den systemd überwacht. Lesen Sie beide Angaben gemeinsam. Ein MainPID von 0 bedeutet, dass systemd keinen Prozess überwacht. Ein MainPID, der auf eine Shell verweist, während die cgroup außerdem Ihren Daemon enthält, entspricht dem oben beschriebenen Wrapper-Fall. Enthält eine cgroup mehr Prozesse als erwartet, ist ein Launcher oder ein forking Daemon beteiligt.

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

systemctl status gibt die Statuszeile und den cgroup-Baum gemeinsam aus. Damit lassen sich häufig beide Fragen gleichzeitig beantworten. journalctl -u beschränkt die Ausgabe mit -b auf den aktuellen Boot-Vorgang. Der Befehl zeigt die Start- und Stoppereignisse, die systemd für die Unit aufgezeichnet hat, einschließlich der festgestellten Exit-Codes. Wenn der Daemon in eine eigene Logdatei statt in das Journal schreibt, lesen Sie auch diese Datei. systemd kann nur Daten aufzeichnen, die es erreicht haben.

Wenn Sie Type= ändern, laden Sie die Unit-Dateien neu und starten Sie die Unit neu.

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

systemd-analyze verify analysiert die Datei und meldet Einstellungen, die systemd nicht akzeptieren kann. daemon-reload veranlasst systemd, die Unit-Dateien erneut von der Festplatte einzulesen. Ein geänderter Type= gilt nicht für eine bereits laufende Unit. Der Neustart ist daher erforderlich.

Testen Sie die Änderung anschließend. Ermitteln Sie die PID des tatsächlich relevanten Prozesses mit systemd-cgls und beenden Sie ihn. Führen Sie direkt danach systemctl is-active app.service aus. Wenn Type= korrekt ist, verlässt die Unit den aktiven Zustand. Bleibt sie aktiv, überwacht systemd weiterhin einen anderen Prozess.

Welchen systemd-Diensttyp Type= sollten Sie verwenden

  • Ein Programm, das im Vordergrund bleibt: Type=exec.
  • Ein Programm mit Unterstützung für Readiness-Benachrichtigungen: Type=notify, und notify-reload, wenn es auch das Neuladen bestätigt.
  • Ein Daemon, der unbedingt in den Hintergrund wechseln muss: Type=forking mit PIDFile= oder mit seiner Option für den Vordergrundbetrieb über Type=exec.
  • Ein Skript, das eine Aufgabe ausführt und beendet wird: Type=oneshot, zusätzlich RemainAfterExit=yes, wenn dabei ein Zustand zurückbleiben soll.
  • Ein Launcher, der sich beendet, während seine untergeordneten Prozesse weiterlaufen: Type=simple mit ExitType=cgroup.

Wenn Sie unsicher sind, welchen Typ ein Daemon eines Drittanbieters benötigt, lesen Sie zuerst die mitgelieferte Unit-Datei. Wenn Sie systemctl cat für eine vom Distributionspaket bereitgestellte Unit ausführen, wird der von der Distribution gewählte Type= angezeigt. Diese Auswahl wurde von mehr Menschen getestet als Ihre eigene.

FAQ

Warum bleibt meine systemd-Unit aktiv, obwohl der Prozess beendet wurde?

Weil der Prozess, den systemd als Hauptprozess behandelt, noch läuft. systemd überwacht pro Dienst eine einzelne PID, die gemäß Type= ausgewählt wird, und nicht jeden Prozess in der cgroup der Unit. Ein mit Type=simple gestartetes Wrapper-Skript ist die häufigste Ursache: Die Shell ist die Haupt-PID. Daher bleibt die Unit aktiv, wenn ein von der Shell im Hintergrund gestarteter Daemon beendet wird. Führen Sie systemctl show -p MainPID app.service aus. Listen Sie anschließend die cgroup der Unit mit systemd-cgls --unit=app.service auf und vergleichen Sie die beiden Werte.

Was ist der Unterschied zwischen Type=simple und Type=exec?

Type=simple betrachtet die Unit als gestartet, sobald systemd den Prozess erstellt hat, noch bevor die Binärdatei ausgeführt wurde. Daher führt ein falscher Pfad in ExecStart= weiterhin zu einem erfolgreichen Startauftrag, auf den anschließend ein Fehler folgt. Type=exec wartet, bis die Ausführung erfolgreich war. Der Fehler wird dann direkt vom Startauftrag gemeldet. Beide behandeln denselben Prozess als Haupt-PID. Type=exec erfordert systemd 240 oder neuer.

Benötige ich bei Type=forking weiterhin PIDFile=?

Ja, wenn der Daemon eine solche Datei schreibt. Ohne sie verwendet systemd GuessMainPID=. Dabei handelt es sich um eine Schätzung, die nur bei einem Dienst zuverlässig ist, der in einem einzelnen Prozess verbleibt. Wenn die Schätzung falsch oder nicht möglich ist, funktionieren die Fehlererkennung und der automatische Neustart für diese Unit nicht mehr. Setzen Sie PIDFile= auf den exakten Pfad, in den der Daemon schreibt, normalerweise unter /run.

Wann sollte ich RemainAfterExit=yes verwenden?

Wenn die Unit den Systemzustand ändern soll, anstatt einen laufenden Prozess aufrechtzuerhalten. Eine Type=oneshot-Unit, die Firewall-Regeln lädt oder einen Container-Stack startet, wird beendet, sobald ihre Aufgabe abgeschlossen ist. Ohne RemainAfterExit=yes wird die Unit inaktiv. Dadurch bleibt für systemctl stop nichts zu stoppen und es gibt keine Möglichkeit, eine ExecStop=-Bereinigung auszuführen. Mit dieser Einstellung bleibt die Unit ohne Prozesse aktiv. Das ist in diesem Fall beabsichtigt.

Erfordert eine Änderung von Type= einen daemon-reload?

Ja. Außerdem muss die Unit neu gestartet werden. systemctl daemon-reload veranlasst systemd, die Unit-Dateien vom Datenträger erneut einzulesen. Eine laufende Instanz verwendet jedoch weiterhin den Type=, mit dem sie gestartet wurde. Führen Sie vor dem Testen sudo systemctl daemon-reload und anschließend sudo systemctl restart app.service aus. Andernfalls beobachten Sie weiterhin das alte Überwachungsverhalten.