Wie kann das Lesen und Schreiben derselben Datei in derselben Pipeline immer "fehlschlagen"?

9

Angenommen, ich habe das folgende Skript:

#!/bin/bash
for i in $(seq 1000)
do
    cp /etc/passwd tmp
    cat tmp | head -1 | head -1 | head -1 > tmp  #this is the key line
    cat tmp
done

In der Schlüsselzeile lese und schreibe ich dieselbe Datei, tmpdie manchmal fehlschlägt.

(Ich habe gelesen, dass dies auf die Rennbedingungen zurückzuführen ist, da die Prozesse in der Pipeline parallel ausgeführt werden. Ich verstehe nicht, warum - jeder headmuss die Daten aus dem vorherigen übernehmen, nicht wahr? Dies ist NICHT meine Hauptfrage. aber du kannst es auch beantworten.)

Wenn ich das Skript ausführe, werden ungefähr 200 Zeilen ausgegeben. Gibt es eine Möglichkeit, dieses Skript zu zwingen, immer 0 Zeilen auszugeben (daher wird die E / A-Umleitung an tmpimmer zuerst vorbereitet und die Daten werden immer zerstört)? Um klar zu sein, meine ich das Ändern der Systemeinstellungen, nicht dieses Skript.

Vielen Dank für Ihre Ideen.

karlosss
quelle

Antworten:

2

Gilles 'Antwort erklärt den Rennzustand. Ich werde nur diesen Teil beantworten:

Gibt es eine Möglichkeit, dieses Skript zu zwingen, immer 0 Zeilen auszugeben (daher wird die E / A-Umleitung zu tmp immer zuerst vorbereitet und die Daten werden immer zerstört)? Um klar zu sein, meine ich das Ändern der Systemeinstellungen

