Wie kann sichergestellt werden, dass nur eine Instanz eines Bash-Skripts ausgeführt wird?

26

Eine Lösung, die keine zusätzlichen Tools erfordert, wäre vorzuziehen.

Tobias Kienzler
quelle
Was ist mit einer Sperrdatei?
Marco
@Marco Ich fand diese SO Antwort mit, aber wie in einem Kommentar angegeben , kann dies eine Rennbedingung erstellen
Tobias Kienzler
3
Das ist BashFAQ 45 .
Jw013
@ jw013 danke! Vielleicht wird also so etwas wie ln -s my.pid .lockdie Sperre beanspruchen (gefolgt von echo $$ > my.pid) und bei einem Fehler überprüfen können, ob die in gespeicherte PID .lockwirklich eine aktive Instanz des Skripts ist
Tobias Kienzler

Antworten:

18

Fast wie nsg Antwort: Verwenden Sie ein Lock - Verzeichnis . Die Verzeichniserstellung ist unter Linux und Unix sowie unter * BSD und vielen anderen Betriebssystemen atomar.

if mkdir $LOCKDIR
then
    # Do important, exclusive stuff
    if rmdir $LOCKDIR
    then
        echo "Victory is mine"
    else
        echo "Could not remove lock dir" >&2
    fi
else
    # Handle error condition
    ...
fi

Sie können die PID des Locking-Sh zu Debugging-Zwecken in eine Datei im Lock-Verzeichnis einfügen, aber nicht in die Falle tappen, dass Sie diese PID überprüfen können, um festzustellen, ob der Locking-Prozess noch ausgeführt wird. Viele Rennbedingungen liegen auf diesem Weg.

Bruce Ediger
quelle
1
Ich würde in Betracht ziehen, die gespeicherte PID zu verwenden, um zu überprüfen, ob die Sperrinstanz noch aktiv ist. Aber hier ist eine Forderung , die mkdirnicht atomar auf NFS ist (was für mich nicht der Fall ist, aber ich denke , man sollte erwähnen, dass wenn true)
Tobias Kienzler
Ja, verwenden Sie auf jeden Fall die gespeicherte PID, um festzustellen, ob der Sperrvorgang noch ausgeführt wird, aber versuchen Sie nicht, etwas anderes als eine Nachricht zu protokollieren. Die Arbeit, die gespeicherte PID zu überprüfen, eine neue PID-Datei usw. zu erstellen, lässt ein großes Fenster für Rennen.
Bruce Ediger
Ok, wie Ihunath feststellte , wäre das lockdir höchstwahrscheinlich das, in /tmpdem normalerweise kein NFS geteilt wird, also sollte das in Ordnung sein.
Tobias Kienzler
Ich würde verwenden rm -rf, um das Sperrverzeichnis zu entfernen. rmdirschlägt fehl, wenn jemand (nicht unbedingt Sie) es geschafft hat, eine Datei zum Verzeichnis hinzuzufügen.
Chepner
18

Um Bruce Edigers Antwort zu ergänzen und sich von dieser Antwort inspirieren zu lassen , sollten Sie auch die Bereinigung intelligenter gestalten, um die Beendigung des Skripts zu verhindern:

#Remove the lock directory
function cleanup {
    if rmdir $LOCKDIR; then
        echo "Finished"
    else
        echo "Failed to remove lock directory '$LOCKDIR'"
        exit 1
    fi
}

if mkdir $LOCKDIR; then
    #Ensure that if we "grabbed a lock", we release it
    #Works for SIGTERM and SIGINT(Ctrl-C)
    trap "cleanup" EXIT

    echo "Acquired lock, running"

    # Processing starts here
else
    echo "Could not create lock directory '$LOCKDIR'"
    exit 1
fi
Igor Zevaka
quelle
Alternativ if ! mkdir "$LOCKDIR"; then handle failure to lock and exit; fi trap and do processing after if-statement.
Kusalananda
6

Dies kann zu einfach sein, bitte korrigieren Sie mich, wenn ich falsch liege. Ist das nicht einfach psgenug?

