Pipe, {Liste; } funktioniert nur mit einigen Programmen

13

Benötigen Sie Erklärungen von Hauptbenutzern für dieses unvorhersehbare Verhalten:

ps -eF | { head -n 1;grep worker; }
UID        PID  PPID  C    SZ   RSS PSR STIME TTY          TIME CMD
root       441     2  0     0     0   2 paź15 ?       00:00:00 [kworker/2:1H]

alles sieht in ordnung aus

ls -la / | { head -n 1;grep sbin; }

Zeigt nur die Ausgabe von an head

... Ich habe darüber nachgedacht stdout 2>&1und arbeite auch nicht für mich. Es ist komisch, irgendwelche Erklärungen oder Vorschläge, wie ich damit umgehen soll.

ast
quelle
1
Der letzte sollte alles ausdrucken. Die headund grepmachen da nichts.
Jordan
ja, du hast recht. Aber warum funktioniert stattdessen ps -eF, während ls -la / not?
ast

Antworten:

9

Ich habe einige Nachforschungen angestellt straceund es scheint daran zu liegen, wie das Programm auf der linken Seite der Pipeline zum Terminal schreibt. Wenn der lsBefehl ausgeführt wird, werden alle Daten in einem einzigen geschrieben write(). Dies führt headdazu, dass der gesamte Stdin verbraucht wird.

psSchreibt dagegen Daten stapelweise aus, sodass nur die ersten von write()verbraucht werden headund dann vorhanden sind. Bei späteren Aufrufen von write()wird der neu erzeugte grepProzess aufgerufen .

Dies bedeutet, dass es nicht funktionieren würde, wenn der Prozess, nach dem Sie suchen, grepnicht im ersten aufgetreten wäre write(), da grepnicht alle Daten angezeigt werden (es werden sogar weniger als nur die Daten ohne die erste Zeile angezeigt).

Hier ist ein Beispiel für den Versuch, auf meinem System nach PID 1 zu suchen:

$ ps -eF | { head -n2; }
UID        PID  PPID  C    SZ   RSS PSR STIME TTY          TIME CMD
root         1     0  0  1697  3768   2 Oct03 ?        00:00:03 /lib/systemd/systemd
$ ps -eF | grep '/lib/systemd/systemd$'
root         1     0  0  1697  3768   2 Oct03 ?        00:00:03 /lib/systemd/systemd
$ ps -eF | { head -n1; grep '/lib/systemd/systemd$'; }
UID        PID  PPID  C    SZ   RSS PSR STIME TTY          TIME CMD

Ihr ps -eFBeispiel funktioniert nur zufällig.

Jordanien
quelle
große und umfassende Erklärung vielen Dank
ast
1
Eigentlich ist es eher eine Rennbedingung. Es ist nur langsamer, wenn Sie mehrere write()Anrufe tätigen. Wenn der Aufruf headnur langsam ausgeführt werden würde read()(so dass der Pipe-Puffer alle Daten enthält), würde er für beide lsund dasselbe Verhalten aufweisen ps.
Patrick
6

Dies wird durch Pufferung in glibc verursacht. Im Falle lsder Ausgabe befindet sich dieser in einem internen Puffer und wird als solcher nur an weitergereicht head. Für die ps -eFist die Ausgabe größer und so einmal headbeendet, greperhält die folgende die verbleibenden Teile (aber nicht die gesamte) Ausgabe von ps.

Sie können es entfernen, indem Sie die Pipe entpuffern - zum Beispiel mit sed -u(ich bin mir nicht sicher, ob es keine GNU-Erweiterung ist):

$ ls -al / | sed -u "#" | { head -n 1; grep bin; }
total 76
drwxr-xr-x   2 root root  4096 Oct  2 21:52 bin
drwxr-xr-x   2 root root  8192 Oct  3 01:54 sbin
peterph
quelle
4

Was passiert ist, dass head -n 1mehr als 1 Zeile liest. Für einen optimalen Durchsatz liest head Teile von Bytes, sodass möglicherweise jeweils 1024 Bytes gelesen werden, und durchsucht diese Bytes dann nach dem ersten Zeilenumbruch. Da der Zeilenumbruch in der Mitte dieser 1024 Bytes auftreten kann, gehen die restlichen Daten verloren. Es kann nicht wieder auf die Pfeife gesetzt werden. Der nächste Prozess, der ausgeführt wird, erhält also nur Bytes 1025 und mehr.

Ihr erster Befehl ist zufällig erfolgreich, da der kworkerProzess nach dem ersten Teil erfolgt, der headgelesen wird.

