Schalten Sie die Pufferung in der Leitung aus

396

Ich habe ein Skript, das zwei Befehle aufruft:

long_running_command | print_progress

Der long_running_commandAusdruck ist ein Fortschritt, aber ich bin unzufrieden damit. Ich verwende print_progress, um es schöner zu machen (nämlich, ich drucke den Fortschritt in einer einzigen Zeile).

Das Problem: Das Verbinden einer Pipe mit stdout aktiviert auch einen 4K-Puffer, an den das nette Druckprogramm nichts ... nichts ... nichts ... eine ganze Menge ... :)

Wie kann ich den 4K-Puffer für den deaktivieren long_running_command(nein, ich habe nicht die Quelle)?

Aaron Digulla
quelle
1
Wenn Sie also long_running_command ohne Piping ausführen, können Sie die Fortschrittsaktualisierungen richtig sehen, aber wenn Sie Piping ausführen, werden sie gepuffert?
1
Ja, genau das passiert.
Aaron Digulla
21
Die Unfähigkeit, die Pufferung einfach zu steuern, ist seit Jahrzehnten ein Problem. Siehe zum Beispiel: marc.info/?l=glibc-bug&m=98313957306297&w=4, in dem im Grunde gesagt wird: "Ich kann mich nicht damit abfinden, und hier ist eine Klatschfalle, um meine Position zu rechtfertigen"
1
Es ist eigentlich stdio nicht die Pipe, die beim Warten auf genügend Daten eine Verzögerung verursacht. Pipes haben zwar eine Kapazität, aber sobald Daten in die Pipe geschrieben wurden, können sie sofort am anderen Ende gelesen werden.
Sam Watkins

Antworten:

254

Sie können den unbufferBefehl (der Bestandteil des expectPakets ist) verwenden, z

unbuffer long_running_command | print_progress

unbufferstellt long_running_commandüber ein Pseudoterminal (pty) eine Verbindung her, wodurch das System es als interaktiven Prozess behandelt und daher die 4-kiB-Pufferung in der Pipeline nicht verwendet, die die wahrscheinliche Ursache für die Verzögerung ist.

Bei längeren Pipelines müssen Sie möglicherweise jeden Befehl (mit Ausnahme des letzten) entpuffern, z

unbuffer x | unbuffer -p y | z
Stephen Kitt
quelle
3
Tatsächlich ist die Verwendung eines Pty zur Verbindung mit interaktiven Prozessen im Allgemeinen selbstverständlich.
15
Beim Pipelining von Aufrufen zum Entpuffern sollten Sie das Argument -p verwenden, damit der Entpuffer von stdin gelesen wird.
26
Hinweis: Auf Debian-Systemen heißt dies expect_unbufferund ist im expect-devPaket enthalten, nicht im expectPaket
bdonlan
4
@bdonlan: Zumindest auf Ubuntu (debian-basiert), expect-devbietet beides unbufferund expect_unbuffer(ersteres ist ein Symlink zu letzterem). Die Links sind seit expect 5.44.1.14-1(2009) verfügbar .
jfs
1
Hinweis: Auf Ubuntu 14.04.x-Systemen ist es auch im Paket expect-dev enthalten.
Alexandre Mazel
463

Eine andere Möglichkeit, diese Katze zu häuten, ist die Verwendung des stdbufProgramms, das Teil von GNU Coreutils ist (FreeBSD hat auch ein eigenes Programm).

stdbuf -i0 -o0 -e0 command

Dadurch wird die Pufferung für Eingabe, Ausgabe und Fehler vollständig deaktiviert. Für einige Anwendungen ist die Zeilenpufferung aus Leistungsgründen möglicherweise besser geeignet:

stdbuf -oL -eL command

Beachten Sie, dass es nur für das stdioPuffern ( printf(), fputs()...) von dynamisch verknüpften Anwendungen funktioniert, und nur, wenn diese Anwendung die Pufferung ihrer Standard-Streams ansonsten nicht selbst anpasst, obwohl dies die meisten Anwendungen abdecken sollte.

