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é ?
Réponses
Trop de publicités?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).
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
).
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.
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.
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
- Réponses précédentes
- Plus de réponses