189 votes

Un moyen rapide et efficace de s'assurer qu'une seule instance d'un shell script est en cours d'exécution à la fois.

Quel est le moyen le plus rapide de s'assurer qu'une seule instance d'un shell script est en cours d'exécution à un moment donné ?

156voto

lhunath Points 27045

Toutes les approches qui testent l'existence de "fichiers de verrouillage" sont défectueuses.

Pourquoi ? Parce qu'il n'y a aucun moyen de vérifier si un fichier existe et de le créer en une seule action atomique. A cause de cela, il y a une condition de course que WILL faire échouer vos tentatives d'exclusion mutuelle.

Au lieu de cela, vous devez utiliser mkdir . mkdir crée un répertoire s'il n'existe pas encore, et s'il existe, il définit un code de sortie. Plus important encore, elle fait tout cela en une seule action atomique, ce qui la rend parfaite pour ce scénario.

if ! mkdir /tmp/myscript.lock 2>/dev/null; then
    echo "Myscript is already running." >&2
    exit 1
fi

Pour tous les détails, consultez l'excellent BashFAQ : http://mywiki.wooledge.org/BashFAQ/045

Si vous voulez vous occuper des serrures périmées, fuser(1) est très utile. Le seul inconvénient ici est que l'opération prend environ une seconde, donc ce n'est pas instantané.

Voici une fonction que j'ai écrite une fois qui résout le problème en utilisant le fuser :

#       mutex file
#
# Open a mutual exclusion lock on the file, unless another process already owns one.
#
# If the file is already locked by another process, the operation fails.
# This function defines a lock on a file as having a file descriptor open to the file.
# This function uses FD 9 to open a lock on the file.  To release the lock, close FD 9:
# exec 9>&-
#
mutex() {
    local file=$1 pid pids 

    exec 9>>"$file"
    { pids=$(fuser -f "$file"); } 2>&- 9>&- 
    for pid in $pids; do
        [[ $pid = $$ ]] && continue

        exec 9>&- 
        return 1 # Locked by a pid.
    done 
}

Vous pouvez l'utiliser dans un script comme ceci :

mutex /var/run/myscript.lock || { echo "Already running." >&2; exit 1; }

Si vous ne vous souciez pas de la portabilité (ces solutions devraient fonctionner sur presque toutes les machines UNIX), le fuser(1) de Linux offre quelques options supplémentaires et il y a aussi flock(1).

103voto

bmdhacks Points 9074

Voici une implémentation qui utilise un fichier de verrouillage et y répercute un PID. Ceci sert de protection si le processus est tué avant la suppression de l'icône pidfile :

LOCKFILE=/tmp/lock.txt
if [ -e ${LOCKFILE} ] && kill -0 `cat ${LOCKFILE}`; then
    echo "already running"
    exit
fi

# make sure the lockfile is removed when we exit and then claim it
trap "rm -f ${LOCKFILE}; exit" INT TERM EXIT
echo $$ > ${LOCKFILE}

# do stuff
sleep 1000

rm -f ${LOCKFILE}

