Ressourcennutzung mit Pipe und Here-String

16

Wir können das gleiche Ergebnis mit den folgenden beiden in erhalten bash,

echo 'foo' | cat

und

cat <<< 'foo'

Meine Frage ist, was ist der Unterschied zwischen diesen beiden, was die verwendeten Ressourcen anbelangt und welche ist besser?

Mein Gedanke ist, dass wir bei der Verwendung von Pipe einen zusätzlichen Prozess echound eine Pipe verwenden, während in diesem String nur ein Dateideskriptor verwendet wird cat.

utlamn
quelle

Antworten:

17

Die Pipe ist eine Datei, die in einem kernelinternen Dateisystem geöffnet wurde und nicht als reguläre Datei auf der Festplatte verfügbar ist. Es wird automatisch nur bis zu einer bestimmten Größe gepuffert und wird schließlich blockiert, wenn es voll ist. Im Gegensatz zu Dateien, die auf Blockgeräten bezogen wurden, verhalten sich Pipes sehr ähnlich wie Zeichengeräte und werden daher im Allgemeinen nicht unterstützt lseek()und die von ihnen gelesenen Daten können nicht wie bei einer regulären Datei erneut gelesen werden.

Der Here-String ist eine reguläre Datei, die in einem gemounteten Dateisystem erstellt wird. Die Shell erstellt die Datei und behält ihren Deskriptor bei, während sie sofort die einzige Dateisystemverknüpfung entfernt (und sie so löscht), bevor sie jemals ein Byte in die Datei schreibt oder daraus liest. Der Kernel behält den für die Datei erforderlichen Speicherplatz bei, bis alle Prozesse alle Deskriptoren für die Datei freigeben. Wenn das Kind, das aus einem solchen Deskriptor liest, die Fähigkeit dazu hat, kann es mit zurückgespult lseek()und erneut gelesen werden.

In beiden Fällen stellen die Token <<<und |die Dateideskriptoren dar und nicht unbedingt die Dateien selbst. Sie können eine bessere Vorstellung davon bekommen, was vor sich geht, indem Sie Dinge tun wie:

readlink /dev/fd/1 | cat

...oder...