IDK, wenn ein Tool dafür bereits vorhanden ist, aber ich habe eine Idee, wie eines implementiert werden könnte. (Beachten Sie jedoch, dass dies nicht immer 0 Zeilen sind, sondern nur ein nützlicher Tester, der einfache Rennen wie dieses leicht fängt, und einige kompliziertere Rennen. Siehe @ Gilles 'Kommentar .) Es würde nicht garantieren, dass ein Skript sicher ist , aber möglicherweise Dies ist ein nützliches Tool zum Testen, ähnlich dem Testen eines Multithread-Programms auf verschiedenen CPUs, einschließlich schwach geordneter Nicht-x86-CPUs wie ARM.

Sie würden es als laufen lassen racechecker bash foo.sh

Verwenden Sie die gleiche System-Call - Tracing / Abfangen von Einrichtungen , die strace -fund die ltrace -fVerwendung jedes Kind Prozess anhängen. (Unter Linux ist dies derselbe ptraceSystemaufruf, der von GDB und anderen Debuggern verwendet wird , um Haltepunkte festzulegen, einzelne Schritte auszuführen und Speicher / Register eines anderen Prozesses zu ändern.)

Instrument der openund openatSystemaufrufe: wenn jeder Prozess unter diesem Tool läuft spielt einen open(2)Systemaufruf (oder openat) mit O_RDONLY, schläft vielleicht 1/2 oder 1 Sekunde. Lassen Sie andere openSystemaufrufe (insbesondere solche einschließlich O_TRUNC) unverzüglich ausführen.

Dies sollte es dem Schreiber ermöglichen, das Rennen in nahezu jeder Rennbedingung zu gewinnen, es sei denn, die Systemlast war ebenfalls hoch oder es war eine komplizierte Rennbedingung, bei der die Kürzung erst nach einem anderen Lesevorgang erfolgte. Eine zufällige Variation, bei der open()s (und möglicherweise read()s oder Schreibvorgänge) verzögert sind, würde die Erkennungsleistung dieses Tools erhöhen, aber natürlich ohne unendlich lange Tests mit einem Verzögerungssimulator, der schließlich alle möglichen Situationen abdeckt, in denen Sie auftreten können In der realen Welt können Sie nicht sicher sein, dass Ihre Skripte frei von Rennen sind, wenn Sie sie nicht sorgfältig lesen und beweisen , dass dies nicht der Fall ist .


Sie würden wahrscheinlich es auf die weiße Liste (nicht Verzögerung benötigen openfür Dateien in) /usr/binund /usr/libso Prozessstart immer nicht statt. (Die dynamische Laufzeitverknüpfung muss open()mehrere Dateien umfassen (siehe strace -eopen /bin/trueoder /bin/lsirgendwann). Wenn die übergeordnete Shell selbst die Kürzung vornimmt, ist dies in Ordnung. Es ist jedoch gut, wenn dieses Tool Skripte nicht unangemessen langsam macht.)

Oder vielleicht eine Whitelist für jede Datei, für die der aufrufende Prozess überhaupt keine Berechtigung zum Abschneiden hat. Das heißt, der Ablaufverfolgungsprozess kann einen access(2)Systemaufruf ausführen, bevor der Prozess, der open()eine Datei enthalten wollte, tatsächlich angehalten wird .


racecheckerselbst müsste in C geschrieben werden, nicht in der Shell, könnte aber möglicherweise straceden Code als Ausgangspunkt verwenden und erfordert möglicherweise nicht viel Arbeit bei der Implementierung.

Sie könnten möglicherweise die gleiche Funktionalität mit einem FUSE-Dateisystem erhalten . Es gibt wahrscheinlich ein FUSE-Beispiel für ein reines Passthrough-Dateisystem, sodass Sie der open()Funktion Überprüfungen hinzufügen können , die sie für schreibgeschützte Öffnungen in den Ruhezustand versetzen, aber das Abschneiden sofort zulassen.

Peter Cordes
quelle
Ihre Idee für einen Race Checker funktioniert nicht wirklich. Erstens gibt es das Problem, dass Zeitüberschreitungen nicht zuverlässig sind: Eines Tages wird der andere länger dauern als erwartet (es ist ein klassisches Problem mit Build- oder Testskripten, die eine Weile zu funktionieren scheinen und dann auf schwer zu debuggende Weise fehlschlagen wenn sich die Arbeitslast erweitert und viele Dinge parallel laufen). Aber darüber hinaus, zu welchem Open werden Sie eine Verzögerung hinzufügen? Um etwas Interessantes zu erkennen, müssten Sie viele Läufe mit unterschiedlichen Verzögerungsmustern durchführen und deren Ergebnisse vergleichen.
Gilles 'SO - hör auf böse zu sein'
@ Gilles: Richtig, eine relativ kurze Verzögerung garantiert nicht , dass das Abschneiden das Rennen gewinnt (auf einer stark beladenen Maschine, wie Sie hervorheben ). Die Idee hier ist, dass Sie dies verwenden, um Ihr Skript einige Male zu testen , und nicht, dass Sie es racecheckerständig verwenden. Und wahrscheinlich möchten Sie die Ruhezeit zum Lesen öffnen, um sie für Benutzer auf sehr stark ausgelasteten Computern konfigurierbar zu machen, die sie höher einstellen möchten, z. B. 10 Sekunden. Oder setzen Sie ihn senken, wie 0,1 Sekunden für lange oder ineffiziente Skripte , die Wieder geöffneten Dateien eine Menge .
Peter Cordes
@ Gilles: Großartige Idee zu verschiedenen Verzögerungsmustern, mit denen Sie möglicherweise mehr Rennen als nur das einfache Material innerhalb derselben Pipeline verfolgen können, das "offensichtlich sein sollte (sobald Sie wissen, wie Shells funktionieren)", wie im Fall des OP. Aber "was öffnet sich?" Jedes schreibgeschützte Öffnen mit einer Whitelist oder einer anderen Möglichkeit, den Prozessstart nicht zu verzögern.
Peter Cordes
Ich denke, Sie denken an komplexere Rennen mit Hintergrundjobs, die erst nach Abschluss eines anderen Prozesses abgeschnitten werden? Ja, möglicherweise sind zufällige Variationen erforderlich, um dies zu erfassen. Oder schauen Sie sich den Prozessbaum an und verzögern Sie "frühe" Lesevorgänge, um zu versuchen, die übliche Reihenfolge umzukehren. Sie könnten das Tool immer komplizierter machen, um immer mehr Nachbestellungsmöglichkeiten zu simulieren, aber irgendwann müssen Sie Ihre Programme immer noch richtig entwerfen, wenn Sie Multitasking ausführen. Automatisierte Tests können für einfachere Skripte nützlich sein, bei denen die möglichen Probleme eingeschränkter sind.
Peter Cordes
Es ist dem Testen von Multithread-Code ziemlich ähnlich, insbesondere von Algorithmen ohne Sperre: Es ist sehr wichtig, logische Überlegungen darüber anzustellen, warum dies korrekt ist, ebenso wie das Testen, da Sie sich nicht darauf verlassen können, auf einem bestimmten Satz von Maschinen zu testen, um alle möglichen Neuordnungen zu erzielen Seien Sie ein Problem, wenn Sie nicht alle Lücken geschlossen haben. Aber genau wie das Testen auf einer schwach geordneten Architektur wie ARM oder PowerPC in der Praxis eine gute Idee ist, kann das Testen eines Skripts unter einem System, das Dinge künstlich verzögert, einige Rennen aufdecken , also ist es besser als nichts. Sie können immer Fehler einführen, die nicht erkannt werden!
Peter Cordes
18

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 -xdruckt 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):

  1. Führen Sie das externe Programm catmit dem Argument aus tmp.
  2. tmpZum Lesen geöffnet .
  3. 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:

  1. Leiten Sie die Standardausgabe an um tmpund kürzen Sie dabei die Datei.
  2. Führen Sie das externe Programm headmit dem Argument aus -1.
  3. 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 tmpund tmpparallel lesen , damit die beiden Dinge parallel ausgeführt werden.