L'astuce ici est le kill -0 qui ne délivre aucun signal mais vérifie simplement si un processus avec le PID donné existe. De même, l'appel à trap veillera à ce que le fichier de verrouillage est supprimée même lorsque votre processus est tué (à l'exception de kill -9 ).

42voto

Cowan Points 17235

Il existe une enveloppe autour de l'appel système flock(2) appelée, de manière peu imaginative, flock(1). Cela rend relativement facile l'obtention fiable de verrous exclusifs sans se soucier du nettoyage, etc. Il y a des exemples sur la page de manuel sur la façon de l'utiliser dans un script shell.

24voto

Pour rendre le verrouillage fiable, il faut une opération atomique. Beaucoup des propositions ci-dessus ne sont pas atomiques. L'utilitaire lockfile(1) proposé semble prometteur comme le mentionne la man-page mentionne, qu'il est "résistant à NFS". Si votre système d'exploitation ne supporte pas lockfile(1) et que votre solution doit fonctionner sur NFS, vous n'avez pas beaucoup d'options....

NFSv2 a deux opérations atomiques :

  • lien symbolique
  • renommer

Avec NFSv3, l'appel de création est également atomique.

Les opérations de répertoire ne sont PAS atomiques sous NFSv2 et NFSv3 (veuillez vous référer au livre 'NFS Illustrated' de Brent Callaghan, ISBN 0-201-32570-5 ; Brent est un vétéran de NFS chez Sun).

Sachant cela, vous pouvez implémenter des spin-locks pour les fichiers et les répertoires (en shell, pas en PHP) :

verrouiller la direction actuelle :

while ! ln -s . lock; do :; done

verrouiller un fichier :

while ! ln -s ${f} ${f}.lock; do :; done

déverrouiller le répertoire actuel (en supposant que le processus en cours d'exécution a réellement acquis le verrou) :

mv lock deleteme && rm deleteme

déverrouiller un fichier (hypothèse, le processus en cours d'exécution a réellement acquis le verrou) :

mv ${f}.lock ${f}.deleteme && rm ${f}.deleteme

La suppression n'est pas non plus atomique, donc d'abord le renommage (qui est atomique) et ensuite la suppression.

Pour les appels symlink et rename, les deux noms de fichiers doivent résider sur le même système de fichiers. Ma proposition : n'utiliser que des noms de fichiers simples (pas de chemins) et mettre le fichier et le verrou dans le même répertoire.

22voto

Mikel Points 10000

Une autre option consiste à utiliser la fonction noclobber en exécutant set -C . Ensuite, > échouera si le fichier existe déjà.

En bref :

set -C
lockfile="/tmp/locktest.lock"
if echo "$$" > "$lockfile"; then
    echo "Successfully acquired lock"
    # do work
    rm "$lockfile"    # XXX or via trap - see below
else
    echo "Cannot acquire lock - already locked by $(cat "$lockfile")"
fi

Cela provoque l'appel du shell :

open(pathname, O_CREAT|O_EXCL)

qui crée atomiquement le fichier ou échoue si le fichier existe déjà.


Selon un commentaire sur BashFAQ 045 cela peut échouer dans ksh88 mais il fonctionne dans tous mes obus :

$ strace -e trace=creat,open -f /bin/bash /home/mikel/bin/testopen 2>&1 | grep -F testopen.lock
open("/tmp/testopen.lock", O_WRONLY|O_CREAT|O_EXCL|O_LARGEFILE, 0666) = 3

$ strace -e trace=creat,open -f /bin/zsh /home/mikel/bin/testopen 2>&1 | grep -F testopen.lock
open("/tmp/testopen.lock", O_WRONLY|O_CREAT|O_EXCL|O_NOCTTY|O_LARGEFILE, 0666) = 3

$ strace -e trace=creat,open -f /bin/pdksh /home/mikel/bin/testopen 2>&1 | grep -F testopen.lock
open("/tmp/testopen.lock", O_WRONLY|O_CREAT|O_EXCL|O_TRUNC|O_LARGEFILE, 0666) = 3

$ strace -e trace=creat,open -f /bin/dash /home/mikel/bin/testopen 2>&1 | grep -F testopen.lock
open("/tmp/testopen.lock", O_WRONLY|O_CREAT|O_EXCL|O_LARGEFILE, 0666) = 3

Intéressant que pdksh ajoute le O_TRUNC mais il est évident que c'est redondant :
soit vous créez un fichier vide, soit vous ne faites rien.


Comment vous faites le rm dépend de la façon dont vous voulez que les sorties non nettoyées soient gérées.

Effacer en cas de sortie propre

Les nouvelles exécutions échouent jusqu'à ce que le problème qui a provoqué la sortie non nettoyée soit résolu et que le fichier de verrouillage soit supprimé manuellement.

# acquire lock
# do work (code here may call exit, etc.)
rm "$lockfile"

Suppression sur toute sortie

Les nouvelles exécutions réussissent à condition que le script ne soit pas déjà en cours d'exécution.

trap 'rm "$lockfile"' EXIT

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X