"Undichte" Pipes unter Linux

11

Nehmen wir an, Sie haben eine Pipeline wie die folgende:

$ a | b

Wenn bdie Verarbeitung von stdin beendet wird, füllt sich die Pipe nach einer Weile und Schreibvorgänge von abis zu ihrem stdout werden blockiert (bis entweder bdie Verarbeitung erneut beginnt oder sie stirbt).

Wenn ich dies vermeiden wollte, könnte ich versucht sein, ein größeres Rohr (oder einfacher buffer(1)) wie folgt zu verwenden :

$ a | buffer | b

Das würde mir einfach mehr Zeit verschaffen, aber am Ende awürde es irgendwann aufhören.

Was ich gerne hätte (für ein sehr spezifisches Szenario, das ich anspreche), wäre eine "undichte" Pipe, die, wenn sie voll ist, einige Daten (idealerweise zeilenweise) aus dem Puffer löscht, um afortzufahren Verarbeitung (wie Sie sich wahrscheinlich vorstellen können, sind die Daten, die in der Pipe fließen, entbehrlich, dh es bist weniger wichtig , dass die Daten von verarbeitet werden, als dass asie ohne Blockierung ausgeführt werden können).

Um es zusammenzufassen, ich hätte gerne so etwas wie einen begrenzten, undichten Puffer:

$ a | leakybuffer | b

Ich könnte es wahrscheinlich ganz einfach in jeder Sprache implementieren. Ich habe mich nur gefragt, ob mir etwas "gebrauchsfertiges" (oder so etwas wie ein Bash-Einzeiler) fehlt.

Hinweis: In den Beispielen verwende ich reguläre Pipes, aber die Frage gilt auch für Named Pipes


Während ich die folgende Antwort erhielt, entschied ich mich auch, den Befehl leakybuffer zu implementieren, da die einfache Lösung unten einige Einschränkungen aufwies: https://github.com/CAFxX/leakybuffer

CAFxX
quelle
Füllen sich benannte Rohre wirklich? Ich hätte gedacht Named Pipes haben ist die Lösung für dieses Problem , aber ich konnte nicht sicher sagen.
Wildcard
3
Benannte Rohre haben (standardmäßig) die gleiche Kapazität wie unbenannte Rohre, AFAIK
CAFxX

Antworten:

14

Am einfachsten wäre es, ein Programm zu durchlaufen, das die nicht blockierende Ausgabe festlegt. Hier ist ein einfacher Perl-Oneliner (den Sie als Leakybuffer speichern können ), der dies tut:

so wird dein a | b:

a | perl -MFcntl -e \
    'fcntl STDOUT,F_SETFL,O_NONBLOCK; while (<STDIN>) { print }' | b

Was getan wird, ist die Eingabe zu lesen und in die Ausgabe zu schreiben (wie cat(1)), aber die Ausgabe ist nicht blockierend - was bedeutet, dass wenn der Schreibvorgang fehlschlägt, ein Fehler zurückgegeben wird und Daten verloren gehen, der Prozess jedoch mit der nächsten Eingabezeile fortgesetzt wird, da wir die Eingabe bequem ignorieren Error. Der Prozess ist wie gewünscht zeilengepuffert, siehe jedoch die folgenden Einschränkungen.

Sie können zum Beispiel testen mit:

seq 1 500000 | perl -w -MFcntl -e \
    'fcntl STDOUT,F_SETFL,O_NONBLOCK; while (<STDIN>) { print }' | \
    while read a; do echo $a; done > output

Sie erhalten eine outputDatei mit verlorenen Zeilen (die genaue Ausgabe hängt von der Geschwindigkeit Ihrer Shell usw. ab) wie folgt :

12768
12769
12770
12771
12772
12773
127775610
75611
75612
75613

Sie sehen, wo die Shell danach Linien verloren hat 12773 , aber auch eine Anomalie - die Perl hatte nicht genug Puffer dafür 12774\n, aber dafür 1277hat sie genau das geschrieben - und so 75610beginnt die nächste Zahl nicht am Anfang der Zeile, was sie klein macht hässlich.

Dies könnte verbessert werden, indem Perl erkannt wird, wenn der Schreibvorgang nicht vollständig erfolgreich war, und später versucht wird, den Rest der Zeile zu löschen, während neue eingehende Zeilen ignoriert werden. Dies würde jedoch das Perl-Skript erheblich komplizieren. Dies bleibt als Übung für der interessierte Leser :)

Update (für Binärdateien): Wenn Sie keine Zeilenumbrüche mit Zeilenumbrüchen (wie Protokolldateien oder ähnliches) verarbeiten, müssen Sie den Befehl geringfügig ändern. Andernfalls verbraucht Perl viel Speicher (abhängig davon, wie oft Zeilenumbrüche in Ihrer Eingabe erscheinen):

perl -w -MFcntl -e 'fcntl STDOUT,F_SETFL,O_NONBLOCK; while (read STDIN, $_, 4096) { print }' 

Es funktioniert auch korrekt für Binärdateien (ohne zusätzlichen Speicher zu verbrauchen).

Update2 - schönere Textdateiausgabe: Vermeiden von Ausgabepuffern ( syswriteanstelle von print):

seq 1 500000 | perl -w -MFcntl -e \
    'fcntl STDOUT,F_SETFL,O_NONBLOCK; while (<STDIN>) { syswrite STDOUT,$_ }' | \
    while read a; do echo $a; done > output

scheint für mich Probleme mit "zusammengeführten Linien" zu beheben:

12766
12767
12768
16384
16385
16386

(Hinweis: Man kann überprüfen, auf welchen Zeilen die Ausgabe geschnitten wurde mit: perl -ne '$c++; next if $c==$_; print "$c $_"; $c=$_' outputoneliner)

Matija Nalis
quelle
Ich liebe den Oneliner: Ich bin kein Perl-Experte, wenn jemand die obigen Verbesserungen vorschlagen könnte, wäre es fantastisch
CAFxX
1
Dies scheint bis zu einem gewissen Grad zu funktionieren . Aber während ich meinen Befehl beobachte perl -w -MFcntl -e 'fcntl STDOUT,F_SETFL,O_WRONLY|O_NONBLOCK; while (<STDIN>) { print }' | aplay -t raw -f dat --buffer-size=16000, scheint Perl ständig mehr Speicher zuzuweisen, bis er vom OOM-Manager beendet wird.
Ponkadoodle
@ Wallacoloo, danke, dass Sie darauf hingewiesen haben, dass mein Fall das Streamen von Protokolldateien war ... Siehe aktualisierte Antwort für geringfügige Änderungen, die zur Unterstützung von Binärdateien erforderlich sind.
Matija Nalis
Siehe auch GNU dds‘ dd oflag=nonblock status=none.
Stéphane Chazelas
1
Tut mir leid, mein schlechtes nochmal, tatsächlich sind Schreibvorgänge mit weniger als PIPE_BUF-Bytes (4096 unter Linux, mindestens 512 von POSIX erforderlich) garantiert atomar, $| = 1und Ihr syswrite()Ansatz verhindert in der Tat kurze Schreibvorgänge, solange die Zeilen relativ kurz sind.
Stéphane Chazelas