a3nm
quelle
6
"Unbuffer" muss in Ubuntu installiert werden, das sich im Paket befindet: expect-dev mit 2 MB ...
27.
2
Dies funktioniert hervorragend bei der Standard-Raspbian-Installation zum Entpuffern der Protokollierung. Ich habe sudo stdbuff … commandWerke gefunden, obwohl stdbuff … sudo commandnicht.
Natevw
20
@qdii stdbuffunktioniert nicht mit tee, da teedie von festgelegten Standardeinstellungen überschrieben werden stdbuf. Siehe die Handbuchseite von stdbuf.
Ceving
5
@lepe Seltsamerweise hat Unbuffer Abhängigkeiten von x11 und tcl / tk, was bedeutet, dass es tatsächlich> 80 MB benötigt, wenn Sie es auf einem Server ohne diese installieren.
Jpatokal
10
@qdii stdbufverwendet einen LD_PRELOADMechanismus, um seine eigene dynamisch geladene Bibliothek einzufügen libstdbuf.so. Dies bedeutet, dass es mit diesen ausführbaren Dateien nicht funktioniert: mit setuid- oder file-Funktionen, die statisch verknüpft sind und nicht die Standard-libc verwenden. In diesen Fällen ist es besser, die Lösungen mit unbuffer/ script/ zu verwenden socat. Siehe auch stdbuf mit setuid / functions .
Pabouk
75

Eine weitere Möglichkeit, den Ausgabemodus für die Zeilenpufferung zu aktivieren, long_running_commandbesteht darin, den scriptBefehl zu verwenden, mit dem Sie long_running_commandin einem Pseudoterminal (pty) ausgeführt werden.

script -q /dev/null long_running_command | print_progress      # FreeBSD, Mac OS X
script -c "long_running_command" /dev/null | print_progress    # Linux
Tschad
quelle
15
+1 netter Trick, da scriptes sich um einen so alten Befehl handelt, sollte er auf allen Unix-ähnlichen Plattformen verfügbar sein.
Aaron Digulla
5
Sie benötigen auch -qunter Linux:script -q -c 'long_running_command' /dev/null | print_progress
jfs
1
Es scheint, als würde ein Skript auslesen stdin, was es unmöglich macht, ein solches Skript long_running_commandim Hintergrund auszuführen , zumindest wenn es über ein interaktives Terminal gestartet wird. Um das Problem zu umgehen, konnte ich stdin von umleiten /dev/null, da meine long_running_commandnicht verwendet wird stdin.
Haridsv
1
Funktioniert sogar auf Android.
not2qubit
3
Ein wesentlicher Nachteil: Strg-Z funktioniert nicht mehr (dh ich kann das Skript nicht anhalten). Dies kann zum Beispiel behoben werden durch: echo | Sudo-Skript -c / usr / local / bin / ec2-snapshot-all / dev / null | Wenn es Ihnen nichts ausmacht, nicht mit dem Programm interagieren zu können.
Rlpowell
66

Für grep, sedund awkSie können die Ausgabe zwangsweise zeilenweise puffern. Sie können verwenden:

grep --line-buffered

Erzwinge die Zeilenpufferung der Ausgabe. Standardmäßig wird die Ausgabe zeilenweise gepuffert, wenn es sich bei der Standardausgabe um eine Klemme handelt und die Ausgabe ansonsten blockweise gepuffert wird.

sed -u

Ausgabezeile gepuffert machen.

Weitere Informationen finden Sie auf dieser Seite: http://www.perkin.org.uk/posts/how-to-fix-stdio-buffering.html

Yaneku
quelle
51

Wenn es ein Problem mit der libc gibt, deren Pufferung / Leerung zu ändern, wenn die Ausgabe nicht an ein Terminal geht, sollten Sie socat ausprobieren . Sie können einen bidirektionalen Stream zwischen nahezu allen E / A-Mechanismen erstellen. Eines davon ist ein Forked-Programm, das mit einer Pseudotty spricht.

 socat EXEC:long_running_command,pty,ctty STDIO 

Was es tut, ist

  • Erstellen Sie eine Pseudotty
  • Fork long_running_command mit der Slave-Seite des Pty als stdin / stdout
  • stelle einen bidirektionalen Stream zwischen der Masterseite des Pty und der zweiten Adresse her (hier ist es STDIO)

Wenn dies die gleiche Ausgabe ergibt wie long_running_command, können Sie mit einer Pipe fortfahren.

Edit: Wow Hab die Unbuffer-Antwort nicht gesehen! Nun, socat ist sowieso ein großartiges Werkzeug, also könnte ich diese Antwort einfach hinterlassen

Shodanex
quelle
1
... und ich wusste nichts über socat - sieht ein bisschen wie netcat aus, vielleicht eher so. ;) Danke und +1.
3
Ich würde socat -u exec:long_running_command,pty,end-close -hier verwenden
Stéphane Chazelas
20

Sie können verwenden

long_running_command 1>&2 |& print_progress