ls -l <<<'' /dev/fd/*

Der bedeutendste Unterschied zwischen den beiden Dateien besteht darin, dass der Here-String / Doc so ziemlich eine All-at-Once-Angelegenheit ist - die Shell schreibt alle Daten hinein, bevor sie dem Kind den Read-Descriptor anbietet. Auf der anderen Seite öffnet die Shell die Pipe für die entsprechenden Deskriptoren und leitet untergeordnete Elemente ab, um diese für die Pipe zu verwalten. Sie wird also an beiden Enden gleichzeitig geschrieben / gelesen .

Diese Unterscheidungen sind jedoch nur allgemein zutreffend. Soweit ich weiß (was eigentlich gar nicht so weit ist), trifft dies auf so ziemlich jede Shell zu, die die <<<Here-String-Kurzschrift für <<eine Here-Document-Umleitung verarbeitet, mit der einzigen Ausnahme von yash. yash, busybox, dashUnd andere ashVarianten neigen dazu , hier-Dokumente mit Rohren zu unterstützen , aber, und so in diesen Schalen wirklich da zwischen den beiden doch sehr wenig Unterschied.

Ok - zwei Ausnahmen. Jetzt, wo ich darüber nachdenke, ksh93mache ich eigentlich gar keine Pipe für |, sondern kümmere mich um das ganze Geschäft mit Sockets - obwohl es eine gelöschte tmp-Datei macht, <<<*wie die meisten anderen. Außerdem werden nur die einzelnen Abschnitte einer Pipeline in einer Subshell-Umgebung abgelegt, die eine Art POSIX-Euphemismus darstellt, da sie sich zumindest wie eine Subshell verhält , und die Gabeln auch nicht.

Tatsache ist, dass @ PSkociks Benchmark- Ergebnisse (die sehr nützlich sind) aus vielen Gründen stark variieren können und die meisten davon von der Implementierung abhängig sind. Die ${TMPDIR}wichtigsten Faktoren für die Einrichtung des Dokuments sind der Typ des Zieldateisystems und die aktuelle Cache-Konfiguration / -Verfügbarkeit sowie die noch zu schreibende Datenmenge. Für die Pipe entspricht dies der Größe des Shell-Prozesses selbst, da für die erforderlichen Gabeln Kopien angefertigt werden. Auf diese Weise bashist das Einrichten von Pipelines (einschließlich Befehlsersetzungen ) furchtbar - weil es groß und sehr langsam ist, aber dabei kaum einen Unterschied macht.$()ksh93

Hier ist ein weiteres kleines Shell-Snippet, um zu demonstrieren, wie eine Shell Subshells für eine Pipeline abspaltet:

pipe_who(){ echo "$$"; sh -c 'echo "$PPID"'; }
pipe_who
pipe_who | { pipe_who | cat /dev/fd/3 -; } 3<&0

32059  #bash's pid
32059  #sh's ppid
32059  #1st subshell's $$
32111  #1st subshell sh's ppid
32059  #2cd subshell's $$
32114  #2cd subshell sh's ppid

Der Unterschied zwischen dem, was ein Pipeline- pipe_who()Aufruf meldet, und dem Bericht einer Ausführung in der aktuellen Shell beruht auf (dem )angegebenen Verhalten einer Subshell, die PID der übergeordneten Shell $$beim Erweitern zu übernehmen. Obwohl bashSubshells definitiv separate Prozesse sind, ist der $$spezielle Shell-Parameter keine zuverlässige Quelle für diese Informationen. Die untergeordnete shShell der Subshell lehnt es jedoch nicht ab , die untergeordnete Shell genau zu melden $PPID.

mikeserv
quelle
Sehr hilfreich. Gibt es einen Namen für das kernelinterne Dateisystem? Bedeutet das, dass es im Kernel-Space existiert?
2.
2
@utlamn - eigentlich ja - einfach pipefs . Es ist alles im Kernel - aber (abgesehen von Sachen wie FUSE) , so ist alle Datei i / o .
mikeserv
10

Es gibt keinen Ersatz für Benchmarking:

pskocik@ProBook:~ 
$ time (for((i=0;i<1000;i++)); do cat<<< foo >/dev/null; done  )

real    0m2.080s
user    0m0.738s
sys 0m1.439s
pskocik@ProBook:~ 
$ time (for((i=0;i<1000;i++)); do echo foo |cat >/dev/null; done  )

real    0m4.432s
user    0m2.095s
sys 0m3.927s
$ time (for((i=0;i<1000;i++)); do cat <(echo foo) >/dev/null; done  )
real    0m3.380s
user    0m1.121s
sys 0m3.423s

Und für eine größere Datenmenge:

TENMEG=$(ruby -e 'puts "A"*(10*1024*1024)')
pskocik@ProBook:~ 
$ time (for((i=0;i<100;i++)); do echo "$TENMEG" |cat >/dev/null; done  )

real    0m42.327s
user    0m38.591s
sys 0m4.226s
pskocik@ProBook:~ 
$ time (for((i=0;i<100;i++)); do cat<<< "$TENMEG" >/dev/null; done  )

real    1m26.946s
user    1m23.116s
sys 0m3.681s
pskocik@ProBook:~ 

$ time (for((i=0;i<100;i++)); do cat <(echo "$TENMEG") >/dev/null; done  )

real    0m43.910s
user    0m40.178s
sys 0m4.119s

Es scheint, dass die Rohrversion höhere Einrichtungskosten hat, aber letztendlich effizienter ist.

PSkocik
quelle
@mikeserv Das war richtig. Ich habe einen Benchmark mit einer größeren Datenmenge hinzugefügt.
PSkocik
2
echo foo >/dev/shm/1;cat /dev/shm/1 >/dev/nullschien in beiden Fällen schnell zu sein ...
user23013
@ user23013 Das macht Sinn. Ich verstehe auch nicht, warum echo "$longstring"oder <<<"$longstring"was die Effizienz angeht, und mit kurzen Saiten spielt Effizienz sowieso keine Rolle.
PSkocik
Es ist interessant, dass in meinem Fall (auf Ubuntu 14.04, Intel Quad Core i7) cat <(echo foo) >/dev/nullschneller ist als echo foo | cat >/dev/null.
Pabouk
1
@Prem Ja, das wäre ein besserer Ansatz, aber noch besser wäre es, sich darüber keine Gedanken zu machen und das richtige Werkzeug für den Job zu verwenden. Es gibt keinen Grund zu der Annahme, dass Heredocs leistungsgestimmt ist.
PSkocik