Was verhindert, dass stdout / stderr verschachtelt?

13

Angenommen, ich führe einige Prozesse aus:

#!/usr/bin/env bash

foo &
bar &
baz &

wait;

Ich führe das obige Skript folgendermaßen aus:

foobarbaz | cat

Wenn einer der Prozesse nach stdout / stderr schreibt, verschachtelt sich die Ausgabe meines Erachtens nie - jede Zeile von stdio scheint atomar zu sein. Wie funktioniert das? Welches Dienstprogramm steuert, wie jede Zeile atomar ist?

Alexander Mills
quelle
3
Wie viele Daten geben Ihre Befehle aus? Versuchen Sie, sie einige Kilobyte ausgeben zu lassen.
Kusalananda
Du meinst, wo einer der Befehle ein paar kb vor einer Newline ausgibt?
Alexander Mills
Nein, ungefähr
muru

Antworten:

22

Sie verschachteln sich! Sie haben nur kurze Output-Bursts ausprobiert, die ungeteilt bleiben. In der Praxis kann jedoch nur schwer garantiert werden, dass ein bestimmter Output ungeteilt bleibt.

Ausgabepufferung

Es kommt darauf an, wie die Programme ihre Ausgabe puffern . Die stdio-Bibliothek , die die meisten Programme beim Schreiben verwenden, verwendet Puffer, um die Ausgabe effizienter zu gestalten. Anstatt Daten auszugeben, sobald das Programm eine Bibliotheksfunktion zum Schreiben in eine Datei aufruft, speichert die Funktion diese Daten in einem Puffer und gibt sie tatsächlich erst dann aus, wenn der Puffer voll ist. Dies bedeutet, dass die Ausgabe in Chargen erfolgt. Genauer gesagt gibt es drei Ausgabemodi:

  • Unbuffered: Die Daten werden sofort ohne Verwendung eines Puffers geschrieben. Dies kann langsam sein, wenn das Programm seine Ausgabe in kleinen Stücken schreibt, z. B. zeichenweise. Dies ist der Standardmodus für Standardfehler.
  • Voll gepuffert: Die Daten werden nur geschrieben, wenn der Puffer voll ist. Dies ist der Standardmodus beim Schreiben in eine Pipe oder in eine reguläre Datei, außer mit stderr.
  • Zeilenweise gepuffert: Die Daten werden nach jeder neuen Zeile oder wenn der Puffer voll ist, geschrieben. Dies ist der Standardmodus beim Schreiben in ein Terminal, außer mit stderr.

Programme können jede Datei neu programmieren, um sich anders zu verhalten, und können den Puffer explizit leeren. Der Puffer wird automatisch geleert, wenn ein Programm die Datei schließt oder normal beendet.

Wenn alle Programme, die in dieselbe Pipe schreiben, entweder den zeilengepufferten Modus oder den ungepufferten Modus verwenden und jede Zeile mit einem einzigen Aufruf an eine Ausgabefunktion schreiben und die Zeilen kurz genug sind, um in einen einzelnen Block zu schreiben, dann Die Ausgabe ist eine Verschachtelung ganzer Zeilen. Wenn jedoch eines der Programme den vollständig gepufferten Modus verwendet oder die Zeilen zu lang sind, werden gemischte Zeilen angezeigt.

Hier ist ein Beispiel, in dem ich die Ausgabe von zwei Programmen verschachtele. Ich habe GNU Coreutils unter Linux verwendet. Verschiedene Versionen dieser Dienstprogramme können sich unterschiedlich verhalten.

  • yes aaaaschreibt aaaafür immer in einem Modus, der im Wesentlichen dem zeilengepufferten Modus entspricht. Das yesDienstprogramm schreibt tatsächlich mehrere Zeilen gleichzeitig. Bei jeder Ausgabe werden jedoch eine ganze Reihe von Zeilen ausgegeben.
  • echo bbbb; done | grep bSchreibt bbbbfür immer im vollständig gepufferten Modus. Es verwendet eine Puffergröße von 8192 und jede Zeile ist 5 Byte lang. Da 5 8192 nicht teilt, befinden sich die Grenzen zwischen Schreibvorgängen im Allgemeinen nicht an einer Liniengrenze.

Lassen Sie uns sie zusammen werfen.

$ { yes aaaa & while true; do echo bbbb; done | grep b & } | head -n 999999 | grep -e ab -e ba
bbaaaa
bbbbaaaa
baaaa
bbbaaaa
bbaaaa
bbbaaaa
ab
bbbbaaa

