Wie stellt dieses Skript sicher, dass nur eine Instanz von sich selbst ausgeführt wird?

22

Am 19. August 2013, Randal L. Schwartz geschrieben diesen Shell - Skript, das auf Linux , um sicherzustellen , sollte, „dass nur eine Instanz von [dem] Skript ausgeführt wird , ohne Rennbedingungen oder mit Lock - Dateien zu bereinigen“:

#!/bin/sh
# randal_l_schwartz_001.sh
(
    if ! flock -n -x 0
    then
        echo "$$ cannot get flock"
        exit 0
    fi
    echo "$$ start"
    sleep 10 # for testing.  put the real task here
    echo "$$ end"
) < $0

Es scheint wie angekündigt zu funktionieren:

$ ./randal_l_schwartz_001.sh & ./randal_l_schwartz_001.sh
[1] 11863
11863 start
11864 cannot get flock
$ 11863 end

[1]+  Done                    ./randal_l_schwartz_001.sh
$

Folgendes verstehe ich:

  • Das Skript leitet ( <) eine Kopie seines eigenen Inhalts (dh von $0) an die STDIN (dh den Dateideskriptor 0) einer Subshell weiter.
  • Innerhalb der Subshell versucht das Skript, eine nicht blockierende exklusive Sperre ( flock -n -x) für den Dateideskriptor abzurufen 0.
    • Wenn dieser Versuch fehlschlägt, wird die Subshell beendet (und das Hauptskript, da nichts anderes zu tun ist).
    • Wenn der Versuch stattdessen erfolgreich ist, führt die Subshell die gewünschte Aufgabe aus.

Hier sind meine Fragen:

  • Warum muss das Skript eine Kopie seines eigenen Inhalts an einen von der Subshell geerbten Dateideskriptor umleiten, anstatt beispielsweise den Inhalt einer anderen Datei? (Ich habe versucht, von einer anderen Datei umzuleiten und wie oben beschrieben erneut auszuführen, und die Ausführungsreihenfolge hat sich geändert: Die nicht im Hintergrund befindliche Task hat die Sperre vor der im Hintergrund befindlichen erhalten. Wenn Sie also den eigenen Inhalt der Datei verwenden, vermeiden Sie Rennbedingungen. Aber wie?)
  • Warum muss das Skript überhaupt eine Kopie des Inhalts einer Datei an einen Dateideskriptor umleiten, der von der Subshell geerbt wird?
  • Warum verhindert das Halten einer exklusiven Sperre für den Dateideskriptor 0in einer Shell, dass eine Kopie desselben Skripts, die in einer anderen Shell ausgeführt wird, eine exklusive Sperre für den Dateideskriptor erhält 0? Nicht Schalen haben ihre eigenen, separaten Kopien der Standard - Datei - Deskriptoren ( 0, 1, und 2, also STDIN, STDOUT und STDERR)?
sampablokuper
quelle
Was war Ihr genauer Testprozess, als Sie in Ihrem Experiment versuchten, von einer anderen Datei umzuleiten?
Freiheit
1
Ich denke, Sie können diesen Link verweisen. stackoverflow.com/questions/185451/…
Deb Paikar

Antworten:

22

Warum muss das Skript eine Kopie seines eigenen Inhalts an einen von der Subshell geerbten Dateideskriptor umleiten, anstatt beispielsweise den Inhalt einer anderen Datei?

Sie können eine beliebige Datei verwenden, sofern alle Kopien des Skripts dieselbe verwenden. Mit " $0Nur" wird die Sperre an das Skript selbst gebunden: Wenn Sie das Skript kopieren und für eine andere Verwendung ändern, müssen Sie keinen neuen Namen für die Sperrdatei festlegen. Das ist praktisch.

Wenn das Skript über einen Symlink aufgerufen wird, befindet sich die Sperre in der eigentlichen Datei und nicht in der Verknüpfung.

(Wenn ein Prozess das Skript ausführt und es als nulltes Argument anstelle des tatsächlichen Pfads als erfundenen Wert ausgibt, bricht dies natürlich ab. Dies wird jedoch selten durchgeführt.)

(Ich habe versucht, eine andere Datei zu verwenden und erneut auszuführen, und die Ausführungsreihenfolge hat sich geändert.)

Sind Sie sicher, dass dies an der verwendeten Datei und nicht nur an zufälligen Variationen lag? Wie bei einer Pipeline gibt es keine Möglichkeit, sicherzugehen, in welcher Reihenfolge die Befehle ausgeführt werden cmd1 & cmd. Es liegt hauptsächlich am OS-Scheduler. Ich erhalte zufällige Abweichungen von meinem System.

Warum muss das Skript überhaupt eine Kopie des Inhalts einer Datei an einen Dateideskriptor umleiten, der von der Subshell geerbt wird?

Es sieht so aus, als ob dies so ist, dass die Shell selbst eine Kopie der Dateibeschreibung enthält, die die Sperre enthält, anstatt nur das flockDienstprogramm, das sie enthält. Eine mit gemachte Sperre flock(2)wird freigegeben, wenn die Dateideskriptoren, die sie haben, geschlossen werden.