#!/bin/bash 

me="$(basename "$0")";
running=$(ps h -C "$me" | grep -wv $$ | wc -l);
[[ $running > 1 ]] && exit;

# do stuff below this comment
terdon
quelle
1
Schön und / oder genial. :)
Spooky
1
Ich habe diese Bedingung für eine Woche verwendet, und in 2 Fällen verhinderte es nicht, dass ein neuer Prozess gestartet wurde. Ich habe herausgefunden, was das Problem ist - new pid ist eine Teilzeichenfolge der alten und wird von versteckt grep -v $$. Beispiele: alt - 14532, neu - 1453, alt - 28858, neu - 858.
Naktibalda
Ich habe das grep -v $$grep -v "^${$} "
Problem
@Naktibalda guten Fang, danke! Sie können es auch mit grep -wv "^$$"(siehe Bearbeiten) beheben .
Terdon
Danke für das Update. Mein Muster schlug gelegentlich fehl, weil kürzere Pids mit Leerzeichen aufgefüllt wurden.
Naktibalda
4

Ich würde eine Sperrdatei verwenden, wie von Marco erwähnt

#!/bin/bash

# Exit if /tmp/lock.file exists
[ -f /tmp/lock.file ] && exit

# Create lock file, sleep 1 sec and verify lock
echo $$ > /tmp/lock.file
sleep 1
[ "x$(cat /tmp/lock.file)" == "x"$$ ] || exit

# Do stuff
sleep 60

# Remove lock file
rm /tmp/lock.file
nsg
quelle
1
(Ich glaube, Sie haben vergessen, die Sperrdatei zu erstellen.) Was ist mit den Rennbedingungen ?
Tobias Kienzler
ops :) Ja, die Rennbedingungen sind in meinem Beispiel ein Problem. Normalerweise schreibe ich stündliche oder tägliche Cronjobs, und die Rennbedingungen sind selten.
nsg
Sie sollten auch in meinem Fall nicht relevant sein, aber es ist etwas, das man beachten sollte. Vielleicht ist die Verwendung auch lsof $0nicht schlecht?
Tobias Kienzler
Sie können die Wettkampfbedingungen verringern, indem Sie Ihre $$in die Sperrdatei schreiben . Dann sleepfür eine kurze Pause und lesen Sie es zurück. Wenn die PID immer noch Ihre ist, haben Sie die Sperre erfolgreich erworben. Benötigt absolut kein zusätzliches Werkzeug.
Manatwork
1
Ich habe lsof nie für diesen Zweck verwendet, ich dies sollte es funktionieren. Beachten Sie, dass lsof in meinem System sehr langsam ist (1-2 Sek.) Und wahrscheinlich viel Zeit für die Rennbedingungen zur Verfügung steht.
nsg
3

Wenn Sie sicherstellen möchten, dass nur eine Instanz Ihres Skripts ausgeführt wird, schauen Sie sich Folgendes an:

Sperren Sie Ihr Skript (gegen parallele Ausführung)

Andernfalls können Sie überprüfen psoder aufrufen lsof <full-path-of-your-script>, da ich sie nicht als zusätzliche Tools bezeichnen würde.


Ergänzung :

Eigentlich habe ich mir überlegt, es so zu machen:

