Hinweis: Die Antwort spiegelt mein eigenes Verständnis dieser Mechanismen wider, das sich aus der Recherche und dem Lesen der Antworten der Kollegen auf dieser Site und unter unix.stackexchange.com ergibt . Sie wird im Laufe der Zeit aktualisiert. Zögern Sie nicht, Fragen zu stellen oder Verbesserungen in den Kommentaren vorzuschlagen. Ich schlage auch vor, Sie versuchen zu sehen, wie Syscalls in Shell mit strace
Befehl funktionieren . Lassen Sie sich auch nicht von der Vorstellung von Internals oder Syscalls einschüchtern - Sie müssen sie nicht kennen oder verwenden können, um zu verstehen, wie Shell Dinge tut, aber sie helfen auf jeden Fall, sie zu verstehen.
TL; DR
|
Pipes sind keinem Eintrag auf der Festplatte zugeordnet, haben also keine Inode- Nummer des Festplatten-Dateisystems (haben aber Inode im virtuellen Pipef- Dateisystem im Kernel-Space), aber bei Umleitungen handelt es sich häufig um Dateien, die Festplatteneinträge haben und daher entsprechende inode.
- Pipes sind nicht in
lseek()
der Lage, sodass Befehle einige Daten nicht lesen und dann zurückspulen können. Wenn Sie jedoch mit einer Datei umleiten >
oder diese <
normalerweise als lseek()
Objekt verwenden, können Befehle nach Belieben navigieren.
- Weiterleitungen sind Manipulationen an Dateideskriptoren, die vielfältig sein können. Pipes haben nur zwei Dateideskriptoren - einen für den linken und einen für den rechten Befehl
- Die Umleitung auf Standard-Streams und Pipes wird gepuffert.
- Bei Rohren handelt es sich fast immer um Gabeln, und daher handelt es sich um Prozesspaare. Umleitungen - nicht immer, obwohl in beiden Fällen resultierende Dateideskriptoren von Unterprozessen geerbt werden.
- Pipes verbinden immer Dateideskriptoren (ein Paar) und Umleitungen - entweder mit einem Pfadnamen oder mit Dateideskriptoren.
- Pipes sind Inter-Process-Kommunikationsmethoden, während Umleitungen nur Manipulationen an geöffneten Dateien oder dateiähnlichen Objekten sind
- Beide verwenden
dup2()
Systemaufrufe unter der Haube, um Kopien von Dateideskriptoren bereitzustellen, bei denen der tatsächliche Datenfluss stattfindet.
- Umleitungen können mit dem
exec
eingebauten Befehl "global" angewendet werden (siehe dies und das ). Wenn Sie dies also tun, wird exec > output.txt
jeder Befehl output.txt
von da an beschrieben. |
Pipes werden nur für den aktuellen Befehl angewendet (dh entweder einfache Befehle oder untergeordnete ähnliche seq 5 | (head -n1; head -n2)
oder zusammengesetzte Befehle).
Wenn die Umleitung für Dateien durchgeführt wird, verwenden Dinge wie echo "TEST" > file
und echo "TEST" >> file
beide open()
syscall für diese Datei ( siehe auch ) und rufen den Dateideskriptor ab, um ihn an diese Datei weiterzuleiten dup2()
. Pipes verwenden |
nur pipe()
und dup2()
Syscall.
Bei der Ausführung von Befehlen handelt es sich bei Pipes und Umleitungen nur um Dateideskriptoren - dateiähnliche Objekte, in die sie möglicherweise blind schreiben oder intern bearbeiten (was zu unerwartetem Verhalten führen kann, apt
z. B. dazu führt, dass sie nicht einmal in die Standardausgabe schreiben wenn es weiß, dass es eine Umleitung gibt).
Einführung
Um zu verstehen, wie sich diese beiden Mechanismen unterscheiden, müssen ihre wesentlichen Eigenschaften, die Geschichte der beiden und ihre Wurzeln in der Programmiersprache C bekannt sein. In der Tat, zu wissen , welche Datei - Deskriptoren sind und wie dup2()
und pipe()
Systemaufrufe Arbeit ist wichtig, sowie lseek()
. Shell ist dazu gedacht, diese Mechanismen für den Benutzer abstrakt zu machen. Wenn Sie jedoch tiefer als die Abstraktion graben, können Sie die wahre Natur des Verhaltens von Shell besser verstehen.
Die Ursprünge von Umleitungen und Pipes
Laut Dennis Ritches Artikel Prophetic Petroglyphs ( Prophetische Petroglyphen) stammten Pfeifen aus einem internen Memo von Malcolm Douglas McIlroy aus dem Jahr 1964 , als sie am Betriebssystem Multics arbeiteten . Zitat:
Um meine größten Sorgen auf den Punkt zu bringen:
- Wir sollten einige Möglichkeiten haben, Programme wie Gartenschlauch anzuschließen - schrauben Sie in einem anderen Segment, wenn es notwendig wird, Daten auf andere Weise zu massieren. Dies ist auch der Weg von IO.
Was offensichtlich war, war, dass Programme zu der Zeit in der Lage waren, auf die Festplatte zu schreiben, was jedoch ineffizient war, wenn die Ausgabe groß war. So zitieren Sie Brian Kernighans Erklärung im Unix-Pipeline- Video:
Erstens müssen Sie kein einziges großes umfangreiches Programm schreiben - es gibt bereits kleinere Programme, die möglicherweise bereits Teile der Arbeit erledigen ... Zum anderen ist es möglich, dass die von Ihnen verarbeitete Datenmenge nicht passt, wenn Sie haben es in einer Datei gespeichert ... denn denken Sie daran, wir sind in den Tagen zurück, in denen die Festplatten auf diesen Dingen, wenn Sie Glück hatten, ein oder zwei Megabyte Daten hatten ... Die Pipeline musste also nie die gesamte Ausgabe instanziieren .
Der konzeptionelle Unterschied ist also offensichtlich: Pipes sind ein Mechanismus, mit dem Programme miteinander kommunizieren. Weiterleitungen - sind das Schreiben in eine Datei auf der Basisebene. In beiden Fällen macht Shell diese beiden Dinge einfach, aber unter der Motorhaube ist eine Menge los.
Tiefer gehen: Systemaufrufe und interne Funktionsweise der Shell
Wir beginnen mit dem Begriff des Dateideskriptors . Dateideskriptoren beschreiben im Grunde genommen eine geöffnete Datei (unabhängig davon, ob es sich um eine Datei auf der Festplatte oder im Speicher oder um eine anonyme Datei handelt), die durch eine Ganzzahl dargestellt wird. Die beiden Standarddatenströme (stdin, stdout, stderr) sind Dateideskriptoren 0,1 bzw. 2. Woher kommen sie ? Nun, in Shell-Befehlen werden die Dateideskriptoren von ihrer übergeordneten Shell geerbt. Und dies gilt im Allgemeinen für alle Prozesse. Der untergeordnete Prozess erbt die Dateideskriptoren der übergeordneten Prozesse. Für Daemons ist es üblich, alle geerbten Dateideskriptoren zu schließen und / oder an andere Stellen umzuleiten.
Zurück zur Umleitung. Was ist das eigentlich? Es ist ein Mechanismus, der die Shell anweist, Dateideskriptoren für den Befehl vorzubereiten (da Umleitungen von der Shell vorgenommen werden, bevor der Befehl ausgeführt wird) und sie an die vom Benutzer vorgeschlagene Stelle zu setzen. Die Standarddefinition für die Ausgabeumleitung lautet
[n]>word
Dass [n]
es die Dateideskriptornummer gibt. Wenn Sie dies tun, wird dort echo "Something" > /dev/null
die Zahl 1 impliziert, und echo 2> /dev/null
.
Unter der Haube geschieht dies durch Duplizieren des Dateideskriptors per dup2()
Systemaufruf. Lass uns nehmen df > /dev/null
. Die Shell erstellt einen untergeordneten Prozess df
, der ausgeführt wird. Vorher wird er jedoch /dev/null
als Dateideskriptor 3 geöffnet und dup2(3,1)
ausgegeben, wodurch eine Kopie von Dateideskriptor 3 erstellt wird. Die Kopie lautet 1. Sie wissen, wie Sie zwei Dateien file1.txt
und haben file2.txt
, und wenn Sie dies tun cp file1.txt file2.txt
, haben Sie zwei gleiche Dateien, aber Sie können sie unabhängig voneinander bearbeiten? Das ist irgendwie das Gleiche, was hier passiert. Oft können Sie vor dem Ausführen sehen , dass das bash
tun wird dup(1,10)
eine Kopie Dateideskriptor # 1 zu machen , das ist stdout
(und diese Kopie wird fd # 10 sein) um sie später wieder herstellen. Wichtig ist zu beachten, dass wenn Sie eingebaute Befehle berücksichtigen(die Teil der Shell selbst sind und keine Datei in /bin
oder an anderer Stelle haben) oder einfache Befehle in der nicht interaktiven Shell , erstellt die Shell keinen untergeordneten Prozess.
Und dann haben wir Dinge wie [n]>&[m]
und [n]&<[m]
. Dies ist das Duplizieren von Dateideskriptoren, der gleiche Mechanismus wie dup2()
jetzt in der Shell-Syntax, der für den Benutzer bequem verfügbar ist.
Eines der wichtigsten Dinge, die bei der Umleitung beachtet werden müssen, ist, dass ihre Reihenfolge nicht festgelegt ist, sondern entscheidend dafür, wie die Shell interpretiert, was der Benutzer möchte. Vergleichen Sie Folgendes:
# Make copy of where fd 2 points , then redirect fd 2
$ ls -l /proc/self/fd/ 3>&2 2> /dev/null
total 0
lrwx------ 1 user user 64 Sep 13 00:08 0 -> /dev/pts/0
lrwx------ 1 user user 64 Sep 13 00:08 1 -> /dev/pts/0
l-wx------ 1 user user 64 Sep 13 00:08 2 -> /dev/null
lrwx------ 1 runner user 64 Sep 13 00:08 3 -> /dev/pts/0
lr-x------ 1 user user 64 Sep 13 00:08 4 -> /proc/29/fd
# redirect fd #2 first, then clone it
$ ls -l /proc/self/fd/ 2> /dev/null 3>&2
total 0
lrwx------ 1 user user 64 Sep 13 00:08 0 -> /dev/pts/0
lrwx------ 1 user user 64 Sep 13 00:08 1 -> /dev/pts/0
l-wx------ 1 user user 64 Sep 13 00:08 2 -> /dev/null
l-wx------ 1 user user 64 Sep 13 00:08 3 -> /dev/null
lr-x------ 1 user user 64 Sep 13 00:08 4 -> /proc/31/fd
Die praktische Verwendung dieser in Shell-Skripten kann vielfältig sein:
und viele weitere.
Sanitär mit pipe()
unddup2()
Wie entstehen Rohre? Über pipe()
syscall , das als Eingabe ein Array (auch als Liste bezeichnet) verwendet, das pipefd
aus zwei Elementen des Typs int
(Ganzzahl) besteht. Diese beiden Ganzzahlen sind Dateideskriptoren. Das pipefd[0]
wird das Leseende der Pipe und pipefd[1]
das Schreibende sein. Also df | grep 'foo'
, grep
erhält Kopie pipefd[0]
und df
eine Kopie erhalten pipefd[1]
. Aber wie ? Natürlich mit der Magie von dup2()
Syscall. Nehmen df
wir in unserem Beispiel an, wir pipefd[1]
haben # 4, damit die Shell ein untergeordnetes Element erzeugt, das Sie ausführen dup2(4,1)
(erinnern Sie sich an mein cp
Beispiel?) Und dann execve()
ausführen, um tatsächlich zu laufen df
. Natürlich,df
erbt den Dateideskriptor Nr. 1, merkt jedoch nicht, dass er nicht mehr auf das Terminal zeigt, sondern auf fd Nr. 4, das eigentlich das Schreibende der Pipe ist. Natürlich wird dasselbe mit grep 'foo'
Ausnahme einer unterschiedlichen Anzahl von Dateideskriptoren auftreten.
Nun eine interessante Frage: Können wir Pipes herstellen, die auch fd # 2 umleiten, nicht nur fd # 1? Ja, |&
genau das macht man in der Bash. Der POSIX - Standard erfordert Shell Befehlssprache zur Unterstützung der df 2>&1 | grep 'foo'
Syntax für diesen Zweck, aber bash
tut |&
auch.
Zu beachten ist, dass Pipes immer mit Dateideskriptoren arbeiten. Es existiert FIFO
oder Named Pipe , die einen Dateinamen auf der Festplatte hat und wir Sie als Datei verwenden, sondern verhält sich wie ein Rohr. Bei den |
Pipetypen handelt es sich jedoch um so genannte anonyme Pipes. Sie haben keinen Dateinamen, da sie eigentlich nur zwei miteinander verbundene Objekte sind. Die Tatsache, dass es sich nicht um Dateien handelt, ist auch eine wichtige Implikation: Pipes sind nicht in lseek()
der Lage. Dateien, entweder im Speicher oder auf der Festplatte, sind statisch - Programme können lseek()
syscall verwenden, um zu Byte 120 zu springen, dann zurück zu Byte 10 und bis zum Ende weiterzuleiten. Pipes sind nicht statisch - sie sind sequentiell und daher können Sie keine Daten zurückspulen, die Sie von ihnen erhaltenlseek()
. Dies ist es, was einige Programme darauf aufmerksam macht, ob sie aus einer Datei oder einer Pipe lesen, und somit die notwendigen Anpassungen für eine effiziente Leistung vornehmen können. Mit anderen Worten, ein prog
kann erkennen, ob ich cat file.txt | prog
oder prog < input.txt
. Ein echtes Arbeitsbeispiel dafür ist der Schwanz .
Die anderen beiden sehr interessanten Eigenschaften von Pipes sind, dass sie einen Puffer haben, der unter Linux 4096 Bytes beträgt , und dass sie tatsächlich ein Dateisystem haben, wie es im Linux-Quellcode definiert ist ! Sie sind nicht nur ein Objekt zum Weitergeben von Daten, sie sind selbst eine Datenstruktur! Da es ein Pipefs-Dateisystem gibt, das sowohl Pipes als auch FIFOs verwaltet, haben Pipes eine Inode- Nummer in ihrem jeweiligen Dateisystem:
# Stdout of ls is wired to pipe
$ ls -l /proc/self/fd/ | cat
lrwx------ 1 user user 64 Sep 13 00:02 0 -> /dev/pts/0
l-wx------ 1 user user 64 Sep 13 00:02 1 -> pipe:[15655630]
lrwx------ 1 user user 64 Sep 13 00:02 2 -> /dev/pts/0
lr-x------ 1 user user 64 Sep 13 00:02 3 -> /proc/22/fd
# stdin of ls is wired to pipe
$ true | ls -l /proc/self/fd/0
lr-x------ 1 user user 64 Sep 13 03:58 /proc/self/fd/0 -> 'pipe:[54741]'
Unter Linux sind Pipes wie Umleitungen unidirektional. Bei einigen Unix-ähnlichen Implementierungen gibt es bidirektionale Pipes. Mit der Magie des Shell-Skripts können Sie auch unter Linux bidirektionale Pipes erstellen .
Siehe auch:
thing1 > temp_file && thing2 < temp_file
, mit Rohren einfacher umzugehen. Aber warum nicht den>
Operator wiederverwenden , um dies zu tun, z. B.thing1 > thing2
für Befehlething1
undthing2
? Warum ein zusätzlicher Betreiber|
?