- Pipelines, die langsam eintreffende Ausgaben über mehrere Befehle verbinden, etwa
tail -f /some/log/file | grep thing1 | grep thing2, bleiben nicht wirklich stehen; sie können leer wirken, weil ein Zwischenbefehl Ausgaben im Puffer sammelt grepund viele Programme prüfen mitisatty, ob stdout ein Terminal ist; bei einem Terminal nutzen sie Zeilenpufferung, bei einer Pipe oder Datei ungefähr Blockpufferung in 8-KB-Einheitentail,catundteesind Beispiele für Befehle ohne Ausgabepufferung; Optionen zur Verringerung der Pufferung unterscheiden sich jedoch je nach Befehl, etwagrep --line-buffered,sed -u,tcpdump -l,jq -u,tr -u- Wenn man eine Pipeline mit
Ctrl-Cabbricht, können Ausgaben verschwinden, die noch im Puffer eines Programms wietcpdumplagen; beendet man es mitkill -TERM $PID, kann der Puffer geflusht werden und die Ausgabe sichtbar werden - Praktische Lösungen sind der Wechsel zu schnell beendeten Befehlen,
grep --line-buffered, ein einzelnesawkoder ein komplexeresgrep,stdbuf,unbufferusw.; bei allen sollte man jedoch Funktionsbedingungen und Nebenwirkungen prüfen
Warum Pipes so wirken, als würden sie hängen
- Wenn Zeilen nur langsam an eine Logdatei angehängt werden, zeigt die folgende Pipeline möglicherweise keine Ausgabe, obwohl es Treffer gibt
tail -f /some/log/file | grep thing1 | grep thing2
- Ursache ist nicht die Pipe selbst, sondern dass das zwischengeschaltete
grep thing1Ergebnisse nicht sofort schreibt, sondern im Puffer speichert - Würde ein Programm jedes Mal sofort schreiben, nähme die Zahl der Systemaufrufe zu; daher sammelt es aus Performancegründen eine gewisse Datenmenge und schreibt sie erst dann in eine Pipe oder Datei
- In diesem Beispiel kann
grep thing1warten, bis sich ungefähr 8KB Ausgabe angesammelt haben; bei einem langsamen Log kann diese Bedingung praktisch nie eintreten
Unterschiedliche Ausgabeweise bei Terminal und Pipe
tail -f file | grep thingfunktioniert gut, aber hängt man ein zweitesgrepdahinter, kann es so aussehen, als bliebe die Ausgabe stehengrepund viele Programme prüfen mit der Funktionisatty, ob stdout ein Terminal ist- Wenn stdout ein Terminal ist, verwenden sie Zeilenpufferung und geben zeilenweise sofort aus
- Wenn stdout eine Pipe oder Datei ist, verwenden sie Blockpufferung und geben erst aus, wenn sich Daten oberhalb einer bestimmten Größe angesammelt haben
- Schreibt
grepalso direkt ins Terminal, erscheinen Zeilen sofort; schreibt es in eine Pipe zum nächsten Befehl, sind sie möglicherweise nicht sichtbar - Die Puffergröße unterscheidet sich je nach Programm
- Bei
grepübernimmt libc die Pufferung, und die Größe von libc ist über die VariableBUFSIZdefiniert - Die Definition in glibc findet sich in stdio.h
- Bei
- Dass beim Schreiben in ein Terminal kein 8KB-Ausgabepuffer verwendet wird, ist kein Naturgesetz; ein Programm könnte es so implementieren, wenn es wollte, aber das käme einem sehr ungewöhnlichen Verhalten nahe
Unterschiedliches Pufferungsverhalten je nach Befehl
- Ausgabepufferung ist deshalb schwierig, weil Nutzer sich merken müssen, welche Befehle bei Pipe-Ausgabe puffern
- Beispiele für Befehle ohne Ausgabepufferung sind:
tailcattee
- Häufige Befehle, die beim Schreiben in eine Pipe Ausgaben puffern, und Möglichkeiten zur Abmilderung sind:
grep:--line-bufferedsed:-uawk: Funktionfflush()tcpdump:-ljq:-utr:-ucut: Pufferung kann nicht deaktiviert werden
- Bei Befehlen wie
sort, die erst arbeiten können, nachdem sie die gesamte Eingabe erhalten haben, ist die Frage der Pufferung praktisch nicht relevant - Es wurde versucht, sowohl Mac-OS- als auch GNU-Versionen zu testen, aber wegen der vielen Varianten können einzelne Fehler enthalten sein
Auch Standardausgaben von Programmiersprachen puffern
- Die standardmäßige
print-Ausgabe mancher Programmiersprachen puffert ebenfalls, wenn in eine Pipe geschrieben wird - Methoden zum Deaktivieren je nach Sprache:
- C:
setvbuf - Python:
python -u,PYTHONUNBUFFERED=1,sys.stdout.reconfigure(line_buffering=False),print(x, flush=True) - Ruby:
STDOUT.sync = true - Perl:
$| = 1
- C:
- Dieses Standardverhalten scheint darauf ausgelegt zu sein, die Standardausgabefunktionen bei Batch-Verarbeitung schneller zu machen
- Je nach Ausgabemethode kann sich unterscheiden, ob gepuffert wird
- In C++ puffert
cout << "hello\n"beim Schreiben in eine Pipe cout << "hello" << endlflusht die Ausgabe
- In C++ puffert
Unterschiede bei Ctrl-C und Datei-Redirects
- Wenn man wie folgt die Ausgabe von
tcpdumpangrepweiterleitet und-lvergisst, kann Ausgabe im Puffer verbleibensudo tcpdump -ni any port 53 | grep example.com
- Idealerweise könnte man erwarten, dass
tcpdumpbeim Drücken vonCtrl-Cden Puffer flusht undgrepsucht, sodass die fehlende Ausgabe sichtbar wird - In der Praxis geht die Ausgabe, die im Puffer von
tcpdumplag, beim Beenden der Programme verloren - Eine Prüfung mit
stracezeigte, dassgrepvortcpdumpSIGINTerhält; selbst wenntcpdumpalso flushen möchte, kanngrepbereits beendet sein - Als Workaround kann man die PID von
tcpdumpermitteln undkill -TERM $PIDausführen; dann kanntcpdumpden Puffer flushen und die Ausgabe sichtbar werden - Auch Datei-Redirects puffern
sudo tcpdump -ni any port 53 > output.txt
- Anders als bei dem Problem, dass
Ctrl-Cden Pufferinhalt vollständig verwerfen kann, wird bei Datei-Redirects der Pufferinhalt erfahrungsgemäß oft vor dem Programmende in die Datei geschrieben - Ob man sich immer auf dieses Verhalten verlassen kann, ist unklar
Fünf Wege, Pufferung zu vermeiden
-
Zu einem schnell beendeten Programm wechseln
- Man kann die Situation selbst vermeiden, in der langsam in eine Pipe geschrieben wird, und zu einem schnell beendeten Befehl wechseln
- Beispiel:
cat /some/log/file | grep thing1 | grep thing2 | tail
- Das verhält sich nicht genauso wie der ursprüngliche
tail -f-Befehl, kann aber komplexe Pufferungsprobleme vermeiden
-
Die Zeilenpuffer-Option von
grepverwendengrephat ein Flag, um Pufferung zu vermeiden- Beispiel:
tail -f /some/log/file | grep --line-buffered thing1 | grep thing2
-
Mit
awkoder einem komplexerengrepzusammenfassen- Situationen mit mehreren
grep-Aufrufen lassen sich durch ein einzelnesawkersetzentail -f /some/log/file | awk '/thing1/ && /thing2/'
- Oder durch
grepmit einer komplexeren Regextail -f /some/log/file | grep -E 'thing1.*thing2'
- Da auch
awkpuffert, mussawkder letzte Befehl in der Pipeline sein, damit diese Methode funktioniert
- Situationen mit mehreren
-
stdbufverwendenstdbufnutztLD_PRELOAD, um die Pufferung von libc abzuschalten- Beispiel zum Abschalten der Ausgabepufferung:
tail -f /some/log/file | stdbuf -o0 grep thing1 | grep thing2
- Wie bei Lösungen auf Basis von
LD_PRELOADist die Zuverlässigkeit begrenzt- Funktioniert nicht bei statischen Binaries
- Funktioniert möglicherweise nicht, wenn das Programm keine libc-Pufferung verwendet
- Funktioniert unter Mac OS nicht immer
- Eine verwandte Erklärung ist Harry Marrs How stdbuf works
-
unbufferverwendenunbuffer programerzwingt, dass die Programmausgabe wie ein TTY wirkt, sodass wie bei einem normalen TTY weniger gepuffert wird und etwa farbige Ausgabe verwendet wird- Beispiel:
tail -f /some/log/file | unbuffer grep thing1 | grep thing2
- Anders als
stdbuffunktioniert es immer, kann aber unerwünschte Nebenwirkungen haben- Zum Beispiel könnte
grep thing1Treffer farbig markieren
- Zum Beispiel könnte
unbufferist im Paketexpectenthalten
Situationen, in denen das Problem vor allem auftritt, und Idee einer Umgebungsvariable
- Dieses Problem tritt vor allem bei Programmen auf, die Daten langsam in eine Pipe fließen lassen
- Beispiele:
tcpdumptail -f- Log-Beobachtung etwa mit
kubectl logs - Ausgaben langsamer Berechnungen
- Eine standardisierte Umgebungsvariable zum Abschalten der Pufferung, ähnlich wie Pythons
PYTHONUNBUFFERED, könnte nützlich sein - Die Idee stammt aus Mark Dominus’ Blogbeitrag von 2018 und dem Folgebeitrag
- Als Name wäre analog zu
NO_COLORetwaNO_BUFFERdenkbar - Das Design ist schwierig
- NETBSD hat Umgebungsvariablen wie
STDBUF,STDBUF1usw., die viel Kontrolle bieten - Die meisten Entwickler möchten für einen vergleichsweise kleinen Edge Case möglicherweise nicht mehrere Umgebungsvariablen implementieren
- NETBSD hat Umgebungsvariablen wie
- Es wäre auch interessant, ob es Programme gibt, die Ausgabepuffer automatisch in Intervallen wie einer Sekunde flushen; mir fällt jedoch keines ein, und es könnte Nachteile haben
Nicht behandelte Bereiche
- Der Unterschied zwischen Zeilenpufferung und vollständig ungepufferter Ausgabe wird ausgelassen
- Der Unterschied zwischen stderr-Pufferung und stdout-Pufferung wird ausgelassen
- Dieser Inhalt behandelt nur Pufferung, die innerhalb von Programmen entsteht
- Auch der TTY-Treiber des Betriebssystems puffert gelegentlich ein wenig
- Andere Gründe außerhalb des Schreibens in eine Pipe, warum Ausgaben geflusht werden müssen, werden ausgelassen
1 Kommentare
Hacker-News-Kommentare
Ein gepufferter Ansatz sollte fast immer nach dem Prinzip „Schwellenwert oder Timeout“ arbeiten: Flush, wenn ein Byte-Schwellenwert erreicht ist oder nach einer bestimmten Zeit, sofern mindestens 1 Byte vorhanden ist.
In Hardware-Interfaces ist das ein übliches Muster, um ähnliche Probleme zu lösen.
In diesem Fall müsste die Library, die im User Space puffert, einen passenden Timer setzen, sobald sie die ersten Daten in den Puffer legt. Der Timeout-Wert könnte als Argument übergeben werden, auf für Menschen kurze 1–100 ms gesetzt werden, proportional zu
{Bandbreite / Schwellenwert}sein oder so gewählt werden, dass der System-Call-Overhead nicht mehr als 0,1 % der Gesamtzeit ausmacht.Dieser Ansatz gilt nicht nur fürs Schreiben, sondern auch fürs Lesen. Wenn man gebündelte oder zusammengeführte Reads macht, braucht man etwas Ähnliches; allerdings muss der Datenkanal eine effiziente Möglichkeit bieten, „wartende Daten“ abzufragen oder darüber benachrichtigt zu werden, weshalb das stärker vom Channel-Design abhängt. In Hardware sind Verfahren wie Interrupt Coalescing üblich.
I/O-Fehler können dann nicht nur beim Schreiben, sondern jederzeit auftreten, und verschiedene System-Calls können durch den Timer unterbrochen werden – nicht nur Stellen, an denen das Programm selbst Timer gesetzt hat oder Signale eintreffen.
Wenn sowohl die Anwendung als auch libc Timer setzen, kann das ebenfalls Verwirrung stiften. Die heutigen Kernel-Timer-APIs sehen besser aus, als ich sie in Erinnerung hatte, daher ist das vielleicht weniger relevant; aber wenn eine Anwendung in kritischen Abschnitten Signale kurz blockiert, wirkt sich das auch auf I/O-Timer aus.
Wegen Zeitpunkt und Art der Signalbehandlung müsste man beim Zugriff auf I/O-Strukturen vorsichtiger sein.
Mit io_uring und User-Space-Timern skaliert das deutlich besser, aber um sehr viele schnelle kleine Writes zu unterstützen, braucht man immer noch Tricks. Ab etwa 1 Million pro Sekunde werden beispielsweise die Kosten der Timer-Verwaltung zunehmend sichtbar, und um bis auf 100 Millionen Writes pro Sekunde zu kommen, waren ziemlich ungewöhnliche Techniken nötig.
Die Ursache des Problems liegt darin, dass Interaktives mit einem Vertrag vermischt wird, der nicht auf Interaktion ausgelegt ist. Zum Beispiel, wenn man die Follow-Ausgabe von
tailin eine Pipe schickt.Ich sehe kein echtes Problem, das gelöst werden müsste. Als Hardware-Analogie wäre das ein Wassertank, der Regenwasser sammelt und erst dann weiterleitet, wenn er voll ist. Ich weiß nicht, welche Beispiele gemeint sind, aber soweit ich weiß, ist zeitbasiertes Flushen in Hardware nicht üblich.
Der vorgeschlagene Fix macht den Vertrag deutlich komplizierter.
Das Problem liegt weniger in der Semantik selbst als darin, die Semantik nicht zu kennen.
Ich arbeite seit über 20 Jahren mit NIX-Systemen und weiß, dass so etwas passiert, aber jedes Mal fällt es mir erst wieder ein, nachdem ich lange gerätselt habe, warum keine Ausgabe erscheint.
Zu der Stelle „Die letzten Beiträge werden ziemlich lang; will wirklich jemand einen 3000-Wörter-Artikel über Buffering lesen?“: Ich persönlich will das lesen.
Manchmal ist ein ausufernder Text auch nur Ballast für Suchmaschinenoptimierung.
Man könnte auch Abschnitte wie TLDR und NTLDR haben, also „lang, aber gelesen“.
Es wäre schön, wenn alle Puffer geleert würden, sobald die CPU im gesamten System idle ist.
Buffering ist im Wesentlichen eine Technik, um CPU zu sparen. Wenn CPU unendlich verfügbar wäre, wäre jeder Puffer 1 Byte groß. Puffer sammeln Daten, um sie aus Effizienzgründen gebündelt zu verarbeiten.
Wenn die CPU aber idle wird, sollte keine „später zu erledigende Arbeit“ übrig bleiben. Sobald der Kernel-Scheduler idle wird, sollte er allen Prozessen ein Signal schicken: Puffer flushen.
Man würde all diese Arbeit erledigen und sie dann erneut System-Calls ausführen lassen, um ihre Puffer zu flushen. Vielleicht könnte man einen Mechanismus ergänzen, mit dem der Kernel User-Space-Puffer kennt und sie im Idle-Zustand direkt von dort holt.
Ich frage mich, ob das in gewisser Weise so etwas wie io_uring https://man7.org/linux/man-pages/man3/io_uring_register_buff... ist.
Der Artikel verwechselt zwei verschiedene Dinge: ungepuffert und zeilengepuffert.
Ungepuffert verschlechtert die Performance unnötig und kann falsche Ausgaben erzeugen, wenn mehrere Quellen in dieselbe Pipe schreiben. Ausreichend lange Zeilen werden sich ohnehin vermischen, aber die meisten realen Ausgabezeilen sind selbst mit Format-/Steuerzeichen und Zeichen aus Supplementary Planes kürzer als 4096 Byte.
Zeilenpufferung ist der Standard für Terminals und in der Regel auch bei Pipes das gewünschte Verhalten. Man kann jeden Befehl unter
stdbuf -oL -eLausführen. Die seltenen Programme, die Aktualisierungen innerhalb einer Zeile wollen, müssen ohnehin manuell flushen und verhalten sich daher auch hier korrekt.Was
stdbuftatsächlich macht, kann man etwa so sehen:env -i \command -v stdbuf` -oL -eL `command -v env``Ich habe früher schon einmal über dieses Problem geschrieben: https://world-playground-deceit.net/blog/2024/09/bourne_shel...
Bei Befehlen ohne Buffering ist das implementationsabhängig oder, im Fall von
cat, möglicherweise falsch. Siehe https://pubs.opengroup.org/onlinepubs/9799919799/utilities/c... und-u. Es ist sehr schmerzhaft, dass POSIX keinen offiziellen Weg aufgenommen hat, das zu steuern.Nicht erwähnt wurde auch Input-Buffering, das zu solchen merkwürdigen Ergebnissen führt:
$ seq 5 | { v1=$(head -1); v2=$(head -1); printf '%s=%s\n' v1 "$v1" v2 "$v2"; }v1=1v2=Die Lösung ist in diesem Fall,
stdbuf -i0 head -1zu verwenden.socketpairliest, dem schreibenden Prozess solche Einschränkungen aufzwingen kann. Außer man nutzt schwere Hacks wieptrace().Vielleicht kann man die Größe des Pipe-Buffers anpassen, aber ich kenne keine Konvention, nach der die Standard-C-Ein-/Ausgabe dem folgen müsste.
Jedenfalls scheint
stdbufin diesem Fall nicht zu helfen:$ ./a | stdbuf -i0 -- cat#include#includeint main(void) {for (;;) {printf("n");usleep(100000);}}Es gibt gute Gründe dafür, dass es Buffer gibt. Ausgaben auf den Bildschirm zu schreiben ist im Vergleich zum Schreiben in einen Buffer relativ sehr langsam.
Zeichen einzeln auszugeben ist extrem ineffizient.
Das ist ein altes Problem und begegnet einem oft beim Umgang mit UARTs. Es gibt mehrere mögliche Lösungen: ein zeilenbasierter Ansatz, bei dem ein spezielles Zeichen wie ein Zeilenumbruch das Ende der Ausgabe markiert; ein längenbasierter Ansatz, bei dem man wartet, bis eine Länge wie 8 KB erreicht ist; oder ein zeitbasierter Ansatz, bei dem alle X Millisekunden ausgegeben wird.
Jeder Ansatz hat Vor- und Nachteile, und was optimal ist, hängt von der Anwendung ab. Ich halte die Stelle im Artikel, an der behauptet wird, manche Programme verwendeten kein Buffering, für falsch. Diese Programme verwenden lediglich offensichtlich keinen längenbasierten Ansatz.
Der zeilenbasierte Ansatz ist so eine Methode, erfordert aber eine Einigung darüber, welches Zeichen verwendet wird. Üblicherweise ist das der Zeilenumbruch.
/dev/nullzu machen, kann die Performance massiv ruinieren.Ich benutze Unix seit über 35 Jahren, habe aber nie vollständig verstanden, wie das funktioniert.
Es war gut, eine Gesamtbeschreibung des Buffering-Verhaltens über mehrere Systeme und Komponenten hinweg zu bekommen, und ich habe definitiv etwas gelernt.
Zu der Stelle „Wenn man in einer Pipe Ctrl-C drückt, geht der Buffer-Inhalt verloren“: Ich würde vermuten, dass die meisten Programme bei SIGINT den Buffer flushen.
Damit das in der Shell so funktioniert, müsste SIGINT allerdings nur an das erste Programm in der Pipeline weitergegeben werden; vermutlich ist das in der Praxis aber nicht so.
sigintbekommt und die übrigensigpipe.