atomares Locking und sichere Mehrfachausführungs-Kontrolle
Bash-Skripte, die ohne Locking in Cron-Jobs oder Deployment-Pipelines laufen, riskieren Race Conditions bei Mehrfachausführung. PID-Files sind verbreitet, aber nicht atomar. flock bietet kernel-gestützte Sperren, die auch bei Crashes automatisch freigegeben werden – das richtige Werkzeug für zuverlässiges Locking in Shell-Skripten.
Inhaltsverzeichnis
- 1. Warum Locking in Bash-Skripten unverzichtbar ist
- 2. Race Conditions in Shell-Skripten verstehen
- 3. PID-Files: das verbreitete, aber fehleranfällige Muster
- 4. flock: atomares Locking mit Kernel-Unterstützung
- 5. flock-Muster: non-blocking, timeout und FD-basiert
- 6. Stale Locks erkennen und automatisch aufräumen
- 7. mkdir als atomarer Locking-Mechanismus
- 8. Locking in Cron-Jobs und Deployment-Pipelines
- 9. Locking-Methoden im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum Locking in Bash-Skripten unverzichtbar ist
Locking in Bash ist kein akademisches Thema, sondern ein tägliches Praxisproblem in jeder Infrastruktur, die Cron-Jobs, parallele CI-Jobs oder Deployment-Skripte verwendet. Wenn ein Backup-Skript um 02:00 Uhr startet und der vorherige Lauf noch nicht beendet ist, erzeugen beide Instanzen inkonsistente Backup-Zustände. Wenn zwei Deployment-Läufe gleichzeitig starten, weil ein manueller Trigger auf einen noch laufenden Cron-Trigger trifft, riskiert man beschädigte Deployments. Das Kernproblem ist immer dasselbe: ein Skript, das nicht weiß, ob bereits eine andere Instanz läuft.
Naives Locking in Bash mit if [ -f lockfile ]; then exit; fi; touch lockfile löst das Problem nicht – zwischen dem Prüfen und dem Erstellen der Datei liegt eine Zeitlücke, in der eine zweite Instanz dieselbe Prüfung durchführt und ebenfalls keinen Lock findet. Diese klassische Race Condition ist in Hochlast-Situationen oder bei gleichzeitig gestarteten CI-Jobs reproduzierbar. Kernel-gestütztes Locking mit flock schließt diese Lücke vollständig, weil die Atomarität im Kernel liegt, nicht in der Shell.
2. Race Conditions in Shell-Skripten verstehen
Eine Race Condition in einem Shell-Skript entsteht, wenn das Ergebnis einer Operation vom Timing anderer Prozesse abhängt. In Bezug auf Locking ist das klassische Szenario: Prozess A prüft, ob ein Lockfile existiert (nein), wird dazwischen durch den Scheduler unterbrochen, Prozess B prüft ebenfalls (nein), Prozess A erstellt das Lockfile und startet, Prozess B erstellt ebenfalls das Lockfile und startet – beide laufen gleichzeitig, kein Locking hat stattgefunden. Dieses Fenster ist in der Praxis oft nur Millisekunden breit, aber bei ausreichend häufiger Ausführung oder unter Last tritt es zuverlässig auf.
Die Schwere der Konsequenz hängt stark vom Skript ab. Bei einem Backup-Skript entstehen doppelte Backups mit inkonsistenten Zuständen. Bei einem Deployment-Skript können zwei gleichzeitige Deployments Datenbankmigrationen doppelt ausführen, was zu korrupten Schemas führt. Bei einem Cleanup-Skript können Dateien gelöscht werden, die gerade von der zweiten Instanz noch genutzt werden. Race Conditions beim Locking sind besonders heimtückisch, weil sie intermittierend auftreten und in Tests, die sequenziell ablaufen, nie sichtbar sind.
3. PID-Files: das verbreitete, aber fehleranfällige Muster
Das PID-File-Muster für Locking in Bash schreibt die Prozess-ID der laufenden Instanz in eine Datei und prüft beim Start, ob diese Datei existiert und der darin gespeicherte Prozess noch läuft. Es ist weit verbreitet, hat aber strukturelle Schwächen. Die Hauptschwäche: Das Prüfen der PID-File-Existenz und das Erstellen der Datei sind keine atomare Operation – die Race Condition ist nur kleiner, nicht eliminiert. Zusätzlich müssen stale PID-Files (aus abgestürzten Skripten) manuell erkannt und entfernt werden.
Ein stale PID-File entsteht, wenn ein Skript durch einen Kill-Signal oder Systemabsturz beendet wird, ohne den Cleanup-Handler auszuführen. Bei der nächsten Ausführung findet das Skript die PID-File, prüft mit kill -0 $PID ob der Prozess noch läuft – was bei einer recycelten PID zum falschen Ergebnis führen kann: Die PID könnte einem völlig anderen, später gestarteten Prozess gehören. Diese PID-Recycling-Problematik macht reine PID-File-Locking-Implementierungen für kritische Workflows unzuverlässig.
#!/usr/bin/env bash
# pid-file-locking.sh — PID-file locking with stale detection (better than naive)
set -euo pipefail
PID_FILE="/var/run/myapp.pid"
acquire_pid_lock() {
if [[ -f "$PID_FILE" ]]; then
local old_pid
old_pid=$(< "$PID_FILE")
# Check if PID is still alive AND belongs to this script
if kill -0 "$old_pid" 2>/dev/null; then
# Extra check: verify it's actually our script type
local proc_name
proc_name=$(ps -p "$old_pid" -o comm= 2>/dev/null || echo "")
if [[ "$proc_name" == "bash" ]]; then
echo "[ERROR] Another instance is running (PID $old_pid)" >&2
return 1
fi
fi
# Stale PID file — previous run crashed
echo "[WARN] Removing stale PID file (PID $old_pid is gone)" >&2
rm -f "$PID_FILE"
fi
# Write current PID — still not 100% atomic!
echo $$ > "$PID_FILE"
}
release_pid_lock() {
[[ -f "$PID_FILE" ]] && rm -f "$PID_FILE"
}
trap release_pid_lock EXIT
acquire_pid_lock || exit 1
echo "[INFO] Lock acquired (PID $$)"
# ... main logic here ...
4. flock: atomares Locking mit Kernel-Unterstützung
flock ist ein Linux-Kommando, das Kernel-gestütztes Locking über File-Descriptors implementiert. Im Gegensatz zum PID-File-Muster ist flock vollständig atomar – die Sperre wird im Kernel gesetzt, und zwei Prozesse können nicht gleichzeitig denselben exklusiven Lock auf denselben File-Descriptor erhalten. Das hat zwei weitere entscheidende Vorteile gegenüber PID-Files: Der Lock wird automatisch freigegeben, wenn der Prozess endet – egal ob normal, durch einen Fehler oder durch einen Crash, ohne dass ein Cleanup-Handler ausgeführt werden muss. Und stale Locks sind physikalisch unmöglich, weil der Kernel die Lock-Freigabe an den Prozess-Lebenszyklus bindet.
Die Verwendungsform exec 9>lockfile; flock -n 9 öffnet einen File-Descriptor auf die Lock-Datei und versucht, einen exklusiven nicht-blockierenden Lock zu setzen. Wenn der Lock nicht verfügbar ist (ein anderer Prozess hält ihn), gibt flock -n sofort mit Exit-Code 1 zurück. Dieses Non-Blocking-Muster ist für Skripte geeignet, die nicht warten sollen, sondern sofort abbrechen, wenn bereits eine Instanz läuft. Das Lockfile selbst ist dabei unwichtig – es wird nie gelesen oder geschrieben, nur als Anker für die Kernel-Sperre verwendet. Die Datei kann leer bleiben.
5. flock-Muster: non-blocking, timeout und FD-basiert
Es gibt drei wesentliche flock-Verwendungsmuster in Bash-Skripten: das Non-Blocking-Muster, das Timeout-Muster und das FD-basierte Wrapper-Muster. Das Non-Blocking-Muster (flock -n 9) eignet sich für Cron-Jobs, die übersprungen werden sollen, wenn bereits eine Instanz läuft. Das Timeout-Muster (flock -w 30 9) wartet maximal 30 Sekunden auf den Lock und bricht dann ab – geeignet für Skripte, die auf einen laufenden Vorläufer warten können. Das FD-basierte Wrapper-Muster kapselt den gesamten Lock-Lebenszyklus in einer Funktion, die den FD öffnet, den Lock hält und beim Funktionsende automatisch freigibt.
Ein besonders elegantes flock-Muster nutzt die Subshell-Variante: flock -n 9 bash -c "..." oder (flock -n 9; your_function) 9>lockfile. Dabei wird der gesamte gesperrte Code in einer Subshell ausgeführt, die den FD besitzt – beim Ende der Subshell wird der Lock automatisch freigegeben. Dieses Muster vermeidet, dass man den FD explizit verwalten muss, und macht den Lock-Scope im Code sichtbar. Für Bash-Skripte, in denen mehrere Abschnitte unterschiedliche Locks benötigen, ist dieses Muster die klarste Lösung.
#!/usr/bin/env bash
# flock-patterns.sh — All flock locking patterns for production use
set -euo pipefail
LOCK_DIR="/var/lock"
SCRIPT_NAME="$(basename "$0" .sh)"
LOCK_FILE="${LOCK_DIR}/${SCRIPT_NAME}.lock"
# Pattern 1: Non-blocking — exit immediately if already running
exec 9>"$LOCK_FILE"
if ! flock -n 9; then
echo "[INFO] Another instance of $SCRIPT_NAME is already running. Exiting." >&2
exit 0
fi
echo "[INFO] Lock acquired (PID $$, FD 9)"
# Pattern 2: Timeout — wait up to 30 seconds
acquire_with_timeout() {
local timeout="${1:-30}"
exec 8>"${LOCK_FILE}.secondary"
if ! flock -w "$timeout" 8; then
echo "[ERROR] Could not acquire lock within ${timeout}s" >&2
return 1
fi
echo "[INFO] Lock acquired after waiting"
}
# Pattern 3: Subshell scope — lock is released when subshell exits
run_exclusive() {
local lock_file="$1"
shift
(
exec 7>"$lock_file"
flock -n 7 || { echo "[ERROR] Resource is locked" >&2; exit 1; }
"$@"
) # lock automatically released here
}
# Example usage
run_exclusive "${LOCK_FILE}.db" bash -c 'echo "Exclusive DB operation"'
# Lock info: show who holds a lock
show_lock_info() {
local file="$1"
if [[ -f "$file" ]]; then
local holder
holder=$(fuser "$file" 2>/dev/null || echo "none")
echo "Lock file: $file | Held by PID: $holder"
fi
}
show_lock_info "$LOCK_FILE"
6. Stale Locks erkennen und automatisch aufräumen
Stale Locks – also veraltete Sperren, die nach einem Prozessende nicht aufgeräumt wurden – sind beim flock-Ansatz strukturell unmöglich, weil der Kernel den Lock zusammen mit dem Prozess-Lebenszyklus verwaltet. Beim PID-File-Muster hingegen sind stale Locks ein reales Problem. Der Grund: Wenn ein Prozess durch kill -9 beendet wird oder wenn das System abrupt neustartet, kann der Cleanup-Handler (trap) nicht ausgeführt werden – die PID-File bleibt zurück.
Das robuste Erkennungsmuster für stale Locks prüft drei Dinge: Existiert die PID-File? Ist die darin gespeicherte PID noch ein aktiver Prozess? Gehört dieser Prozess zu einem Skript mit dem erwarteten Namen? Erst wenn alle drei Bedingungen zutreffen, ist der Lock gültig. Für die automatische Aufräumung bei stale PID-Files bietet sich eine Altersprüfung an: Eine PID-File, die älter als die maximale erwartete Laufzeit des Skripts ist, kann als stale behandelt werden. Mit find "$PID_FILE" -mmin +120 kann geprüft werden, ob die Datei älter als 120 Minuten ist.
#!/usr/bin/env bash
# stale-lock-cleanup.sh — Stale PID-file detection and cleanup
set -euo pipefail
PID_FILE="/var/run/backup.pid"
MAX_AGE_MINUTES=120 # Maximum expected runtime
is_stale_lock() {
local pid_file="$1"
# No file — no lock
[[ -f "$pid_file" ]] || return 1
local stored_pid
stored_pid=$(< "$pid_file") 2>/dev/null || return 1
# Validate: must be a number
[[ "$stored_pid" =~ ^[0-9]+$ ]] || { echo "[WARN] Invalid PID in lock file" >&2; return 0; }
# Check 1: Is the process alive?
if ! kill -0 "$stored_pid" 2>/dev/null; then
echo "[WARN] Stale lock: PID $stored_pid is gone" >&2
return 0 # stale
fi
# Check 2: Is it too old? (could be a stuck process)
if find "$pid_file" -mmin +"$MAX_AGE_MINUTES" | grep -q .; then
echo "[WARN] Lock file older than ${MAX_AGE_MINUTES}min — possible stuck process" >&2
return 0 # treat as stale
fi
# Check 3: Process name check
local proc_cmd
proc_cmd=$(ps -p "$stored_pid" -o args= 2>/dev/null || echo "")
if ! echo "$proc_cmd" | grep -q "backup"; then
echo "[WARN] PID $stored_pid is not our process (PID recycled?)" >&2
return 0 # stale
fi
return 1 # Lock is valid
}
if is_stale_lock "$PID_FILE"; then
echo "[INFO] Removing stale lock file" >&2
rm -f "$PID_FILE"
fi
# Now try to acquire with flock for real atomicity
exec 9>"${PID_FILE%.pid}.flock"
flock -n 9 || { echo "[ERROR] Lock held by active process" >&2; exit 1; }
echo $$ > "$PID_FILE"
trap 'rm -f "$PID_FILE"' EXIT
7. mkdir als atomarer Locking-Mechanismus
Das mkdir-Kommando ist eine wenig bekannte, aber portable Alternative zu flock für atomares Locking in Bash. Das Betriebssystem garantiert, dass mkdir atomar ist: Zwei Prozesse, die gleichzeitig mkdir /tmp/mylock ausführen, erhalten garantiert nur einen Exit-Code 0 – der andere schlägt fehl. Diese Atomarität liegt im POSIX-Standard verankert und ist auf allen Systemen verfügbar, auch dort, wo flock nicht installiert ist (z. B. einige Container-Images oder NFS-Mounts, die flock nicht unterstützen).
Der Nachteil des mkdir-Locking-Musters: Im Gegensatz zu flock ist es nicht crash-sicher. Wenn das Skript ohne Cleanup-Handler endet, bleibt das Verzeichnis zurück und blockiert alle nachfolgenden Ausführungen. Der Ausweg ist die PID-im-Verzeichnis-Methode: Das Lock-Verzeichnis enthält eine Datei mit der PID des Lock-Inhabers, was stale Lock Erkennung ermöglicht. Das Muster eignet sich besonders für portierbare Skripte, die auf verschiedenen Systemen ohne garantierten flock-Zugriff laufen müssen, oder für NFS-Shares, auf denen flock aufgrund von NFS-Einschränkungen nicht zuverlässig funktioniert.
8. Locking in Cron-Jobs und Deployment-Pipelines
Locking in Bash ist in Cron-Jobs besonders kritisch, weil der Cron-Daemon keine inhärente Serialisierung bietet. Wenn ein Job länger läuft als sein Ausführungsintervall, startet Cron eine neue Instanz, ohne auf das Ende der laufenden zu warten. Das Muster für Cron-sichere Locking: Ein Non-Blocking flock am Skriptanfang, das bei fehlgeschlagenem Lock sofort mit Exit-Code 0 abbricht (nicht 1, um keine Cron-Fehler-E-Mails zu generieren) und eine kurze Meldung in eine Logdatei schreibt. So ist die übersprungene Ausführung dokumentiert, ohne den Cron-Daemon zu alarmieren.
In CI/CD-Pipelines gibt es einen anderen Kontext: Hier wird oft gewünscht, dass ein zweiter Deployment-Lauf wartet, bis der erste abgeschlossen ist, statt einfach abzubrechen. Das Timeout-Muster von flock ist dafür geeignet: flock -w 300 9 wartet bis zu 5 Minuten. Kombiniert mit einer Fortschrittsanzeige (periodisches echo im Hintergrund, das beim Warten aktiv ist) verhindert es, dass CI-Systeme den Job wegen fehlender Ausgabe als hängend markieren. Das Muster ist vollständig und berücksichtigt sowohl die Anforderung an atomares Locking als auch die CI-spezifischen Verhaltensanforderungen.
9. Locking-Methoden im direkten Vergleich
Jede Locking-Methode in Bash hat spezifische Stärken und Einschränkungen. Die Wahl des richtigen Musters hängt von der Umgebung, den Anforderungen an crash-Sicherheit und der gewünschten Verhalten bei konkurrierenden Instanzen ab.
| Methode | Atomar? | Crash-sicher? | NFS-tauglich? | Empfehlung |
|---|---|---|---|---|
| flock (FD) | Ja (Kernel) | Ja (auto-release) | Eingeschränkt | Erste Wahl auf Linux |
| PID-File | Nein (TOCTOU) | Nein (stale) | Bedingt | Nur mit stale detection |
| mkdir | Ja (POSIX) | Nein (cleanup nötig) | Ja (POSIX) | Portabler Fallback |
| ln -s (symlink) | Ja (POSIX) | Nein (cleanup nötig) | Ja | Veraltet, selten nötig |
| lockfile (procmail) | Ja | Nein | Ja | Legacy, procmail-abhängig |
Die Empfehlung für den Produktionseinsatz ist klar: flock auf Linux-Systemen für alle lokalen Dateisysteme. mkdir als portabler Fallback für Systeme ohne flock oder für NFS-Shares. PID-Files nur in Kombination mit stale detection und zusätzlichem flock für die Race-Condition-freie Erstellung. In Docker-Containern und CI-Umgebungen ist flock in den meisten Basis-Images verfügbar und immer die erste Wahl.
Mironsoft
Shell-Automatisierung, robuste Skripte und Deployment-Infrastruktur
Bash-Skripte gegen Race Conditions und Mehrfachausführung absichern?
Wir analysieren bestehende Shell-Skripte auf Race Conditions und unsichere Locking-Muster und ersetzen fragile PID-File-Ansätze durch atomares flock-basiertes Locking für Cron, CI/CD und Deployment-Workflows.
Race-Condition-Analyse
Skripte auf TOCTOU-Schwachstellen und unsichere Locking-Muster prüfen
flock-Migration
PID-File-Muster durch atomares flock-Locking ersetzen und testen
Cron & CI-Härtung
Locking-Strategien für Cron-Jobs und parallele CI-Pipelines implementieren
10. Zusammenfassung
Locking in Bash mit flock löst das grundlegende Problem der Race Condition bei Mehrfachausführung durch atomares, kernel-gestütztes Sperren. Das PID-File-Muster ist weit verbreitet, hat aber strukturelle Schwächen: keine Atomarität bei der Erstellung, kein automatisches Aufräumen bei Crashes, und PID-Recycling kann stale locks fälschlich als gültig erscheinen lassen. flock -n 9 auf einem geöffneten File-Descriptor ist das zuverlässige Gegenmodell: atomar, crash-sicher, kein manuelles Cleanup nötig. Die Lock-Datei dient nur als Anker – ihr Inhalt ist irrelevant.
Für Cron-Jobs ist das Non-Blocking-Muster mit Exit-Code 0 bei fehlgeschlagenem Lock die richtige Wahl – keine Fehler-E-Mails, aber Logging. Für CI/CD-Pipelines bietet das Timeout-Muster eine elegante Wartemöglichkeit. Auf NFS oder in Umgebungen ohne flock ist mkdir das atomare POSIX-Fallback. Wer alle drei Szenarien in Deployment-Workflows abdecken muss, implementiert eine Locking-Funktion, die je nach Systemverfügbarkeit zwischen flock und mkdir wählt.
Locking in Bash — Das Wichtigste auf einen Blick
flock — die erste Wahl
exec 9>lockfile; flock -n 9 || exit 0 – atomar, crash-sicher, kein Cleanup nötig. Kernel gibt Sperre bei Prozessende automatisch frei.
PID-Files nur mit Stale Detection
kill -0 $PID prüft Existenz. Prozessname vergleichen gegen PID-Recycling. Alterscheck für gesteckte Prozesse. Niemals ohne diese drei Prüfungen verwenden.
Race Conditions verstehen
Check-then-act ohne Atomarität erzeugt immer eine Race Condition. TOCTOU-Fenster ist meist Millisekunden klein, tritt aber bei Last reproduzierbar auf.
mkdir als Fallback
POSIX-atomar, NFS-tauglich. Aber nicht crash-sicher: trap 'rmdir lockdir' EXIT ist Pflicht. PID in Datei im Verzeichnis für stale detection.