Das Problem ist, dass libc einen Zeilenpuffer erstellt, wenn stdout auf den Bildschirm zeigt, und einen vollständigen Puffer, wenn stdout auf eine Datei zeigt. Aber kein Puffer für stderr.

Ich glaube nicht, dass es das Problem mit dem Pipe-Buffer ist, es geht nur um die Buffer-Policy von libc.

Wang HongQin
quelle
Du hast recht; Meine Frage ist immer noch: Wie kann ich die Pufferrichtlinie von libc beeinflussen, ohne neu zu kompilieren?
Aaron Digulla
@ StéphaneChazelas FD1 wird auf stderr umgeleitet
Wang Hongqin
@ StéphaneChazelas Ich verstehe deine Argumentation nicht. Bitte machen Sie einen Test, es funktioniert
Wang HongQin
3
OK, was passiert ist, dass mit beiden zsh(woher |&kommt von Csh angepasst) und bash, wenn Sie dies tun cmd1 >&2 |& cmd2, beide FD 1 und 2 mit dem äußeren STDOUT verbunden sind. Es verhindert also das Puffern, wenn es sich bei der äußeren Standardausgabe um ein Terminal handelt, aber nur, weil die Ausgabe nicht über die Pipe erfolgt (also print_progressnichts druckt). Es ist also dasselbe wie long_running_command & print_progress(außer dass print_progress stdin eine Pipe ist, die keinen Writer hat). Sie können mit im ls -l /proc/self/fd >&2 |& catVergleich zu überprüfen ls -l /proc/self/fd |& cat.
Stéphane Chazelas
3
Das ist, weil |&es 2>&1 |buchstäblich Abkürzung ist. So cmd1 |& cmd2ist es auch cmd1 1>&2 2>&1 | cmd2. Somit sind sowohl fd 1 als auch 2 mit dem ursprünglichen stderr verbunden, und es bleibt nichts übrig, was in die Pipe geschrieben wird. ( s/outer stdout/outer stderr/gIn meinem vorherigen Kommentar).
Stéphane Chazelas
11

Früher war und ist es wahrscheinlich immer noch so, dass beim Schreiben der Standardausgabe in ein Terminal standardmäßig eine Zeile gepuffert wird - beim Schreiben einer neuen Zeile wird die Zeile in das Terminal geschrieben. Wenn die Standardausgabe an eine Pipe gesendet wird, ist sie vollständig gepuffert. Daher werden die Daten nur dann an den nächsten Prozess in der Pipeline gesendet, wenn der Standard-E / A-Puffer gefüllt ist.

Das ist die Ursache des Problems. Ich bin mir nicht sicher, ob Sie viel tun können, um das Problem zu beheben, ohne das Programm zu ändern, das in die Pipe geschrieben wird. Sie können die setvbuf()Funktion mit dem _IOLBFFlag verwenden, um bedingungslos stdoutin den zeilengepufferten Modus zu wechseln. Aber ich sehe keine einfache Möglichkeit, dies in einem Programm durchzusetzen. Oder das Programm kann fflush()an geeigneten Punkten (nach jeder Ausgabezeile), aber der gleiche Kommentar gilt.

Ich nehme an, wenn Sie die Pipe durch ein Pseudo-Terminal ersetzen, würde die Standard-E / A-Bibliothek annehmen, dass die Ausgabe ein Terminal ist (da es sich um eine Art Terminal handelt) und den Zeilenpuffer automatisch ausführen. Das ist jedoch eine komplexe Art, mit Dingen umzugehen.

Jonathan Leffler
quelle
7

Ich weiß, dass dies eine alte Frage ist und bereits viele Antworten hatte, aber wenn Sie das Pufferproblem vermeiden möchten, versuchen Sie einfach Folgendes:

stdbuf -oL tail -f /var/log/messages | tee -a /home/your_user_here/logs.txt

Dadurch werden die Protokolle in Echtzeit ausgegeben und in der logs.txtDatei gespeichert, und der Puffer hat keine Auswirkungen mehr auf den tail -fBefehl.

Marin Nedea
quelle
4
Dies sieht aus wie die zweite Antwort: - /
Aaron Digulla
2
stdbuf ist in gnu coreutils enthalten (ich habe es auf der neuesten Version 8.25 überprüft). hat bestätigt, dass dies auf einem eingebetteten Linux funktioniert.
Zhaorufei
Aus der Dokumentation von stdbuf, NOTE: If COMMAND adjusts the buffering of its standard streams ('tee' does for example) then that will override corresponding changes by 'stdbuf'.
shrewmouse
6