flockEs stehen zwei Modi zur Verfügung, entweder um eine Sperre auf der Grundlage eines Dateinamens zu setzen und einen externen Befehl auszuführen (in diesem Fall wird flockder erforderliche offene Dateideskriptor gespeichert) oder um einen Dateideskriptor von außen zu speichern, sodass ein externer Prozess für das Speichern verantwortlich ist es.

Beachten Sie, dass der Inhalt der Datei hier nicht relevant ist und keine Kopien erstellt werden. Die Umleitung zur Subshell kopiert keine Daten in sich selbst, sondern öffnet lediglich ein Handle für die Datei.

Warum verhindert das Halten einer exklusiven Sperre für Dateideskriptor 0 in einer Shell, dass eine Kopie desselben Skripts, die in einer anderen Shell ausgeführt wird, eine exklusive Sperre für Dateideskriptor 0 erhält? Haben Shells keine eigenen, separaten Kopien der Standard-Dateideskriptoren (0, 1 und 2, dh STDIN, STDOUT und STDERR)?

Ja, aber die Sperre bezieht sich auf die Datei , nicht auf den Dateideskriptor. Es kann immer nur eine geöffnete Instanz der Datei die Sperre halten.


Ich denke, Sie sollten in der Lage sein, dasselbe ohne die Subshell zu tun, indem Sie execein Handle für die Sperrdatei öffnen:

$ cat lock.sh
#!/bin/sh

exec 9< "$0"

if ! flock -n -x 9; then
    echo "$$/$1 cannot get flock" 
    exit 0
fi

echo "$$/$1 got the lock"
sleep 2
echo "$$/$1 exit"

$ ./lock.sh bg & ./lock.sh fg ; wait; echo
[1] 11362
11363/fg got the lock
11362/bg cannot get flock
11363/fg exit
[1]+  Done                    ./lock.sh bg
ilkkachu
quelle
1
Verwenden { }statt ( )würde auch funktionieren und die Unterschale vermeiden.
R ..
Weiter unten in den Kommentaren zum G + -Post schlug jemand dort in etwa die gleiche Methode vor exec.
David Z
@ R .., oh, sicher. Aber es ist immer noch hässlich mit den zusätzlichen Klammern um das eigentliche Skript.
Ilkkachu
9

Ein Dateisperre angebracht ist, um eine Datei durch eine Dateibeschreibung . Auf hoher Ebene lautet die Abfolge der Vorgänge in einer Instanz des Skripts wie folgt:

  1. Öffnen Sie die Datei, an die die Sperre angehängt ist ("die Sperrdatei").
  2. Machen Sie eine Sperre für die Sperrdatei.
  3. Sachen machen.
  4. Schließen Sie die Sperrdatei. Dadurch wird die Sperre aufgehoben, die an die Dateibeschreibung angehängt ist, die durch Öffnen einer Datei erstellt wurde.

Wenn Sie die Sperre gedrückt halten, wird verhindert, dass eine weitere Kopie desselben Skripts ausgeführt wird, da Sperren dies tun. Solange eine exklusive Sperre für eine Datei irgendwo im System vorhanden ist, ist es unmöglich, eine zweite Instanz derselben Sperre zu erstellen, selbst wenn eine andere Dateibeschreibung verwendet wird.

Beim Öffnen einer Datei wird eine Dateibeschreibung erstellt . Dies ist ein Kernel-Objekt, das in Programmierschnittstellen nicht direkt sichtbar ist. Sie greifen indirekt über Dateideskriptoren auf eine Dateibeschreibung zu, normalerweise stellen Sie sich den Zugriff auf die Datei vor (Lesen oder Schreiben des Inhalts oder der Metadaten). Eine Sperre ist eines der Attribute, die eine Eigenschaft für die Dateibeschreibung und nicht für eine Datei oder einen Deskriptor sind.

Zu Beginn des Öffnens einer Datei verfügt die Dateibeschreibung über einen einzelnen Dateideskriptor. Sie können jedoch weitere Deskriptoren erstellen, indem Sie entweder einen anderen Deskriptor (die dupFamilie der Systemaufrufe) erstellen oder einen Unterprozess forken (nach dem sowohl der übergeordnete als auch der übergeordnete Deskriptor ) Kind hat Zugriff auf die gleiche Dateibeschreibung). Ein Dateideskriptor kann explizit geschlossen werden oder wenn der Prozess, in dem er sich befindet, abstürzt. Wenn der letzte an eine Datei angehängte Dateideskriptor geschlossen wird, wird die Dateibeschreibung geschlossen.