Wie man sieht, wird ja manchmal grep unterbrochen und umgekehrt. Nur etwa 0,001% der Leitungen wurden unterbrochen, aber es ist passiert. Die Ausgabe ist randomisiert, sodass die Anzahl der Unterbrechungen variiert, aber ich habe jedes Mal mindestens ein paar Unterbrechungen gesehen. Wenn die Leitungen länger wären, gäbe es einen höheren Anteil unterbrochener Leitungen, da die Wahrscheinlichkeit einer Unterbrechung zunimmt, wenn die Anzahl der Leitungen pro Puffer abnimmt.

Es gibt verschiedene Möglichkeiten, die Ausgabepufferung anzupassen . Die wichtigsten sind:

  • Deaktivieren Sie die Pufferung in Programmen, die die stdio-Bibliothek verwenden, ohne die Standardeinstellungen mit dem Programm zu ändern stdbuf -o0 sich in GNU coreutils und einigen anderen Systemen wie FreeBSD befindet. Alternativ können Sie mit auf Zeilenpufferung umschalten stdbuf -oL.
  • Wechseln Sie zur Zeilenpufferung, indem Sie die Programmausgabe über ein Terminal leiten, das nur für diesen Zweck mit erstellt wurde unbuffer . Einige Programme verhalten sich möglicherweise anders. Beispielsweise werden grepstandardmäßig Farben verwendet, wenn es sich bei der Ausgabe um ein Terminal handelt.
  • Konfigurieren Sie das Programm zum Beispiel durch Übergeben --line-buffered an GNU grep übergeben.

Sehen wir uns den obigen Ausschnitt noch einmal an, diesmal mit Zeilenpufferung auf beiden Seiten.

{ stdbuf -oL yes aaaa & while true; do echo bbbb; done | grep --line-buffered b & } | head -n 999999 | grep -e ab -e ba
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb

Also diesmal hat ja grep nie unterbrochen, aber grep hat ja manchmal unterbrochen. Ich werde später kommen, warum.

Pipe-Interleaving

Solange jedes Programm jeweils eine Zeile ausgibt und die Zeilen kurz genug sind, werden die Ausgabezeilen sauber getrennt. Aber es gibt eine Grenze, wie lange die Leitungen sein können, damit dies funktioniert. Die Pipe selbst hat einen Transferpuffer. Wenn ein Programm auf einer Pipe ausgegeben wird, werden die Daten vom Schreibprogramm in den Übertragungspuffer der Pipe und später vom Übertragungspuffer der Pipe in das Leseprogramm kopiert. (Zumindest konzeptionell - der Kernel kann dies manchmal zu einer einzigen Kopie optimieren.)

Wenn mehr Daten kopiert werden müssen, als in den Übertragungspuffer der Pipe passen, kopiert der Kernel jeweils einen Puffer. Wenn mehrere Programme in dieselbe Pipe schreiben und das erste Programm, das der Kernel auswählt, mehr als ein Pufferprogramm schreiben möchte, kann nicht garantiert werden, dass der Kernel das gleiche Programm beim zweiten Mal erneut auswählt. Wenn beispielsweise P die Puffergröße ist, foo2 · P Bytes barschreiben und 3 Bytes schreiben möchte, dann ist eine mögliche Verschachtelung P Bytes von foo, dann 3 Bytes von barund P Bytes vonfoo .

Wenn ich auf das obige yes + grep-Beispiel zurückkehre, werden auf meinem System yes aaaaso viele Zeilen geschrieben, wie auf einmal in einen 8192-Byte-Puffer passen. Da 5 Bytes zu schreiben sind (4 druckbare Zeichen und die Newline), bedeutet dies, dass jedes Mal 8190 Bytes geschrieben werden. Die Pipe-Puffergröße beträgt 4096 Bytes. Es ist daher möglich, 4096 Bytes von yes abzurufen, dann eine Ausgabe von grep und dann den Rest des Schreibvorgangs von yes (8190 - 4096 = 4094 Bytes). 4096 Bytes lassen Platz für 819 Zeilen mit aaaaund einem Lone a. Daher eine Zeile mit diesem Lone, agefolgt von einem Schreiben von grep, wobei eine Zeile mit gegeben wird abbbb.

Wenn Sie die Details der Vorgänge anzeigen möchten, getconf PIPE_BUF .wird die Pipe-Puffergröße auf Ihrem System angezeigt und Sie können eine vollständige Liste der Systemaufrufe anzeigen, die von jedem Programm mit ausgeführt werden

strace -s9999 -f -o line_buffered.strace sh -c '{ stdbuf -oL yes aaaa & while true; do echo bbbb; done | grep --line-buffered b & }' | head -n 999999 | grep -e ab -e ba

So stellen Sie eine saubere Linienverschachtelung sicher