Damit dies funktioniert, headmüsste jeweils 1 Zeichen gelesen werden. Aber das ist extrem langsam, also nicht.
Der einzige Weg, um so etwas effizient zu machen, besteht darin, dass ein einzelner Prozess sowohl "head" als auch "grep" ausführt.

Hierzu gibt es zwei Möglichkeiten:

echo -e '1\n2\n3\n4\n5' | perl -ne 'print if $i++ == 0 || /4/'

oder

echo -e '1\n2\n3\n4\n5' | awk '{if (NR == 1 || /4/) print }'

Es gibt noch viel mehr ...

Patrick
quelle
Ja, ich kenne 'way of awk', um diese Aufgabe zu bewältigen, habe mich aber gefragt, warum das Verhalten mit {list so unvorhersehbar war. }. Vielen Dank für die Klarstellung, wie es funktioniert. Ich bin mit allen oben genannten Antworten beeindruckt
ast
2

Wenn Sie nur die eine oder andere erste Zeile verwenden möchten, funktioniert der folgende Trick und vermeidet die Pufferungsprobleme, die durch die Verwendung von zwei verschiedenen Befehlen zum Lesen des Ausgabestreams verursacht werden:

$ ps -eF   | { IFS= read -r x ; echo "$x" ; grep worker; }
$ ls -la / | { IFS= read -r x ; echo "$x" ; grep sbin; }

Das readist in die Shell integriert und verbraucht nicht den gesamten Eingabepuffer, um nur die eine Zeile auszugeben. Wenn Sie also verwenden, readbleibt der Rest der Ausgabe für den folgenden Befehl übrig.

Wenn Sie die Pufferungsprobleme, die in den Beispielen mit zwei verschiedenen Befehlen gezeigt werden, hervorheben möchten, fügen Sie ein hinzu, um die Zeitprobleme sleepzu beseitigen, und lassen Sie den Befehl auf der linken Seite alle seine Ausgaben generieren, bevor die Befehle auf der rechten Seite versuchen, einen der Befehle zu lesen es:

$ ps -eF   | { sleep 5 ; head -n 1 ; grep worker; }
$ ls -la / | { sleep 5 ; head -n 1 ; grep sbin; }

Nun schlagen beide obigen Beispiele auf die gleiche Weise fehl - der headliest einen gesamten Puffer der Ausgabe, nur um die eine Zeile zu erzeugen, und dieser Puffer steht den folgenden nicht zur Verfügung grep.

Sie können das Pufferungsproblem noch deutlicher erkennen, indem Sie einige Beispiele verwenden, die die Ausgabezeilen nummerieren, damit Sie feststellen können, welche Zeilen fehlen:

$ ps -eF          | cat -n | { sleep 5 ; head -n 1 ; head ; }
$ ls -la /usr/bin | cat -n | { sleep 5 ; head -n 1 ; head ; }

Eine einfache Möglichkeit, das Pufferungsproblem zu erkennen, besteht darin seq, eine Liste von Zahlen zu erstellen. Wir können leicht erkennen, welche Zahlen fehlen:

$ seq 1 100000    | { sleep 5 ; head -n 1 ; head ; }
1

1861
1862
1863
1864
1865
1866
1867
1868
1869

Meine Tricklösung mit der Shell zum Lesen und Echo der ersten Zeile funktioniert auch mit der hinzugefügten Schlafverzögerung korrekt:

$ seq 1 100000 | { sleep 5 ; IFS= read -r x ; echo "$x" ; head ; }
1
2
3
4
5
6
7
8
9
10
11

Im Folgenden finden Sie ein vollständiges Beispiel, in dem die headPufferungsprobleme dargestellt sind. Es wird gezeigt, wie headein gesamter Puffer der Ausgabe verbraucht wird, um jeweils nur die fünf Zeilen zu erzeugen. Dieser verbrauchte Puffer ist für den nächsten headBefehl in der Sequenz nicht verfügbar :

$ seq 1 100000 | { sleep 5 ; head -5 ; head -5 ; head -5 ; head -5 ; }
1
2
3
4
5

1861
1862
1863
1864
499
3500
3501
3502
3503
7
5138
5139
5140
5141

Anhand der 1861obigen Zahl können wir die Größe des verwendeten Puffers berechnen, indem wir headdie seqAusgabe von 1bis zählen 1860:

$ seq 1 1860 | wc -c
8193

Wir sehen, dass headdas Puffern durch gleichzeitiges Lesen von 8 KB (8 * 1024 Byte) der Pipe-Ausgabe erfolgt, selbst um nur einige Zeilen der eigenen Ausgabe zu erzeugen.

Ian D. Allen
quelle