So wirkt sich die oben beschriebene Abfolge von Vorgängen auf die Dateibeschreibung aus.

  1. Die Umleitung <$0öffnet die Skriptdatei in der Subshell und erstellt eine Dateibeschreibung. An dieser Stelle befindet sich ein einzelner Dateideskriptor, der an die Beschreibung angehängt ist: Deskriptor Nummer 0 in der Subshell.
  2. Die Subshell wird aufgerufen flockund wartet darauf, dass sie beendet wird. Während Flock ausgeführt wird, sind zwei Deskriptoren an die Beschreibung angehängt: Nummer 0 in der Subshell und Nummer 0 im Flock-Prozess. Wenn Flock die Sperre übernimmt, wird eine Eigenschaft der Dateibeschreibung festgelegt. Wenn eine andere Dateibeschreibung bereits eine Sperre für die Datei hat, kann Flock die Sperre nicht übernehmen, da es sich um eine exklusive Sperre handelt.
  3. Die Subshell macht Sachen. Da für die Beschreibung mit der Sperre immer noch ein offener Dateideskriptor vorhanden ist, bleibt diese Beschreibung bestehen und die Sperre bleibt bestehen, da niemand die Sperre aufhebt.
  4. Die Unterschale stirbt in der schließenden Klammer. Dadurch wird der letzte Dateideskriptor in der Dateibeschreibung mit der Sperre geschlossen, sodass die Sperre an dieser Stelle verschwindet.

Der Grund, warum das Skript eine Umleitung von verwendet, $0ist, dass die Umleitung die einzige Möglichkeit ist, eine Datei in der Shell zu öffnen, und dass eine Umleitung aktiv bleibt, die einzige Möglichkeit, einen Dateideskriptor offen zu halten. Die Subshell liest nie von ihrer Standardeingabe, sondern muss nur geöffnet bleiben. In einer Sprache, die direkten Zugriff auf das Öffnen und Schließen von Anrufen bietet, können Sie verwenden

fd = open($0)
flock(fd, LOCK_EX)
do stuff
close(fd)

Sie können die gleiche Abfolge von Operationen in der Shell erhalten, wenn Sie die Umleitung mit dem execeingebauten Befehl ausführen.

exec <$0
flock -n -x 0
# do stuff
exec <&-

Das Skript könnte einen anderen Dateideskriptor verwenden, wenn weiterhin auf die ursprüngliche Standardeingabe zugegriffen werden soll.

exec 3<$0
flock -n -x 0
# do stuff
exec 3<&-

oder mit einer Unterschale:

(
  flock -n -x 3
  # do stuff
) 3<$0

Die Sperre muss sich nicht in der Skriptdatei befinden. Es kann sich um eine beliebige Datei handeln, die zum Lesen geöffnet werden kann (es muss also vorhanden sein, es muss sich um einen Dateityp handeln, der gelesen werden kann, z. B. eine reguläre Datei oder eine Named Pipe, jedoch kein Verzeichnis, und der Skriptprozess muss über Folgendes verfügen die Erlaubnis, es zu lesen). Die Skriptdatei hat den Vorteil, dass sie garantiert vorhanden und lesbar ist (außer in dem Randfall, in dem sie zwischen dem Aufrufen des Skripts und dem Erreichen der <$0Umleitung durch das Skript extern gelöscht wurde ).

Solange dies flockerfolgreich ist und sich das Skript in einem Dateisystem befindet, in dem Sperren nicht fehlerhaft sind (einige Netzwerkdateisysteme wie NFS sind möglicherweise fehlerhaft), verstehe ich nicht, wie die Verwendung einer anderen Sperrdatei eine Race Condition ermöglichen kann. Ich vermute einen Manipulationsfehler von Ihrer Seite.

Gilles 'SO - hör auf böse zu sein'
quelle
Es gibt eine Race-Bedingung: Sie können nicht steuern, welche Instanz des Skripts die Sperre erhält. Glücklicherweise spielt es für fast alle Zwecke keine Rolle.
Mark
4
@Mark Es gibt ein Rennen um das Schloss, aber es ist keine Rennbedingung. Eine Rennbedingung ist, wenn das Timing dazu führen kann, dass etwas Schlimmes passiert, z. B. dass sich zwei Prozesse zur gleichen Zeit im selben kritischen Bereich befinden. Wenn nicht bekannt ist, welcher Prozess in den kritischen Bereich eintritt, ist dies keine Rennbedingung.
Gilles 'SO - hör auf böse zu sein'
1
Nur zu Ihrer Information, der Link in der "Dateibeschreibung" verweist eher auf die Indexseite der Open Group Specs als auf eine spezifische Beschreibung des Konzepts. Oder Sie können Ihre ältere Antwort auch hier verlinken
Sergiy Kolodyazhnyy
5

Die zum Sperren verwendete Datei ist unwichtig, das Skript verwendet sie, $0da es sich um eine Datei handelt, von der bekannt ist, dass sie existiert.

Die Reihenfolge, in der die Sperren aktiviert werden, ist mehr oder weniger zufällig, je nachdem, wie schnell Ihr Computer die beiden Aufgaben starten kann.

Sie können einen beliebigen Dateideskriptor verwenden, nicht unbedingt 0. Die Sperre gilt für die Datei , die für den Dateideskriptor geöffnet wurde, nicht für den Deskriptor selbst.

( flock -x 9 || exit 1
  echo 'Locking for 5 secs'; sleep 5; echo 'Done' ) 9>/tmp/lock &
Kusalananda
quelle