Wenn die Zeilenlängen kleiner als die Pipe-Puffergröße sind, stellt die Zeilenpufferung sicher, dass die Ausgabe keine gemischten Zeilen enthält.

Wenn die Zeilenlängen größer sein können, gibt es keine Möglichkeit, willkürliches Mischen zu vermeiden, wenn mehrere Programme auf dieselbe Pipe schreiben. Um die Trennung zu gewährleisten, müssen Sie jedes Programm in eine andere Pipe schreiben lassen und die Zeilen mit einem Programm kombinieren. Zum Beispiel macht GNU Parallel dies standardmäßig.

Gilles 'SO - hör auf böse zu sein'
quelle
Interessant, also was könnte ein guter Weg sein, um sicherzustellen, dass alle Zeilen catatomar geschrieben wurden, so dass der cat-Prozess ganze Zeilen von entweder foo / bar / baz, aber nicht eine halbe Zeile von einer und eine halbe Zeile von einer anderen usw. erhält. Kann ich mit dem Bash-Skript etwas anfangen?
Alexander Mills
1
klingt dies gilt für meinen fall auch wenn ich hunderte dateien hatte und awkzwei (oder mehr) ausgabezeilen für die gleiche id mit find -type f -name 'myfiles*' -print0 | xargs -0 awk '{ seen[$1]= seen[$1] $2} END { for(x in seen) print x, seen[x] }' aber find -type f -name 'myfiles*' -print0 | xargs -0 cat| awk '{ seen[$1]= seen[$1] $2} END { for(x in seen) print x, seen[x] }'damit korrekt nur eine zeile für jede id erzeugt wurde.
αғsнιη
Um jegliches Interleaving zu verhindern, kann ich das mit einer Programmierumgebung wie Node.js machen, aber mit bash / shell, nicht sicher, wie es geht.
Alexander Mills
1
@JoL Es liegt daran, dass der Pipe-Puffer voll ist. Ich wusste, dass ich den zweiten Teil der Geschichte schreiben musste… Fertig.
Gilles 'SO- hör auf böse zu sein'
1
@OlegzandrDenman TLDR hinzugefügt: Sie verschachteln. Der Grund ist kompliziert.
Gilles 'SO- hör auf böse zu sein'
1

http://mywiki.wooledge.org/BashPitfalls#Non-atomic_writes_with_xargs_-P hat Folgendes untersucht:

GNU xargs unterstützt die parallele Ausführung mehrerer Jobs. -P n wobei n die Anzahl der parallel auszuführenden Jobs ist.

seq 100 | xargs -n1 -P10 echo "$a" | grep 5
seq 100 | xargs -n1 -P10 echo "$a" > myoutput.txt

Dies funktioniert in vielen Situationen einwandfrei, hat jedoch einen trügerischen Fehler: Wenn $ a mehr als ~ 1000 Zeichen enthält, ist das Echo möglicherweise nicht atomar (es kann in mehrere write () -Aufrufe aufgeteilt werden), und es besteht die Gefahr, dass zwei Zeilen vorhanden sind wird gemischt.

$ perl -e 'print "a"x2000, "\n"' > foo
$ strace -e write bash -c 'read -r foo < foo; echo "$foo"' >/dev/null
write(1, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 1008) = 1008
write(1, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 993) = 993
+++ exited with 0 +++

Offensichtlich tritt das gleiche Problem auf, wenn mehrere Aufrufe von echo oder printf vorliegen:

slowprint() {
  printf 'Start-%s ' "$1"
  sleep "$1"
  printf '%s-End\n' "$1"
}
export -f slowprint
seq 10 | xargs -n1 -I {} -P4 bash -c "slowprint {}"
# Compare to no parallelization
seq 10 | xargs -n1 -I {} bash -c "slowprint {}"
# Be sure to see the warnings in the next Pitfall!

Die Ausgaben der parallelen Jobs werden gemischt, da jeder Job aus zwei (oder mehr) separaten write () -Aufrufen besteht.

Wenn Sie die Ausgänge ungemischt benötigen, wird daher empfohlen, ein Tool zu verwenden, das garantiert, dass die Ausgabe serialisiert wird (wie GNU Parallel).

Ole Tange
quelle
Dieser Abschnitt ist falsch. xargs echoRuft nicht die eingebaute Echo-Bash auf, sondern das echoDienstprogramm von $PATH. Und trotzdem kann ich dieses Bash-Echo-Verhalten mit Bash 4.4 nicht reproduzieren. Unter Linux sind Schreibvorgänge in eine Pipe (not / dev / null), die größer als 4 KB ist, jedoch nicht garantiert atomar.
Stéphane Chazelas