Ich glaube nicht, dass das Problem bei der Pfeife liegt. Es hört sich so an, als würde Ihr langer Prozess seinen eigenen Puffer nicht häufig genug leeren. Das Ändern der Puffergröße der Pipe wäre ein Hack, um das Problem zu umgehen, aber ich denke nicht, dass dies möglich ist, ohne den Kernel neu zu erstellen - etwas, das Sie nicht als Hack tun möchten, da es wahrscheinlich viele andere Prozesse in Mitleidenschaft zieht.


quelle
18
Die Hauptursache ist, dass libc auf 4k-Pufferung umschaltet, wenn das stdout kein tty ist.
Aaron Digulla
5
Das ist sehr interessant ! weil Pipe keine Pufferung verursacht. Sie bieten Pufferung, aber wenn Sie aus einer Pipe lesen und die verfügbaren Daten abrufen, müssen Sie nicht auf einen Puffer in der Pipe warten. Schuld daran wäre also die stdio-Pufferung in der Anwendung.
3

In diesem Beitrag können Sie versuchen, das Pipe-Ulimit auf einen einzelnen 512-Byte-Block zu reduzieren. Zwar wird die Pufferung nicht ausgeschaltet, aber 512 Bytes sind weit weniger als 4K: 3

RAKK
quelle
3

Ähnlich wie bei der Antwort von Chad können Sie ein kleines Skript wie das folgende schreiben:

# save as ~/bin/scriptee, or so
script -q /dev/null sh -c 'exec cat > /dev/null'

Verwenden Sie dann diesen scripteeBefehl als Ersatz für tee.

my-long-running-command | scriptee

Leider scheint es mir nicht möglich zu sein, dass eine solche Version unter Linux einwandfrei funktioniert, daher scheint sie auf BSD-Unixe beschränkt zu sein.

Unter Linux ist dies nicht weit entfernt, aber Sie erhalten Ihre Eingabeaufforderung nicht zurück, wenn dies abgeschlossen ist (bis Sie die Eingabetaste usw. drücken) ...

script -q -c 'cat > /proc/self/fd/1' /dev/null
jwd
quelle
Warum funktioniert das? Deaktiviert "script" die Pufferung?
Aaron Digulla
@ Aaron Digulla: scriptemuliert ein Terminal, also ja, ich glaube, es deaktiviert die Pufferung. Es wird auch jedes Zeichen zurückgesendet, das an dieses gesendet wurde - weshalb im Beispiel an catgesendet wird /dev/null. Für das Programm, das im Inneren ausgeführt scriptwird, spricht es mit einer interaktiven Sitzung. Ich glaube, dass es expectin dieser Hinsicht ähnlich ist , aber scriptwahrscheinlich Teil Ihres Basissystems ist.
JWD
Der Grund, den ich benutze, teebesteht darin, eine Kopie des Streams an eine Datei zu senden. Wo wird die Datei angegeben scriptee?
Bruno Bronosky
@BrunoBronosky: Sie haben recht, es ist ein schlechter Name für dieses Programm. Es ist nicht wirklich eine "Tee" -Operation. Gemäß der ursprünglichen Frage wird lediglich die Pufferung der Ausgabe deaktiviert. Vielleicht sollte es "scriptcat" heißen (obwohl es auch keine Verkettung macht ...). Unabhängig davon können Sie den catBefehl durch ersetzen tee myfile.txt, und Sie sollten den gewünschten Effekt erzielen.
JWD
2

Ich habe diese clevere Lösung gefunden: (echo -e "cmd 1\ncmd 2" && cat) | ./shell_executable

Das macht den Trick. catliest zusätzliche Eingaben (bis EOF) und übergibt diese an die Pipe, nachdem die echoArgumente in den Eingabestream von eingegeben wurden shell_executable.

jaggedsoft
quelle
2
Tatsächlich catsieht der die Ausgabe von echo; Sie führen nur zwei Befehle in einer Subshell aus und die Ausgabe von beiden wird in die Pipe gesendet. Der zweite Befehl in der Subshell ('cat') liest aus dem Parent / Outer Stdin, deshalb funktioniert es.
Aaron Digulla
0

Nach dieser scheint die Rohrpuffergröße in dem Kernel gesetzt zu werden und würde verlangen , dass Sie Ihren Kernel neu kompilieren zu ändern.


quelle
7
Ich glaube, das ist ein anderer Puffer.
Samuel Edwin Ward