Ok, es gibt eine "Systemeinstellung", die Sie ändern könnten: Sie könnten /bin/bashdurch 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 catLesen 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 catdas 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 .

Gilles 'SO - hör auf böse zu sein'
quelle
Das war mein Ziel, festzustellen, ob das Skript gut geschrieben ist oder nicht. Wenn das Skript Daten auf diese Weise zerstört hat, wollte ich nur, dass sie jedes Mal zerstört werden. Es ist nicht gut zu hören, dass dies fast unmöglich ist. Dank Ihnen weiß ich jetzt, wo das Problem liegt, und werde versuchen, eine Lösung zu finden.
Karlos
@karlosss: Hmm, ich frage mich, ob Sie das gleiche System-Call-Tracing / Intercepting-Material wie strace(z. B. Linux ptrace) verwenden könnten , um alle openSystemlesungen zum Lesen (in allen untergeordneten Prozessen) eine halbe Sekunde lang schlafen zu lassen, also wenn Sie mit Rennen fahren Bei einer Kürzung gewinnt die Kürzung fast immer.
Peter Cordes
@PeterCordes Ich bin ein Neuling in diesem Bereich. Wenn Sie einen Weg finden, dies zu erreichen und ihn als Antwort aufzuschreiben, werde ich ihn akzeptieren.
Karlos
@PeterCordes Sie können nicht garantieren, dass die Kürzung mit einer Verzögerung gewinnt. Es wird die meiste Zeit funktionieren, aber gelegentlich schlägt Ihr Skript auf einem stark geladenen Computer auf mehr oder weniger mysteriöse Weise fehl.
Gilles 'SO - hör auf böse zu sein'
@ Gilles: Lass uns das unter meiner Antwort diskutieren.
Peter Cordes