Prozesse verbinden, ohne eine Zwischendatei auf die Platte zu schreiben
Eine Named Pipe (FIFO) verhält sich im Dateisystem wie eine Datei, funktioniert aber wie eine klassische Pipe: Daten fließen direkt von einem Schreiber zu einem Leser, ohne jemals auf der Platte zu landen. Wer FIFOs richtig einsetzt, entkoppelt unabhängige Prozesse, baut Logging-Multiplexer und vermeidet Race Conditions, die bei Zwischendateien fast unvermeidlich sind.
Inhaltsverzeichnis
- 1. Was eine FIFO ist und wie sie sich von einer normalen Pipe unterscheidet
- 2. mkfifo: eine benannte Pipe anlegen und ihre Lebensdauer verstehen
- 3. Prozesse über eine FIFO verbinden: Schreiber und Leser entkoppeln
- 4. Blockierendes Verhalten: warum ein einfaches open() auf einer FIFO hängen bleibt
- 5. FIFOs für Logging-Pipelines und Multiplexing nutzen
- 6. FIFOs vs. Process Substitution: wann welches Werkzeug
- 7. Fehlerbehandlung und Aufräumen: trap, Race Conditions, Deadlocks vermeiden
- 8. Sicherheitsaspekte: FIFOs in gemeinsam genutzten Verzeichnissen
- 9. FIFOs im Vergleich zu anderen IPC-Mechanismen
- 10. Zusammenfassung
- 11. FAQ
1. Was eine FIFO ist und wie sie sich von einer normalen Pipe unterscheidet
Eine Named Pipe (kurz FIFO, First In First Out) ist ein besonderer Dateityp, den das Dateisystem verwaltet, aber niemals tatsächlich Bytes auf die Platte schreibt. Im Unterschied zur anonymen Pipe, die eine Shell mit | zwischen zwei Befehlen erzeugt und die nur innerhalb einer Prozessfamilie existiert, hat eine FIFO einen sichtbaren Namen im Dateisystem und kann von völlig unabhängigen Prozessen geöffnet werden, solange diese Lese- und Schreibrechte auf den Pfad haben.
Das macht die FIFO zum richtigen Werkzeug, wenn zwei Skripte kommunizieren sollen, die nicht in derselben Pipe-Kette stehen, zum Beispiel ein Daemon, der Log-Zeilen schreibt, und ein zweites, unabhängig gestartetes Skript, das diese Zeilen live verarbeitet. Eine Zwischendatei würde hier funktionieren, bringt aber unnötige Festplatten-I/O, Race Conditions beim gleichzeitigen Lesen und Schreiben, und das Problem, alte Daten irgendwann aufzuräumen. Eine Named Pipe löst alle drei Probleme gleichzeitig, weil sie rein im Kernel-Puffer lebt.
2. mkfifo: eine benannte Pipe anlegen und ihre Lebensdauer verstehen
Das Kommando mkfifo erzeugt eine FIFO an einem beliebigen Pfad im Dateisystem. Ein ls -l zeigt sie mit dem Dateityp-Buchstaben p am Zeilenanfang, und ihre Größe bleibt konstant bei null, weil niemals Daten dauerhaft gespeichert werden. Die Lebensdauer der FIFO als Dateisystem-Eintrag ist unabhängig vom Datenfluss: Sie existiert weiter, bis sie explizit mit rm gelöscht wird, auch wenn kein Prozess gerade liest oder schreibt.
Ein häufiger Anfängerfehler ist, eine Named Pipe im temporären Verzeichnis anzulegen und zu vergessen, dass sie bei jedem erneuten Skriptlauf schon existieren könnte. Deshalb prüft ein robustes Bash-Pattern vor dem Anlegen, ob der Pfad schon eine FIFO ist, statt mkfifo blind aufzurufen und bei einem Fehler abzubrechen.
#!/usr/bin/env bash
set -euo pipefail
readonly PIPE_PATH="/tmp/mironsoft-log-pipe.$$"
# Idempotent creation: only create if it does not already exist as a FIFO
if [[ ! -p "$PIPE_PATH" ]]; then
mkfifo -m 600 "$PIPE_PATH"
fi
# Always clean up, no matter how the script exits
trap 'rm -f "$PIPE_PATH"' EXIT
ls -l "$PIPE_PATH"
# prow------- 1 deploy deploy 0 Aug 6 10:00 /tmp/mironsoft-log-pipe.12345
3. Prozesse über eine FIFO verbinden: Schreiber und Leser entkoppeln
Sobald die FIFO existiert, kann ein beliebiger Prozess hineinschreiben und ein beliebiger anderer Prozess daraus lesen, solange beide denselben Pfad kennen. Das entkoppelt Schreiber und Leser vollständig voneinander: Sie müssen nicht zur selben Zeit gestartet werden, nicht denselben Elternprozess haben und nicht einmal in derselben Programmiersprache geschrieben sein. Ein Python-Daemon kann in dieselbe Named Pipe schreiben, aus der ein Bash-Skript liest.
Der Datenfluss selbst verhält sich exakt wie bei einer anonymen Pipe: Was der Schreiber mit > in die FIFO schickt, kommt beim Leser mit < in derselben Reihenfolge an, bis zu der Puffergröße des Kernels (typischerweise 64 KB unter Linux). Überschreitet der Schreiber diese Grenze, bevor der Leser liest, blockiert der Schreib-Aufruf, bis wieder Platz im Puffer ist. Das ist kein Fehler, sondern das gewünschte Backpressure-Verhalten einer FIFO.
#!/usr/bin/env bash
set -euo pipefail
readonly PIPE_PATH="/tmp/mironsoft-events.pipe"
[[ -p "$PIPE_PATH" ]] || mkfifo "$PIPE_PATH"
# Reader: runs in the background, processes every line as it arrives
process_events() {
while IFS= read -r line; do
echo "[EVENT] $line"
done < "$PIPE_PATH"
}
process_events &
reader_pid=$!
# Writer: an unrelated process, started independently, feeds the FIFO
{
echo "deploy.started"
sleep 1
echo "deploy.finished"
} > "$PIPE_PATH"
wait "$reader_pid"
rm -f "$PIPE_PATH"
4. Blockierendes Verhalten: warum ein einfaches open() auf einer FIFO hängen bleibt
Der wichtigste Unterschied zu einer normalen Datei zeigt sich beim Öffnen: Ein Prozess, der eine FIFO nur zum Lesen öffnet (< pipe), blockiert so lange, bis mindestens ein anderer Prozess sie zum Schreiben öffnet, und umgekehrt. Wer aus Versehen ein Skript schreibt, das eine FIFO liest, ohne dass jemals ein Schreiber startet, sieht ein scheinbar eingefrorenes Skript, obwohl kein Fehler vorliegt. Das ist die häufigste Quelle von Verwirrung bei Einsteigern.
Ein bewährtes Bash-Pattern, um dieses Blockieren zu umgehen, ist, die FIFO mit File-Descriptor-Umleitung sowohl zum Lesen als auch zum Schreiben zu öffnen (exec 3<>"$PIPE_PATH"). Bash öffnet den Descriptor dann im Lese-Schreib-Modus, was den Kernel dazu bringt, sofort zurückzukehren, weil aus Sicht des Kernels bereits ein Schreiber (der Prozess selbst) vorhanden ist. Dieses Muster ist besonders nützlich für Heartbeat-Skripte, die eine FIFO offen halten wollen, ohne auf einen externen Schreiber zu warten.
#!/usr/bin/env bash
set -euo pipefail
readonly PIPE_PATH="/tmp/mironsoft-heartbeat.pipe"
[[ -p "$PIPE_PATH" ]] || mkfifo "$PIPE_PATH"
# Open for BOTH read and write on fd 3 -- avoids the "wait for a writer" block
exec 3<>"$PIPE_PATH"
# Now a plain read never blocks forever, because fd 3 counts as a writer too
read -t 2 -u 3 line && echo "Received: $line" || echo "No data within timeout"
exec 3>&- # close fd 3
rm -f "$PIPE_PATH"
5. FIFOs für Logging-Pipelines und Multiplexing nutzen
Ein produktiver Einsatzzweck für Named Pipes ist das Multiplexen mehrerer Log-Quellen in einen einzigen Verarbeitungsprozess. Statt jedes Skript einer Deployment-Pipeline seine eigene Logdatei schreiben zu lassen, schreiben alle Skripte in dieselbe FIFO, und ein zentraler Log-Collector liest daraus, versieht jede Zeile mit Zeitstempel und Quelle und leitet sie an ein Monitoring-System weiter.
Diese Architektur skaliert gut, solange klar ist, dass mehrere gleichzeitige Schreiber auf dieselbe FIFO ihre Zeilen ineinander verschachteln können, wenn eine Zeile länger als der atomare Schreibpuffer von PIPE_BUF (unter Linux 4096 Bytes) ist. Für normale Log-Zeilen ist das selten ein Problem, aber wer sehr lange, einzeilige JSON-Events schreibt, sollte das im Hinterkopf behalten oder auf ein echtes Message-Queue-System ausweichen, sobald die Zeilenlänge diese Grenze regelmäßig überschreitet.
6. FIFOs vs. Process Substitution: wann welches Werkzeug
Bash bietet mit Process Substitution (<(befehl) und >(befehl)) eine syntaktische Abkürzung, die unter der Haube in vielen Fällen exakt eine anonyme FIFO-artige Konstruktion erzeugt, auf Linux tatsächlich über /dev/fd/N. Der Unterschied: Process Substitution ist an die Lebensdauer des aufrufenden Befehls gebunden und braucht keinen expliziten mkfifo-Aufruf, während eine echte Named Pipe unabhängig von einem einzelnen Befehlsaufruf existiert und von komplett unabhängig gestarteten Prozessen genutzt werden kann.
Als Faustregel gilt: Process Substitution eignet sich, wenn Schreiber und Leser im selben Skript, in derselben Ausführung entstehen, etwa um zwei Kommando-Ausgaben mit diff <(sort a) <(sort b) zu vergleichen. Eine echte FIFO mit mkfifo ist dagegen Pflicht, wenn Schreiber und Leser in getrennten Prozessaufrufen, getrennten Cron-Jobs oder sogar getrennten Containern laufen und sich nur über einen gemeinsam bekannten Dateisystempfad finden.
7. Fehlerbehandlung und Aufräumen: trap, Race Conditions, Deadlocks vermeiden
Weil eine FIFO als Dateisystem-Eintrag bestehen bleibt, bis sie gelöscht wird, gehört trap 'rm -f "$PIPE_PATH"' EXIT in jedes Skript, das mit mkfifo arbeitet, genau wie bei temporären Dateien. Ohne diesen Trap sammeln sich verwaiste FIFOs im temporären Verzeichnis an, besonders wenn das Skript durch ein Signal oder einen Fehler abbricht, bevor es die reguläre Cleanup-Logik erreicht.
Ein Deadlock entsteht, wenn ein Skript eine Named Pipe zum Schreiben öffnet und dabei selbst der einzige Prozess ist, der sie auch lesen könnte, aber sequenziell erst nach dem Schreiben lesen will. Da das Öffnen zum Schreiben blockiert, bis ein Leser existiert, wartet das Skript auf sich selbst. Die Lösung ist entweder, den Leser in einem Hintergrundprozess (&) parallel zu starten, oder wie im vorherigen Abschnitt gezeigt, die FIFO im kombinierten Lese-Schreib-Modus zu öffnen, um das Blockieren von vornherein zu vermeiden.
8. Sicherheitsaspekte: FIFOs in gemeinsam genutzten Verzeichnissen
Wird eine FIFO in einem für mehrere Benutzer schreibbaren Verzeichnis wie /tmp angelegt, kann ein anderer lokaler Nutzer denselben Pfad vorab mit mkfifo belegen (Symlink- oder Preallocation-Angriff), sodass das eigentliche Skript unwissentlich mit den Daten eines fremden Prozesses arbeitet. Der Aufruf mkfifo schlägt zwar fehl, wenn der Pfad schon existiert, aber ein Skript, das diesen Fehler ignoriert und einfach mit der vorhandenen Datei weiterarbeitet, öffnet eine Sicherheitslücke.
Das sichere Bash-Pattern kombiniert einen mit mktemp -u erzeugten, garantiert eindeutigen Pfad mit restriktiven Rechten direkt beim Anlegen (mkfifo -m 600), sodass nur der eigene Benutzer lesen und schreiben darf. Für produktive Deployment-Skripte sollte die FIFO außerdem in einem Verzeichnis liegen, das ausschließlich dem ausführenden Service-Account gehört, nicht im global schreibbaren /tmp.
9. FIFOs im Vergleich zu anderen IPC-Mechanismen
Named Pipes sind nur eine von mehreren Möglichkeiten, wie unabhängige Prozesse unter Linux Daten austauschen. Die Wahl des richtigen Mechanismus hängt davon ab, ob Daten nur einmal fließen oder dauerhaft gespeichert werden sollen, ob mehrere Leser gleichzeitig zugreifen müssen und wie groß die zu übertragende Datenmenge ist.
| Mechanismus | Persistenz | Mehrere Leser | Typischer Einsatz |
|---|---|---|---|
mkfifo (Named Pipe) |
Nein, reiner Kernel-Puffer | Nein, ein aktiver Leser | Zwei unabhängige Prozesse live verbinden |
| Zwischendatei | Ja, bleibt auf Platte | Ja | Batch-Verarbeitung, Debugging, Audit-Trail |
| Process Substitution | Nein | Nein | Schreiber/Leser im selben Skriptaufruf |
| Unix Domain Socket | Nein | Ja, mehrere Verbindungen | Strukturierte Nachrichten, viele Clients |
| Message Queue (Redis, RabbitMQ) | Optional | Ja, mit Consumer-Groups | Verteilte Systeme, mehrere Hosts |
Mironsoft
Shell-Automatisierung, DevOps-Tooling und Deployment-Infrastruktur
Shell-Skripte, die in der Produktion zuverlässig laufen?
Wir analysieren bestehende Bash-Skripte, erkennen fragile Muster und ersetzen sie durch robuste Bash-Patterns: mit vollständiger Fehlerbehandlung, Logging und sicherer Parallelisierung für euren Deployment-Stack.
Code-Review
ShellCheck-Analyse und manuelle Prüfung auf kritische Bash-Pattern-Verstöße.
Refactoring
Fehlerbehandlung, Logging und sichere Dateioperationen nachrüsten.
CI-Integration
ShellCheck und BATS in Pipelines integrieren und Regressionstests aufbauen.
10. Zusammenfassung
Named Pipes (FIFOs) in Bash: Das Wichtigste auf einen Blick
Grundprinzip
mkfifo legt einen benannten Dateisystem-Eintrag an, der Daten nur im Kernel-Puffer hält, niemals dauerhaft auf der Platte.
Blockierendes Öffnen
Lesen blockiert ohne Schreiber und umgekehrt. exec 3<>pipe öffnet lese- und schreibbar zugleich und vermeidet Deadlocks.
Aufräumen
trap 'rm -f "$PIPE_PATH"' EXIT gehört in jedes Skript, das mkfifo verwendet, um verwaiste FIFOs zu vermeiden.
Sicherheit
mkfifo -m 600 mit mktemp-erzeugtem Pfad statt fester /tmp-Namen verhindert Preallocation-Angriffe durch andere lokale Nutzer.