von setsid und PID-Dateien bis zur systemd-Unit
Ein Skript mit & im Hintergrund zu starten macht noch keinen Daemon. Ein echter Daemon löst sich vollständig vom Terminal, verhindert Mehrfachstart über eine PID-Datei, reagiert kontrolliert auf Signale wie SIGTERM und SIGHUP, und schreibt sein Log an eine Stelle, die auch ohne Terminal erreichbar bleibt.
Inhaltsverzeichnis
- 1. Was einen Daemon von einem Hintergrundskript unterscheidet
- 2. Double-Fork und setsid im Detail
- 3. PID-Dateien und Locking gegen Mehrfachstart
- 4. Signal-Handling: SIGTERM, SIGHUP und sauberes Beenden
- 5. Logging ohne Terminal: Dateien und Syslog
- 6. Health-Checks und Restart-Strategien
- 7. systemd als moderne Alternative
- 8. Typische Fehler beim Bau eines Bash-Daemons
- 9. Daemonisierungsansätze im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Was einen Daemon von einem Hintergrundskript unterscheidet
Ein Daemon ist ein Prozess, der dauerhaft und unabhängig von jedem Terminal im Hintergrund läuft, typischerweise für die gesamte Laufzeit des Systems oder bis er explizit beendet wird. Ein einfaches ./skript.sh & ist noch kein Daemon, weil der Prozess weiterhin an das startende Terminal gebunden bleibt und bei dessen Schließen ein SIGHUP erhält, das ihn in den meisten Fällen sofort beendet.
Ein echter Daemon braucht mehrere Eigenschaften gleichzeitig: vollständige Trennung vom Terminal, eine neue Sitzung ohne Controlling-Terminal, umgeleitete Standard-Datenströme, ein bekanntes Arbeitsverzeichnis, und meist eine PID-Datei, über die andere Prozesse ihn identifizieren und steuern können. Keine dieser Eigenschaften entsteht automatisch durch einfaches Starten im Hintergrund, sie müssen im Skript selbst implementiert werden.
In der Praxis stellt sich oft die Frage, ob man einen Daemon in reinem Bash von Hand baut oder auf systemd setzt, das viele dieser Aufgaben übernimmt. Dieser Artikel zeigt beide Wege: die klassische manuelle Daemonisierung für Umgebungen ohne systemd oder für portable Skripte, und den Übergang zu systemd als robustere moderne Alternative.
2. Double-Fork und setsid im Detail
Die klassische Technik zur Daemonisierung eines Prozesses ist der Double-Fork, kombiniert mit setsid. Der erste fork erzeugt einen Kindprozess und beendet den Elternprozess sofort, wodurch das Kind zum Waisen wird und von init beziehungsweise systemd adoptiert wird. setsid im Kind erzeugt eine neue Sitzung, in der der Prozess zum Sitzungsführer wird, aber noch kein Controlling-Terminal besitzt. Ein zweiter fork verhindert, dass der Prozess später versehentlich ein Controlling-Terminal erhält, weil nur Sitzungsführer eines erwerben können und der zweite Fork-Nachkomme keiner ist.
In Bash lässt sich dieser klassische Double-Fork-Ansatz nachbilden, wobei setsid als externes Kommando meist einfacher ist als der reine fork-Ansatz, weil Bash selbst kein direktes fork-Builtin anbietet. setsid befehl startet den Befehl in einer neuen Sitzung ohne Terminal, was für die meisten Daemon-Anwendungsfälle bereits ausreicht, ohne die volle Komplexität eines echten Double-Forks in C nachzubilden.
#!/usr/bin/env bash
set -euo pipefail
daemonize() {
local cmd="$1"
local pidfile="$2"
local logfile="$3"
# setsid detaches from the controlling terminal and starts a new session
setsid bash -c "
exec >> '$logfile' 2>&1
exec < /dev/null
echo \$\$ > '$pidfile'
exec $cmd
" &
disown
echo "Daemon started, see $logfile"
}
daemonize "./worker-loop.sh" "/var/run/myapp.pid" "/var/log/myapp.log"
Entscheidend im Beispiel ist die Kombination aus setsid für die neue Sitzung, der Umleitung aller Standard-Datenströme vor dem eigentlichen exec, und dem Schreiben der PID in eine Datei, sobald der neue Prozess seine endgültige Identität hat. exec ersetzt dabei den bash -c Wrapper-Prozess durch den eigentlichen Daemon-Befehl, ohne einen weiteren Kindprozess zu erzeugen.
3. PID-Dateien und Locking gegen Mehrfachstart
Eine PID-Datei speichert die Prozess-ID eines laufenden Daemon, üblicherweise unter /var/run/ oder /run/, und dient zwei Zwecken: anderen Skripten zu erlauben, den Daemon gezielt zu stoppen oder Signale an ihn zu senden, und dem Daemon selbst zu erlauben, beim Start zu prüfen, ob bereits eine Instanz läuft. Ohne diese Prüfung könnten mehrere Instanzen desselben Daemons parallel laufen, was bei exklusiven Ressourcen wie Ports oder Lockfiles zu Konflikten führt.
Die robuste Prüfung liest die PID aus der Datei, sofern vorhanden, und verifiziert mit kill -0, ob der Prozess tatsächlich noch existiert. Eine verwaiste PID-Datei von einem abgestürzten Daemon darf nicht dazu führen, dass ein Neustart verweigert wird, deshalb muss die Existenzprüfung zwingend zur reinen Dateiprüfung dazukommen. flock auf die PID-Datei selbst bietet eine zusätzliche, race-condition-freie Absicherung gegenüber der reinen PID-Prüfung.
#!/usr/bin/env bash
set -euo pipefail
PIDFILE="/var/run/myapp.pid"
LOCKFILE="/var/run/myapp.lock"
check_already_running() {
if [[ -f "$PIDFILE" ]]; then
local existing_pid
existing_pid="$(cat "$PIDFILE")"
if kill -0 "$existing_pid" 2>/dev/null; then
echo "[ERROR] Already running with PID $existing_pid" >&2
exit 1
else
echo "[WARN] Stale PID file found, removing" >&2
rm -f "$PIDFILE"
fi
fi
}
# Additional race-condition-free guard using flock
exec 9>"$LOCKFILE"
flock -n 9 || { echo "[ERROR] Could not acquire lock, already running" >&2; exit 1; }
check_already_running
echo $$ > "$PIDFILE"
trap 'rm -f "$PIDFILE" "$LOCKFILE"' EXIT
4. Signal-Handling: SIGTERM, SIGHUP und sauberes Beenden
Ein sauber implementierter Daemon reagiert auf mindestens zwei Signale gezielt: SIGTERM zum kontrollierten Beenden und SIGHUP häufig zum Neuladen der Konfiguration, ohne den Prozess komplett neu zu starten. Ohne expliziten trap für SIGTERM würde der Standardhandler den Prozess sofort ohne Cleanup beenden, was offene Dateien, Locks oder unvollständige Schreiboperationen hinterlassen kann.
Die trap-Funktion für SIGTERM sollte laufende Arbeit kontrolliert abschließen, offene Ressourcen freigeben und die PID-Datei entfernen, bevor der Prozess tatsächlich endet. SIGHUP wird traditionell genutzt, um einem Daemon mitzuteilen, seine Konfigurationsdatei neu einzulesen, ohne laufende Verbindungen oder Verarbeitungen zu unterbrechen, ein Muster, das viele klassische Unix-Daemons wie nginx oder rsyslog implementieren.
#!/usr/bin/env bash
set -euo pipefail
running=true
config_file="/etc/myapp/config.conf"
load_config() {
# shellcheck disable=SC1090
source "$config_file"
echo "$(date -Iseconds) Config reloaded" >> /var/log/myapp.log
}
shutdown_gracefully() {
echo "$(date -Iseconds) Received SIGTERM, finishing current task" >> /var/log/myapp.log
running=false
}
reload_config() {
echo "$(date -Iseconds) Received SIGHUP, reloading config" >> /var/log/myapp.log
load_config
}
trap shutdown_gracefully TERM
trap reload_config HUP
load_config
while $running; do
process_next_task
sleep 1
done
echo "$(date -Iseconds) Daemon stopped cleanly" >> /var/log/myapp.log
5. Logging ohne Terminal: Dateien und Syslog
Ohne Terminal-Verbindung landen Ausgaben eines Daemon nirgendwo, sofern sie nicht explizit umgeleitet werden. Der einfachste Ansatz leitet stdout und stderr direkt an eine Logdatei um, entweder bereits beim Start des Daemons oder mit exec > logfile 2>&1 als erste Zeile im Skript selbst. Für Log-Rotation muss der Daemon zusätzlich auf SIGHUP oder ein eigenes Signal reagieren, um die Logdatei neu zu öffnen, sonst schreibt er nach einer Rotation weiterhin in den alten, bereits umbenannten Dateideskriptor.
Eine robustere Alternative ist logger, das Nachrichten direkt an syslog beziehungsweise journald weiterreicht, ohne dass der Daemon selbst Rotation verwalten muss. echo "Nachricht" | logger -t myapp schreibt einen Log-Eintrag mit dem Tag myapp, der über journalctl -t myapp oder klassische Syslog-Dateien einsehbar ist. Dieser Ansatz delegiert Rotation, Kompression und Aufbewahrungsfristen komplett an die Systemd-Journal- oder Syslog-Infrastruktur.
#!/usr/bin/env bash
set -euo pipefail
log_message() {
local level="$1"
local message="$2"
# Delegate rotation and retention to syslog/journald instead of
# writing raw files that need manual rotation handling
logger -t myapp -p "user.${level}" "$message"
}
log_message "info" "Daemon started with PID $$"
process_item() {
local item="$1"
if ! do_work "$item"; then
log_message "err" "Failed to process $item"
return 1
fi
log_message "info" "Processed $item successfully"
}
6. Health-Checks und Restart-Strategien
Ein produktiver Daemon sollte seinen eigenen Gesundheitszustand nach außen sichtbar machen, damit externes Monitoring erkennen kann, ob der Prozess noch sinnvoll arbeitet, nicht nur, ob er noch existiert. Eine einfache Methode ist eine Heartbeat-Datei, die der Daemon in regelmäßigen Abständen mit dem aktuellen Zeitstempel aktualisiert. Ein externes Monitoring-Skript prüft dann, ob diese Datei innerhalb eines erwarteten Zeitfensters aktualisiert wurde, und schlägt Alarm, falls der Daemon zwar als Prozess existiert, aber intern hängt.
Für automatische Neustarts nach Abstürzen reicht ein in Bash gebauter Daemon allein nicht aus, weil ein abgestürzter Prozess sich nicht selbst neu starten kann. Hier braucht es entweder eine übergeordnete Watchdog-Instanz, die den Daemon überwacht und bei Bedarf neu startet, oder den Wechsel zu einem Process Supervisor wie systemd, der genau diese Aufgabe standardmäßig übernimmt.
7. systemd als moderne Alternative
Für die meisten neuen Projekte ist ein systemd-Service die robustere Alternative zur manuellen Bash-Daemonisierung, weil systemd PID-Datei-Verwaltung, Restart-Strategien, Logging über journald und Signal-Handling bereits als Bordmittel bereitstellt. Das Bash-Skript muss dann selbst keinen Daemon-Modus mehr implementieren, sondern kann im Vordergrund laufen, während systemd über Type=simple oder Type=notify die Prozessüberwachung übernimmt.
Der Umstieg reduziert den eigenen Code erheblich: kein Double-Fork, keine manuelle PID-Datei, kein manuelles Log-Rotation-Handling. Restart=on-failure sorgt automatisch für einen Neustart nach einem Absturz, was ein reiner Bash-Daemon ohne externen Watchdog nicht leisten kann. Für neue Projekte auf Linux-Systemen mit systemd ist dieser Weg fast immer vorzuziehen, während die manuelle Double-Fork-Technik für portable Skripte oder Container ohne systemd relevant bleibt.
#!/usr/bin/env bash
set -euo pipefail
# Generate a systemd unit instead of hand-rolling daemonization logic
cat > /etc/systemd/system/myapp.service << 'EOF'
[Unit]
Description=My Bash worker daemon
After=network.target
[Service]
Type=simple
ExecStart=/opt/myapp/worker-loop.sh
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now myapp.service
systemctl status myapp.service --no-pager
8. Typische Fehler beim Bau eines Bash-Daemons
Der häufigste Fehler ist, stdin nicht umzuleiten. Ein Daemon ohne exec < /dev/null kann bei einem versehentlichen Lesezugriff auf stdin unerwartet blockieren oder ein SIGTTIN erhalten, wenn er noch an ein Terminal gebunden ist. Der zweite häufige Fehler ist, die PID-Datei zu früh zu schreiben, bevor der eigentliche Zielprozess seine endgültige PID über exec erreicht hat, was zu einer PID-Datei mit der PID des Wrapper-Prozesses statt des tatsächlichen Daemons führt.
Ein dritter Fehler betrifft fehlendes Signal-Handling: Ein Daemon ohne trap für SIGTERM wird beim Stoppen abrupt beendet, ohne Cleanup, ohne das Schließen offener Dateien und ohne Entfernen der PID-Datei. Das führt beim nächsten Start zu einer verwaisten PID-Datei, die fälschlich einen bereits laufenden Prozess suggeriert, wenn die Existenzprüfung mit kill -0 nicht sorgfältig implementiert ist.
9. Daemonisierungsansätze im Vergleich
Die folgende Tabelle vergleicht die wichtigsten Ansätze, einen Prozess dauerhaft im Hintergrund zu betreiben.
| Ansatz | Automatischer Neustart | Log-Verwaltung | Aufwand |
|---|---|---|---|
| & allein | Nein | Keine | Minimal, aber unzuverlässig |
| setsid + PID-Datei | Nein (ohne Watchdog) | Manuell, Rotation nötig | Mittel |
| setsid + logger/syslog | Nein (ohne Watchdog) | Über syslog/journald | Mittel |
| systemd-Service | Ja, konfigurierbar | Automatisch über journald | Niedrig |
Für neue Bash-Daemons auf einem System mit systemd ist die letzte Zeile fast immer die richtige Wahl. Die manuelle Kombination aus setsid, PID-Datei und Signal-Handling bleibt relevant für Container ohne Init-System, ältere Systeme oder portable Skripte, die auf mehreren Plattformen ohne systemd laufen müssen.
Mironsoft
Shell-Automatisierung, DevOps-Tooling und Deployment-Infrastruktur
Bash-Skripte, die zuverlässig als Dienst laufen sollen?
Wir bauen robuste Daemonisierung mit PID-Dateien, Signal-Handling und Logging in eure Bash-Skripte ein, oder migrieren sie auf systemd-Units mit automatischem Neustart und zentralem Logging über journald.
Daemon-Review
Bestehende Hintergrundskripte auf Robustheit prüfen
systemd-Migration
Units mit Restart-Strategie und journald-Logging aufsetzen
Signal-Handling
Sauberes Beenden und Config-Reload ohne Prozessneustart
10. Zusammenfassung
Ein echter Daemon braucht mehr als ein & am Zeilenende: vollständige Trennung vom Terminal über setsid, eine PID-Datei mit robuster Existenzprüfung gegen Mehrfachstart, sauberes Signal-Handling für SIGTERM und SIGHUP, sowie Logging, das auch ohne Terminal erreichbar bleibt. Jede dieser Eigenschaften muss im Bash-Skript explizit implementiert werden, sie entstehen nicht automatisch durch einfaches Starten im Hintergrund.
Für neue Projekte auf Systemen mit systemd ist ein systemd-Service in den meisten Fällen die robustere und wartungsärmere Alternative, weil automatischer Neustart, Logging über journald und Prozessüberwachung bereits als Bordmittel vorhanden sind. Die manuelle Daemon-Implementierung bleibt relevant für portable Skripte, Container ohne Init-System und Umgebungen, in denen systemd nicht verfügbar ist.
Bash-Skripte als Daemon betreiben — Das Wichtigste auf einen Blick
Trennung vom Terminal
setsid startet eine neue Sitzung ohne Controlling-Terminal, exec < /dev/null verhindert stdin-Blockaden.
PID-Datei
Existenzprüfung mit kill -0 vor dem Start, flock als zusätzliche race-condition-freie Absicherung.
Signale
trap für SIGTERM zum sauberen Beenden, SIGHUP häufig für Config-Reload ohne Neustart.
Alternative
systemd übernimmt Restart, Logging und Prozessüberwachung als Bordmittel, meist die robustere Wahl.