for LINE in `lsof -c <your_script> -F p`; do 
    if [ $$ -gt ${LINE#?} ] ; then
        echo "'$0' is already running" 1>&2
        exit 1;
    fi
done

Auf diese Weise wird sichergestellt, dass nur der Prozess mit dem niedrigsten pidWert weiter ausgeführt wird, auch wenn Sie mehrere Instanzen <your_script>gleichzeitig ausführen .

user1146332
quelle
1
Vielen Dank für den Link, aber könnten Sie die wesentlichen Teile in Ihre Antwort aufnehmen? Es ist eine gängige Politik bei SE, um das Verrotten von Links zu verhindern ... Aber [[(lsof $0 | wc -l) > 2]] && exitvielleicht reicht so etwas tatsächlich aus, oder ist dies auch anfällig für Rennbedingungen?
Tobias Kienzler,
Sie haben Recht, der wesentliche Teil meiner Antwort fehlte, und nur Links zu posten ist ziemlich lahm. Ich habe der Antwort meinen eigenen Vorschlag hinzugefügt.
user1146332
3

Eine andere Möglichkeit, um sicherzustellen, dass eine einzelne Instanz des Bash-Skripts ausgeführt wird:

#!/bin/bash

# Check if another instance of script is running
pidof -o %PPID -x $0 >/dev/null && echo "ERROR: Script $0 already running" && exit 1

...

pidof -o %PPID -x $0 Ruft die PID des vorhandenen Skripts ab, wenn es bereits ausgeführt wird, oder wird mit Fehlercode 1 beendet, wenn kein anderes Skript ausgeführt wird

Sethu
quelle
3

Obwohl Sie nach einer Lösung ohne zusätzliche Tools gefragt haben, verwende ich am liebsten die folgenden Methoden flock:

#!/bin/sh

[ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en "$0" "$0" "$@" || :

echo "servus!"
sleep 10

Dies stammt aus dem Beispielteil von man flock, in dem Folgendes näher erläutert wird:

Dies ist ein nützlicher Code für Shell-Skripte. Setzen Sie es oben in das Shell-Skript, das Sie sperren möchten, und es sperrt sich automatisch beim ersten Start. Wenn env var $ FLOCKER nicht auf das ausgeführte Shell-Skript festgelegt ist, führen Sie flock aus und greifen Sie auf eine exklusive nicht blockierende Sperre zu (verwenden Sie das Skript selbst als Sperrdatei), bevor Sie sich mit den richtigen Argumenten erneut ausführen. Außerdem wird die FLOCKER-Umgebungsvariable auf den richtigen Wert gesetzt, damit sie nicht erneut ausgeführt wird.

Punkte, die man beachten sollte:

Siehe auch https://stackoverflow.com/questions/185451/quick-and-dirty-way-to-sure-only-one-instance-of-a-shell-script-is-running-at .

schieferstapel
quelle
1

Dies ist eine modifizierte Version von Anselmos Antwort . Die Idee ist, einen schreibgeschützten Dateideskriptor mit dem Bash-Skript selbst zu erstellen und flockdie Sperre zu handhaben.

SCRIPT=`realpath $0`     # get absolute path to the script itself
exec 6< "$SCRIPT"        # open bash script using file descriptor 6
flock -n 6 || { echo "ERROR: script is already running" && exit 1; }   # lock file descriptor 6 OR show error message if script is already running

echo "Run your single instance code here"

Der Hauptunterschied zu allen anderen Antworten besteht darin, dass dieser Code das Dateisystem nicht verändert, einen sehr geringen Platzbedarf aufweist und keine Bereinigung erfordert, da der Dateideskriptor geschlossen wird, sobald das Skript unabhängig vom Beendigungsstatus beendet wird. Somit ist es egal, ob das Skript fehlschlägt oder erfolgreich ist.

John Doe
quelle
Sie sollten immer alle Shell-Variablenreferenzen angeben, es sei denn, Sie haben einen guten Grund, dies nicht zu tun , und Sie sind sicher , dass Sie wissen, was Sie tun. Also solltest du machen exec 6< "$SCRIPT".
Scott
@Scott Ich habe den Code gemäß Ihren Vorschlägen geändert. Danke vielmals.
John Doe
1

Ich verwende cksum, um zu überprüfen, ob mein Skript wirklich eine einzelne Instanz ausführt, auch wenn ich den Dateinamen und den Dateipfad ändere .

Ich verwende keine Trap & Lock-Datei. Wenn mein Server plötzlich ausfällt, muss ich die gesperrte Datei manuell entfernen, nachdem der Server hochgefahren ist.

Hinweis: #! / Bin / bash in der ersten Zeile ist für grep ps erforderlich

#!/bin/bash

checkinstance(){
   nprog=0
   mysum=$(cksum $0|awk '{print $1}')
   for i in `ps -ef |grep /bin/bash|awk '{print $2}'`;do 
        proc=$(ls -lha /proc/$i/exe 2> /dev/null|grep bash) 
        if [[ $? -eq 0 ]];then 
           cmd=$(strings /proc/$i/cmdline|grep -v bash)
                if [[ $? -eq 0 ]];then 
                   fsum=$(cksum /proc/$i/cwd/$cmd|awk '{print $1}')
                   if [[ $mysum -eq $fsum ]];then
                        nprog=$(($nprog+1))
                   fi
                fi
        fi
   done

   if [[ $nprog -gt 1 ]];then
        echo $0 is already running.
        exit
   fi
}

checkinstance 

#--- run your script bellow 

echo pass
while true;do sleep 1000;done

Oder Sie können cksum in Ihrem Skript fest codieren, sodass Sie sich keine Sorgen mehr machen, wenn Sie den Dateinamen, den Pfad oder den Inhalt Ihres Skripts ändern möchten .

#!/bin/bash

mysum=1174212411

checkinstance(){
   nprog=0
   for i in `ps -ef |grep /bin/bash|awk '{print $2}'`;do 
        proc=$(ls -lha /proc/$i/exe 2> /dev/null|grep bash) 
        if [[ $? -eq 0 ]];then 
           cmd=$(strings /proc/$i/cmdline|grep -v bash)
                if [[ $? -eq 0 ]];then 
                   fsum=$(grep mysum /proc/$i/cwd/$cmd|head -1|awk -F= '{print $2}')
                   if [[ $mysum -eq $fsum ]];then
                        nprog=$(($nprog+1))
                   fi
                fi
        fi
   done

   if [[ $nprog -gt 1 ]];then
        echo $0 is already running.
        exit
   fi
}

checkinstance

#--- run your script bellow

echo pass
while true;do sleep 1000;done
Arputra
quelle
1
Bitte erläutern Sie genau, wie gut es ist, die Prüfsumme fest zu codieren.
Scott
Wenn eine andere Instanz ausgeführt wird, wird der andere Shell-Skriptprozess überprüft und die Datei zuerst kategorisiert, wenn sich Ihr Identitätsschlüssel in dieser Datei befindet. Dies bedeutet, dass Ihre Instanz bereits ausgeführt wird.
Arputra
OKAY; Bitte bearbeite deine Antwort, um das zu erklären. Und veröffentlichen Sie in Zukunft bitte nicht mehrere Codeblöcke mit einer Länge von 30 Zeilen, die aussehen, als wären sie (fast) identisch, ohne zu sagen und zu erklären, wie sie sich unterscheiden. Und sagen Sie keine Dinge wie "Sie können cksum in Ihrem Skript fest codieren", und verwenden Sie keine Variablennamen mehr mysumund fsumwenn Sie nicht mehr über eine Prüfsumme sprechen.
Scott,
Sieht interessant aus, danke! Und willkommen bei unix.stackexchange :)
Tobias Kienzler
0

Sie können dies verwenden: https://github.com/sayanarijit/pidlock

sudo pip install -U pidlock

pidlock -n sleepy_script -c 'sleep 10'
Arijit Basu
quelle
> Eine Lösung, die keine zusätzlichen Werkzeuge erfordert, wäre vorzuziehen.
Dhag
0

Mein Code für Sie

#!/bin/bash

script_file="$(/bin/readlink -f $0)"
lock_file=${script_file////_}

function executing {
  echo "'${script_file}' already executing"
  exit 1
}

(
  flock -n 9 || executing

  sleep 10

) 9> /var/lock/${lock_file}

Basierend auf man flock, nur verbessern:

  • Der Name der Sperrdatei, der auf dem vollständigen Namen des Skripts basiert
  • die Nachricht executing

Wo ich das hier hinstelle sleep 10, kann man das ganze Hauptskript setzen.

Anselmo Blanco Dominguez
quelle