Warum gibt es eine Rennbedingung
Die beiden Seiten eines Rohrs werden parallel ausgeführt, nicht nacheinander. Es gibt eine sehr einfache Möglichkeit, dies zu demonstrieren: Ausführen
time sleep 1 | sleep 1
Dies dauert eine Sekunde, nicht zwei.
Die Shell startet zwei untergeordnete Prozesse und wartet, bis beide abgeschlossen sind. Diese beiden Prozesse ausführen parallel: der einzige Grund , warum einer von ihnen mit der anderen synchronisieren würde, wenn es muss für den anderen warten. Der häufigste Synchronisationspunkt ist, wenn die rechte Seite blockiert und darauf wartet, dass Daten auf ihrer Standardeingabe gelesen werden, und wenn die linke Seite mehr Daten schreibt, wird die Blockierung aufgehoben. Das Umgekehrte kann auch passieren, wenn die rechte Seite Daten nur langsam liest und die linke Seite ihre Schreiboperation blockiert, bis die rechte Seite mehr Daten liest (in der Pipe selbst befindet sich ein Puffer, der von der verwaltet wird Kernel, aber es hat eine kleine maximale Größe).
Beachten Sie die folgenden Befehle, um einen Synchronisationspunkt zu beobachten ( sh -x
druckt jeden Befehl während der Ausführung aus):
time sh -x -c '{ sleep 1; echo a; } | { cat; }'
time sh -x -c '{ echo a; sleep 1; } | { cat; }'
time sh -x -c '{ echo a; sleep 1; } | { sleep 1; cat; }'
time sh -x -c '{ sleep 2; echo a; } | { cat; sleep 1; }'
Spielen Sie mit Variationen, bis Sie mit dem, was Sie beobachten, vertraut sind.
Gegeben den zusammengesetzten Befehl
cat tmp | head -1 > tmp
Der Prozess auf der linken Seite führt Folgendes aus (ich habe nur Schritte aufgelistet, die für meine Erklärung relevant sind):
- Führen Sie das externe Programm
cat
mit dem Argument aus tmp
.
tmp
Zum Lesen geöffnet .
- Während das Ende der Datei noch nicht erreicht ist, lesen Sie einen Teil der Datei und schreiben Sie ihn in die Standardausgabe.
Der Prozess auf der rechten Seite führt Folgendes aus:
- Leiten Sie die Standardausgabe an um
tmp
und kürzen Sie dabei die Datei.
- Führen Sie das externe Programm
head
mit dem Argument aus -1
.
- Lesen Sie eine Zeile von der Standardeingabe und schreiben Sie sie in die Standardausgabe.
Der einzige Synchronisationspunkt ist, dass Right-3 darauf wartet, dass Left-3 eine vollständige Zeile verarbeitet hat. Es gibt keine Synchronisation zwischen left-2 und right-1, daher können sie in jeder Reihenfolge erfolgen. In welcher Reihenfolge sie auftreten, ist nicht vorhersehbar: Dies hängt von der CPU-Architektur, der Shell, dem Kernel, den Kernen ab, auf denen die Prozesse geplant sind, von den Interrupts, die die CPU zu diesem Zeitpunkt empfängt usw.
So ändern Sie das Verhalten
Sie können das Verhalten nicht ändern, indem Sie eine Systemeinstellung ändern. Der Computer macht das, was Sie ihm sagen. Sie haben ihm gesagt, er soll abschneiden tmp
und tmp
parallel lesen , damit die beiden Dinge parallel ausgeführt werden.
Ok, es gibt eine "Systemeinstellung", die Sie ändern könnten: Sie könnten /bin/bash
durch ein anderes Programm ersetzen , das nicht bash ist. Ich hoffe, es versteht sich von selbst, dass dies keine gute Idee ist.
Wenn die Kürzung vor der linken Seite der Pipe erfolgen soll, müssen Sie sie außerhalb der Pipeline platzieren, z. B.:
{ cat tmp | head -1; } >tmp
oder
( exec >tmp; cat tmp | head -1 )
Ich habe keine Ahnung, warum Sie das wollen. Was bringt es, aus einer Datei zu lesen, von der Sie wissen, dass sie leer ist?
Wenn Sie dagegen möchten, dass die Ausgabeumleitung (einschließlich der Kürzung) nach dem cat
Lesen erfolgt, müssen Sie entweder die Daten im Speicher vollständig puffern, z
line=$(cat tmp | head -1)
printf %s "$line" >tmp
oder schreiben Sie in eine andere Datei und verschieben Sie sie. Dies ist normalerweise die robuste Methode, um Dinge in Skripten auszuführen, und hat den Vorteil, dass die Datei vollständig geschrieben wird, bevor sie durch den ursprünglichen Namen sichtbar wird.
cat tmp | head -1 >new && mv new tmp
Die moreutils- Sammlung enthält ein Programm namens sponge
.
cat tmp | head -1 | sponge tmp
So erkennen Sie das Problem automatisch
Wenn Ihr Ziel darin bestand, schlecht geschriebene Skripte zu erstellen und automatisch herauszufinden, wo sie kaputt gehen, ist das Leben leider nicht so einfach. Die Laufzeitanalyse findet das Problem nicht zuverlässig, da cat
das Lesen manchmal vor dem Abschneiden beendet ist. Statische Analyse kann es im Prinzip tun; Das vereinfachte Beispiel in Ihrer Frage wird von Shellcheck abgefangen , in einem komplexeren Skript wird jedoch möglicherweise kein ähnliches Problem festgestellt .