Quel est un moyen rapide et sale de s'assurer qu'une seule instance d'un script shell est en cours d'exécution à un moment donné?
Réponses:
Voici une implémentation qui utilise un fichier de verrouillage et y fait écho un PID. Cela sert de protection si le processus est tué avant de supprimer le 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 celle kill -0
qui ne délivre aucun signal mais vérifie simplement si un processus avec le PID donné existe. L'appel à trap
garantira également que le fichier de verrouillage est supprimé même lorsque votre processus est tué (sauf kill -9
).
Utilisez flock(1)
pour faire d'un verrou à portée exclusive un descripteur de fichier. De cette façon, vous pouvez même synchroniser différentes parties du script.
#!/bin/bash
(
# Wait for lock on /var/lock/.myscript.exclusivelock (fd 200) for 10 seconds
flock -x -w 10 200 || exit 1
# Do stuff
) 200>/var/lock/.myscript.exclusivelock
Cela garantit que le code entre (
et )
n'est exécuté que par un processus à la fois et que le processus n'attend pas trop longtemps un verrou.
Attention: cette commande particulière fait partie de util-linux
. Si vous exécutez un système d'exploitation autre que Linux, il peut être disponible ou non.
( command A ) command B
invoque un sous-shell pour command A
. Documenté sur tldp.org/LDP/abs/html/subshells.html . Je ne suis toujours pas sûr du moment de l'invocation du sous-shell et de la commande B.
if flock -x -w 10 200; then ...Do stuff...; else echo "Failed to lock file" 1>&2; fi
sorte que si le délai d'attente se produit (un autre processus a le fichier verrouillé), ce script ne continue pas et ne modifie pas le fichier. Probablement ... le contre-argument est `` mais si cela a pris 10 secondes et que le verrou n'est toujours pas disponible, il ne sera jamais disponible '', probablement parce que le processus qui maintient le verrou ne se termine pas (peut-être est-il en cours d'exécution sous un débogueur?).
exit
est de la partie à l'intérieur du (
)
. Lorsque le sous-processus se termine, le verrou est automatiquement libéré, car aucun processus ne le retient.
Toutes les approches qui testent l'existence de "fichiers verrouillés" 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. À cause de ce; il existe une condition de concurrence qui rendra vos tentatives d'exclusion mutuelle interrompues.
Au lieu de cela, vous devez utiliser mkdir
. mkdir
crée un répertoire s'il n'existe pas encore, et si c'est le cas, il définit un code de sortie. Plus important encore, il fait tout cela en une seule action atomique, ce qui le rend parfait 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 souhaitez éliminer les verrous périmés, l' unité de fusion (1) est très pratique. 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 d'utilisation de l'unité de fusion:
# 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 à peu près n'importe quel boîtier UNIX), le fuser de Linux (1) offre des options supplémentaires et il y a aussi flock (1) .
if ! mkdir
partie en vérifiant si le processus avec le PID stocké (lors du démarrage réussi) à l'intérieur du lockdir est réellement en cours d'exécution et identique au script pour la protection stalenes. Cela protégerait également contre la réutilisation du PID après un redémarrage, et ne nécessiterait même pas fuser
.
mkdir
n'est pas défini comme une opération atomique et en tant que tel, «effet secondaire» est un détail d'implémentation du système de fichiers. Je le crois pleinement s'il dit que NFS ne l'implémente pas de manière atomique. Bien que je ne soupçonne pas que votre /tmp
sera un partage NFS et sera probablement fourni par un fs qui implémente mkdir
atomiquement.
ln
pour créer un lien physique à partir d'un autre fichier. Si vous avez des systèmes de fichiers étranges qui ne garantissent pas cela, vous pouvez vérifier l'inode du nouveau fichier par la suite pour voir s'il est le même que le fichier d'origine.
open(... O_CREAT|O_EXCL)
. Pour ce faire, vous avez juste besoin d'un programme utilisateur approprié, tel que lockfile-create
(in lockfile-progs
) ou dotlockfile
(in liblockfile-bin
). Et assurez-vous de nettoyer correctement (par exemple trap EXIT
), ou testez les verrous périmés (par exemple avec --use-pid
).
Il y a un wrapper autour de l'appel système flock (2) appelé, sans imagination, flock (1). Cela rend relativement facile l'obtention de verrous exclusifs de manière fiable sans se soucier du nettoyage, etc. Il y a des exemples sur la page de manuel pour savoir comment l'utiliser dans un script shell.
flock()
appel système n'est pas POSIX et ne fonctionne pas pour les fichiers sur les montages NFS.
flock -x -n %lock file% -c "%command%"
pour m'assurer qu'une seule instance est en cours d'exécution.
Vous avez besoin d'une opération atomique, comme flock, sinon cela échouera éventuellement.
Mais que faire si le troupeau n'est pas disponible. Eh bien, il y a mkdir. C'est aussi une opération atomique. Un seul processus aboutira à un mkdir réussi, tous les autres échoueront.
Le code est donc:
if mkdir /var/lock/.myscript.exclusivelock
then
# do stuff
:
rmdir /var/lock/.myscript.exclusivelock
fi
Vous devez vous occuper des verrous périmés sinon après un crash, votre script ne sera plus jamais exécuté.
sleep 10
avant rmdir
et essayez à nouveau de cascade - rien ne «fuira».
Pour rendre le verrouillage fiable, vous avez besoin d'une opération atomique. La plupart des propositions ci-dessus ne sont pas atomiques. L'utilitaire proposé lockfile (1) semble prometteur, comme la page de manuel l'a mentionné, qu'il est "résistant à NFS". Si votre OS ne prend pas en charge lockfile (1) et que votre solution doit fonctionner sur NFS, vous n'avez pas beaucoup d'options ...
NFSv2 a deux opérations atomiques:
Avec NFSv3, l'appel de création est également atomique.
Les opérations d'annuaire 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 verrous rotatifs pour les fichiers et les répertoires (en shell, pas en PHP):
verrouiller le répertoire actuel:
while ! ln -s . lock; do :; done
verrouiller un fichier:
while ! ln -s ${f} ${f}.lock; do :; done
déverrouiller le répertoire actuel (hypothèse, le processus en cours a vraiment acquis le verrou):
mv lock deleteme && rm deleteme
déverrouiller un fichier (hypothèse, le processus en cours a vraiment acquis le verrou):
mv ${f}.lock ${f}.deleteme && rm ${f}.deleteme
Remove n'est pas non plus atomique, donc d'abord le renommage (qui est atomique) puis le remove.
Pour les appels de lien symbolique et de changement de nom, les deux noms de fichiers doivent résider sur le même système de fichiers. Ma proposition: n'utilisez que des noms de fichiers simples (pas de chemins) et placez le fichier et verrouillez-le dans le même répertoire.
lockfile
si disponible, ou utilise cette symlink
méthode si ce n'est pas le cas.
mv
, rm
), devrait- rm -f
on utiliser, plutôt que rm
dans le cas où deux processus P1, P2 sont en course? Par exemple, P1 commence le déverrouillage avec mv
, puis P2 se verrouille, puis P2 se déverrouille (à la fois mv
et rm
), enfin P1 tente rm
et échoue.
$$
dans le ${f}.deleteme
nom de fichier.
Une autre option consiste à utiliser l' noclobber
option du shell en exécutant set -C
. Puis >
é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 ksh88
, mais cela fonctionne dans tous mes shells:
$ 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 qui pdksh
ajoute le O_TRUNC
drapeau, mais évidemment c'est redondant:
soit vous créez un fichier vide, soit vous ne faites rien.
La façon dont vous procédez rm
dépend de la manière dont vous voulez que les sorties sales soient gérées.
Supprimer à la sortie propre
Les nouvelles exécutions échouent jusqu'à ce que le problème qui a entraîné la sortie impure soit résolu et que le fichier de verrouillage soit supprimé manuellement.
# acquire lock
# do work (code here may call exit, etc.)
rm "$lockfile"
Supprimer à n'importe quelle 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
Vous pouvez l'utiliser GNU Parallel
car il fonctionne comme un mutex lorsqu'il est appelé en tant que sem
. Donc, concrètement, vous pouvez utiliser:
sem --id SCRIPTSINGLETON yourScript
Si vous souhaitez également un délai d'attente, utilisez:
sem --id SCRIPTSINGLETON --semaphoretimeout -10 yourScript
Timeout de <0 signifie quitter sans exécuter de script si le sémaphore n'est pas libéré dans le délai, timeout de> 0 signifie exécuter le script quand même.
Notez que vous devez lui donner un nom (avec --id
) sinon il est par défaut le terminal de contrôle.
GNU Parallel
est une installation très simple sur la plupart des plates-formes Linux / OSX / Unix - ce n'est qu'un script Perl.
sem
question connexe unix.stackexchange.com/a/322200/199525 .
Pour les scripts shell, j'ai tendance à aller avec le mkdir
dessus flock
car cela rend les verrous plus portables.
Quoi qu'il en soit, l'utilisation set -e
ne suffit pas. Cela ne quitte le script que si une commande échoue. Vos serrures seront toujours laissées pour compte.
Pour un nettoyage correct des verrous, vous devez vraiment définir vos pièges sur quelque chose comme ce code psuedo (levé, simplifié et non testé mais à partir de scripts activement utilisés):
#=======================================================================
# Predefined Global Variables
#=======================================================================
TMPDIR=/tmp/myapp
[[ ! -d $TMP_DIR ]] \
&& mkdir -p $TMP_DIR \
&& chmod 700 $TMPDIR
LOCK_DIR=$TMP_DIR/lock
#=======================================================================
# Functions
#=======================================================================
function mklock {
__lockdir="$LOCK_DIR/$(date +%s.%N).$$" # Private Global. Use Epoch.Nano.PID
# If it can create $LOCK_DIR then no other instance is running
if $(mkdir $LOCK_DIR)
then
mkdir $__lockdir # create this instance's specific lock in queue
LOCK_EXISTS=true # Global
else
echo "FATAL: Lock already exists. Another copy is running or manually lock clean up required."
exit 1001 # Or work out some sleep_while_execution_lock elsewhere
fi
}
function rmlock {
[[ ! -d $__lockdir ]] \
&& echo "WARNING: Lock is missing. $__lockdir does not exist" \
|| rmdir $__lockdir
}
#-----------------------------------------------------------------------
# Private Signal Traps Functions {{{2
#
# DANGER: SIGKILL cannot be trapped. So, try not to `kill -9 PID` or
# there will be *NO CLEAN UP*. You'll have to manually remove
# any locks in place.
#-----------------------------------------------------------------------
function __sig_exit {
# Place your clean up logic here
# Remove the LOCK
[[ -n $LOCK_EXISTS ]] && rmlock
}
function __sig_int {
echo "WARNING: SIGINT caught"
exit 1002
}
function __sig_quit {
echo "SIGQUIT caught"
exit 1003
}
function __sig_term {
echo "WARNING: SIGTERM caught"
exit 1015
}
#=======================================================================
# Main
#=======================================================================
# Set TRAPs
trap __sig_exit EXIT # SIGEXIT
trap __sig_int INT # SIGINT
trap __sig_quit QUIT # SIGQUIT
trap __sig_term TERM # SIGTERM
mklock
# CODE
exit # No need for cleanup code here being in the __sig_exit trap function
Voici ce qui va se passer. Tous les pièges produiront une sortie de sorte que la fonction __sig_exit
se produira toujours (sauf un SIGKILL) qui nettoie vos serrures.
Remarque: mes valeurs de sortie ne sont pas des valeurs faibles. Pourquoi? Divers systèmes de traitement par lots créent ou ont des attentes concernant les nombres de 0 à 31. En les définissant sur autre chose, je peux faire réagir mes scripts et mes flux de lots en conséquence au travail ou au script précédent.
rm -r $LOCK_DIR
ou même le forcer si nécessaire (comme je l'ai fait aussi dans des cas particuliers tels que la tenue de fichiers de travail relatifs). À votre santé.
exit 1002
?
Vraiment rapide et vraiment sale? Ce one-liner en haut de votre script fonctionnera:
[[ $(pgrep -c "`basename \"$0\"`") -gt 1 ]] && exit
Bien sûr, assurez-vous simplement que le nom de votre script est unique. :)
-gt 2
? grep ne se trouve pas toujours dans le résultat de ps!
pgrep
n'est pas dans POSIX. Si vous voulez que cela fonctionne de manière portable, vous avez besoin de POSIX ps
et de traiter sa sortie.
-c
n'existe pas, vous devrez utiliser | wc -l
. À propos de la comparaison des nombres: -gt 1
est vérifiée puisque la première instance se voit.
Voici une approche qui combine le verrouillage de répertoire atomique avec une vérification du verrouillage périmé via PID et redémarrer si périmé. De plus, cela ne repose sur aucun bashisme.
#!/bin/dash
SCRIPTNAME=$(basename $0)
LOCKDIR="/var/lock/${SCRIPTNAME}"
PIDFILE="${LOCKDIR}/pid"
if ! mkdir $LOCKDIR 2>/dev/null
then
# lock failed, but check for stale one by checking if the PID is really existing
PID=$(cat $PIDFILE)
if ! kill -0 $PID 2>/dev/null
then
echo "Removing stale lock of nonexistent PID ${PID}" >&2
rm -rf $LOCKDIR
echo "Restarting myself (${SCRIPTNAME})" >&2
exec "$0" "$@"
fi
echo "$SCRIPTNAME is already running, bailing out" >&2
exit 1
else
# lock successfully acquired, save PID
echo $$ > $PIDFILE
fi
trap "rm -rf ${LOCKDIR}" QUIT INT TERM EXIT
echo hello
sleep 30s
echo bye
Cet exemple est expliqué dans le man flock, mais il a besoin de quelques améliorations, car nous devons gérer les bogues et les codes de sortie:
#!/bin/bash
#set -e this is useful only for very stupid scripts because script fails when anything command exits with status more than 0 !! without possibility for capture exit codes. not all commands exits >0 are failed.
( #start subprocess
# Wait for lock on /var/lock/.myscript.exclusivelock (fd 200) for 10 seconds
flock -x -w 10 200
if [ "$?" != "0" ]; then echo Cannot lock!; exit 1; fi
echo $$>>/var/lock/.myscript.exclusivelock #for backward lockdir compatibility, notice this command is executed AFTER command bottom ) 200>/var/lock/.myscript.exclusivelock.
# Do stuff
# you can properly manage exit codes with multiple command and process algorithm.
# I suggest throw this all to external procedure than can properly handle exit X commands
) 200>/var/lock/.myscript.exclusivelock #exit subprocess
FLOCKEXIT=$? #save exitcode status
#do some finish commands
exit $FLOCKEXIT #return properly exitcode, may be usefull inside external scripts
Vous pouvez utiliser une autre méthode, lister les processus que j'ai utilisés dans le passé. Mais c'est plus compliqué que la méthode ci-dessus. Vous devez lister les processus par ps, filtrer par son nom, filtre supplémentaire grep -v grep pour supprimer le parasite et enfin le compter par grep -c. et comparer avec le nombre. C'est compliqué et incertain
Les réponses existantes publiées reposent sur l'utilitaire CLI flock
ou ne sécurisent pas correctement le fichier de verrouillage. L'utilitaire flock n'est pas disponible sur tous les systèmes non Linux (c'est-à-dire FreeBSD) et ne fonctionne pas correctement sur NFS.
Dans mes premiers jours de l' administration du système et le développement du système, on m'a dit qu'une méthode sûre et relativement portable de créer un fichier de verrouillage était de créer un fichier temporaire en utilisant mkemp(3)
oumkemp(1)
, à écrire des informations d'identification dans le fichier temporaire (c'est-à-dire PID), puis à un lien physique le fichier temporaire dans le fichier de verrouillage. Si le lien a réussi, vous avez réussi à obtenir le verrou.
Lors de l'utilisation de verrous dans des scripts shell, je place généralement une obtain_lock()
fonction dans un profil partagé, puis je la source à partir des scripts. Voici un exemple de ma fonction de verrouillage:
obtain_lock()
{
LOCK="${1}"
LOCKDIR="$(dirname "${LOCK}")"
LOCKFILE="$(basename "${LOCK}")"
# create temp lock file
TMPLOCK=$(mktemp -p "${LOCKDIR}" "${LOCKFILE}XXXXXX" 2> /dev/null)
if test "x${TMPLOCK}" == "x";then
echo "unable to create temporary file with mktemp" 1>&2
return 1
fi
echo "$$" > "${TMPLOCK}"
# attempt to obtain lock file
ln "${TMPLOCK}" "${LOCK}" 2> /dev/null
if test $? -ne 0;then
rm -f "${TMPLOCK}"
echo "unable to obtain lockfile" 1>&2
if test -f "${LOCK}";then
echo "current lock information held by: $(cat "${LOCK}")" 1>&2
fi
return 2
fi
rm -f "${TMPLOCK}"
return 0;
};
Voici un exemple d'utilisation de la fonction de verrouillage:
#!/bin/sh
. /path/to/locking/profile.sh
PROG_LOCKFILE="/tmp/myprog.lock"
clean_up()
{
rm -f "${PROG_LOCKFILE}"
}
obtain_lock "${PROG_LOCKFILE}"
if test $? -ne 0;then
exit 1
fi
trap clean_up SIGHUP SIGINT SIGTERM
# bulk of script
clean_up
exit 0
# end of script
N'oubliez pas d'appeler clean_up
à tous les points de sortie de votre script.
J'ai utilisé ce qui précède dans les environnements Linux et FreeBSD.
Lorsque je cible une machine Debian, je trouve que le lockfile-progs
paquet est une bonne solution. procmail
est également livré avec un lockfile
outil. Cependant, parfois je suis coincé avec aucun de ces derniers.
Voici ma solution qui utilise mkdir
pour atomic-ness et un fichier PID pour détecter les verrous périmés. Ce code est actuellement en production sur une configuration Cygwin et fonctionne bien.
Pour l'utiliser, appelez simplement exclusive_lock_require
lorsque vous avez besoin d'un accès exclusif à quelque chose. Un paramètre de nom de verrou facultatif vous permet de partager des verrous entre différents scripts. Il existe également deux fonctions de niveau inférieur ( exclusive_lock_try
et exclusive_lock_retry
) si vous avez besoin de quelque chose de plus complexe.
function exclusive_lock_try() # [lockname]
{
local LOCK_NAME="${1:-`basename $0`}"
LOCK_DIR="/tmp/.${LOCK_NAME}.lock"
local LOCK_PID_FILE="${LOCK_DIR}/${LOCK_NAME}.pid"
if [ -e "$LOCK_DIR" ]
then
local LOCK_PID="`cat "$LOCK_PID_FILE" 2> /dev/null`"
if [ ! -z "$LOCK_PID" ] && kill -0 "$LOCK_PID" 2> /dev/null
then
# locked by non-dead process
echo "\"$LOCK_NAME\" lock currently held by PID $LOCK_PID"
return 1
else
# orphaned lock, take it over
( echo $$ > "$LOCK_PID_FILE" ) 2> /dev/null && local LOCK_PID="$$"
fi
fi
if [ "`trap -p EXIT`" != "" ]
then
# already have an EXIT trap
echo "Cannot get lock, already have an EXIT trap"
return 1
fi
if [ "$LOCK_PID" != "$$" ] &&
! ( umask 077 && mkdir "$LOCK_DIR" && umask 177 && echo $$ > "$LOCK_PID_FILE" ) 2> /dev/null
then
local LOCK_PID="`cat "$LOCK_PID_FILE" 2> /dev/null`"
# unable to acquire lock, new process got in first
echo "\"$LOCK_NAME\" lock currently held by PID $LOCK_PID"
return 1
fi
trap "/bin/rm -rf \"$LOCK_DIR\"; exit;" EXIT
return 0 # got lock
}
function exclusive_lock_retry() # [lockname] [retries] [delay]
{
local LOCK_NAME="$1"
local MAX_TRIES="${2:-5}"
local DELAY="${3:-2}"
local TRIES=0
local LOCK_RETVAL
while [ "$TRIES" -lt "$MAX_TRIES" ]
do
if [ "$TRIES" -gt 0 ]
then
sleep "$DELAY"
fi
local TRIES=$(( $TRIES + 1 ))
if [ "$TRIES" -lt "$MAX_TRIES" ]
then
exclusive_lock_try "$LOCK_NAME" > /dev/null
else
exclusive_lock_try "$LOCK_NAME"
fi
LOCK_RETVAL="${PIPESTATUS[0]}"
if [ "$LOCK_RETVAL" -eq 0 ]
then
return 0
fi
done
return "$LOCK_RETVAL"
}
function exclusive_lock_require() # [lockname] [retries] [delay]
{
if ! exclusive_lock_retry "$@"
then
exit 1
fi
}
Si les limitations de flock, qui ont déjà été décrites ailleurs sur ce fil, ne sont pas un problème pour vous, alors cela devrait fonctionner:
#!/bin/bash
{
# exit if we are unable to obtain a lock; this would happen if
# the script is already running elsewhere
# note: -x (exclusive) is the default
flock -n 100 || exit
# put commands to run here
sleep 100
} 100>/tmp/myjob.lock
-n
fera exit 1
immédiatement s'il ne peut pas obtenir la serrure
Certains unix ont lockfile
ce qui est très similaire à celui déjà mentionné flock
.
Depuis la page de manuel:
lockfile peut être utilisé pour créer un ou plusieurs fichiers de sémaphore. Si lock-file ne peut pas créer tous les fichiers spécifiés (dans l'ordre spécifié), il attend le temps d'arrêt (par défaut à 8) secondes et réessaye le dernier fichier qui n'a pas réussi. Vous pouvez spécifier le nombre de tentatives à effectuer jusqu'à ce que l'échec soit renvoyé. Si le nombre de tentatives est -1 (par défaut, c'est-à-dire -r-1), lockfile réessaiera indéfiniment.
lockfile
utilitaire ??
lockfile
est distribué avec procmail
. Il existe également une alternative dotlockfile
qui va avec le liblockfile
package. Ils prétendent tous deux travailler de manière fiable sur NFS.
En fait, bien que la réponse de bmdhacks soit presque bonne, il y a une légère chance que le second script s'exécute après avoir vérifié le fichier de verrouillage et avant de l'écrire. Ainsi, ils écriront tous les deux le fichier de verrouillage et ils fonctionneront tous les deux. Voici comment le faire fonctionner à coup sûr:
lockfile=/var/lock/myscript.lock
if ( set -o noclobber; echo "$$" > "$lockfile") 2> /dev/null ; then
trap 'rm -f "$lockfile"; exit $?' INT TERM EXIT
else
# or you can decide to skip the "else" part if you want
echo "Another instance is already running!"
fi
le noclobber
option s'assurera que la commande de redirection échouera si le fichier existe déjà. Donc, la commande de redirection est en fait atomique - vous écrivez et vérifiez le fichier avec une seule commande. Vous n'avez pas besoin de supprimer le fichier de verrouillage à la fin du fichier - il sera supprimé par le piège. J'espère que cela aidera les gens qui le liront plus tard.
PS Je n'ai pas vu que Mikel avait déjà répondu correctement à la question, bien qu'il n'ait pas inclus la commande trap pour réduire le risque de laisser le fichier de verrouillage après l'arrêt du script avec Ctrl-C par exemple. C'est donc la solution complète
J'utilise une approche simple qui gère les fichiers de verrouillage périmés.
Notez que certaines des solutions ci-dessus qui stockent le pid ignorent le fait que le pid peut s'enrouler. Donc - il ne suffit pas de vérifier s'il existe un processus valide avec le pid stocké, en particulier pour les scripts à exécution longue.
J'utilise noclobber pour m'assurer qu'un seul script peut ouvrir et écrire dans le fichier de verrouillage à la fois. De plus, je stocke suffisamment d'informations pour identifier de manière unique un processus dans le fichier de verrouillage. Je définis l'ensemble de données pour identifier de manière unique un processus comme étant pid, ppid, lstart.
Lorsqu'un nouveau script démarre, s'il ne parvient pas à créer le fichier de verrouillage, il vérifie alors que le processus qui a créé le fichier de verrouillage est toujours là. Sinon, nous supposons que le processus d'origine est mort d'une mort honteuse et a laissé un fichier de verrouillage périmé. Le nouveau script prend alors possession du fichier de verrouillage, et tout est bien le monde, encore une fois.
Devrait fonctionner avec plusieurs shells sur plusieurs plates-formes. Rapide, portable et simple.
#!/usr/bin/env sh
# Author: rouble
LOCKFILE=/var/tmp/lockfile #customize this line
trap release INT TERM EXIT
# Creates a lockfile. Sets global variable $ACQUIRED to true on success.
#
# Returns 0 if it is successfully able to create lockfile.
acquire () {
set -C #Shell noclobber option. If file exists, > will fail.
UUID=`ps -eo pid,ppid,lstart $$ | tail -1`
if (echo "$UUID" > "$LOCKFILE") 2>/dev/null; then
ACQUIRED="TRUE"
return 0
else
if [ -e $LOCKFILE ]; then
# We may be dealing with a stale lock file.
# Bring out the magnifying glass.
CURRENT_UUID_FROM_LOCKFILE=`cat $LOCKFILE`
CURRENT_PID_FROM_LOCKFILE=`cat $LOCKFILE | cut -f 1 -d " "`
CURRENT_UUID_FROM_PS=`ps -eo pid,ppid,lstart $CURRENT_PID_FROM_LOCKFILE | tail -1`
if [ "$CURRENT_UUID_FROM_LOCKFILE" == "$CURRENT_UUID_FROM_PS" ]; then
echo "Script already running with following identification: $CURRENT_UUID_FROM_LOCKFILE" >&2
return 1
else
# The process that created this lock file died an ungraceful death.
# Take ownership of the lock file.
echo "The process $CURRENT_UUID_FROM_LOCKFILE is no longer around. Taking ownership of $LOCKFILE"
release "FORCE"
if (echo "$UUID" > "$LOCKFILE") 2>/dev/null; then
ACQUIRED="TRUE"
return 0
else
echo "Cannot write to $LOCKFILE. Error." >&2
return 1
fi
fi
else
echo "Do you have write permissons to $LOCKFILE ?" >&2
return 1
fi
fi
}
# Removes the lock file only if this script created it ($ACQUIRED is set),
# OR, if we are removing a stale lock file (first parameter is "FORCE")
release () {
#Destroy lock file. Take no prisoners.
if [ "$ACQUIRED" ] || [ "$1" == "FORCE" ]; then
rm -f $LOCKFILE
fi
}
# Test code
# int main( int argc, const char* argv[] )
echo "Acquring lock."
acquire
if [ $? -eq 0 ]; then
echo "Acquired lock."
read -p "Press [Enter] key to release lock..."
release
echo "Released lock."
else
echo "Unable to acquire lock."
fi
Je voulais supprimer les fichiers de verrouillage, les lockdirs, les programmes de verrouillage spéciaux et même pidof
parce qu'ils ne se trouvent pas sur toutes les installations Linux. Je voulais également avoir le code le plus simple possible (ou au moins le moins de lignes possible). Déclaration la plus simple if
, en une seule ligne:
if [[ $(ps axf | awk -v pid=$$ '$1!=pid && $6~/'$(basename $0)'/{print $1}') ]]; then echo "Already running"; exit; fi
/bin/ps -a --format pid,cmd | awk -v pid=$$ '/'$(basename $0)'/ { if ($1!=pid) print $1; }'
Ajoutez cette ligne au début de votre script
[ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en "$0" "$0" "$@" || :
C'est un code standard de Man Flock.
Si vous voulez plus de journalisation, utilisez celui-ci
[ "${FLOCKER}" != "$0" ] && { echo "Trying to start build from queue... "; exec bash -c "FLOCKER='$0' flock -E $E_LOCKED -en '$0' '$0' '$@' || if [ \"\$?\" -eq $E_LOCKED ]; then echo 'Locked.'; fi"; } || echo "Lock is free. Completing."
Cela définit et vérifie les verrous en utilisant flock
utilitaire. Ce code détecte s'il a été exécuté pour la première fois en vérifiant la variable FLOCKER, s'il n'est pas défini sur le nom du script, puis il essaie de redémarrer le script de manière récursive en utilisant flock et avec la variable FLOCKER initialisée, si FLOCKER est correctement défini, puis flock à l'itération précédente réussi et vous pouvez continuer. Si le verrou est occupé, il échoue avec un code de sortie configurable.
Il semble ne pas fonctionner sur Debian 7, mais semble fonctionner à nouveau avec le paquet expérimental util-linux 2.25. Il écrit "flock: ... Fichier texte occupé". Il peut être remplacé en désactivant l'autorisation d'écriture sur votre script.
Les fichiers PID et lockfiles sont certainement les plus fiables. Lorsque vous essayez d'exécuter le programme, il peut vérifier le fichier de verrouillage qui et s'il existe, il peut utiliser ps
pour voir si le processus est toujours en cours d'exécution. Si ce n'est pas le cas, le script peut démarrer et mettre à jour le PID du fichier de verrouillage vers le sien.
Je trouve que la solution de bmdhack est la plus pratique, du moins pour mon cas d'utilisation. L'utilisation de flock et lockfile repose sur la suppression du lockfile en utilisant rm lorsque le script se termine, ce qui ne peut pas toujours être garanti (par exemple, kill -9).
Je changerais une chose mineure à propos de la solution de bmdhack: cela met un point d'honneur à supprimer le fichier de verrouillage, sans déclarer que cela n'est pas nécessaire pour le fonctionnement en toute sécurité de ce sémaphore. Son utilisation de kill -0 garantit qu'un ancien fichier de verrouillage pour un processus mort sera simplement ignoré / écrasé.
Ma solution simplifiée est donc d'ajouter simplement ce qui suit en haut de votre singleton:
## Test the lock
LOCKFILE=/tmp/singleton.lock
if [ -e ${LOCKFILE} ] && kill -0 `cat ${LOCKFILE}`; then
echo "Script already running. bye!"
exit
fi
## Set the lock
echo $$ > ${LOCKFILE}
Bien sûr, ce script a toujours le défaut que les processus susceptibles de démarrer en même temps présentent un risque de course, car le test de verrouillage et les opérations de positionnement ne sont pas une seule action atomique. Mais la solution proposée pour cela par lhunath d'utiliser mkdir a le défaut qu'un script tué peut laisser derrière le répertoire, empêchant ainsi d'autres instances de s'exécuter.
L' utilitaire sémaphorique utilise flock
(comme discuté ci-dessus, par exemple par presto8) pour implémenter un sémaphore de comptage . Il active n'importe quel nombre spécifique de processus simultanés que vous souhaitez. Nous l'utilisons pour limiter le niveau de concurrence de divers processus de travail de file d'attente.
C'est comme sem mais beaucoup plus léger. (Divulgation complète: je l'ai écrit après avoir trouvé que le sem était beaucoup trop lourd pour nos besoins et qu'il n'y avait pas d'utilitaire de comptage simple de sémaphore disponible.)
Un exemple avec flock (1) mais sans sous-coque. flock () ed file / tmp / foo n'est jamais supprimé, mais cela n'a pas d'importance car il obtient flock () et un-flock () ed.
#!/bin/bash
exec 9<> /tmp/foo
flock -n 9
RET=$?
if [[ $RET -ne 0 ]] ; then
echo "lock failed, exiting"
exit
fi
#Now we are inside the "critical section"
echo "inside lock"
sleep 5
exec 9>&- #close fd 9, and release lock
#The part below is outside the critical section (the lock)
echo "lock released"
sleep 5
Déjà répondu un million de fois, mais d'une autre manière, sans avoir besoin de dépendances externes:
LOCK_FILE="/var/lock/$(basename "$0").pid"
trap "rm -f ${LOCK_FILE}; exit" INT TERM EXIT
if [[ -f $LOCK_FILE && -d /proc/`cat $LOCK_FILE` ]]; then
// Process already exists
exit 1
fi
echo $$ > $LOCK_FILE
Chaque fois qu'il écrit le PID actuel ($$) dans le fichier de verrouillage et au démarrage du script, il vérifie si un processus est en cours d'exécution avec le dernier PID.
L'utilisation du verrou du processus est beaucoup plus forte et prend également en charge les sorties peu gracieuses. lock_file reste ouvert tant que le processus est en cours d'exécution. Il sera fermé (par shell) une fois que le processus existe (même s'il est tué). J'ai trouvé cela très efficace:
lock_file=/tmp/`basename $0`.lock
if fuser $lock_file > /dev/null 2>&1; then
echo "WARNING: Other instance of $(basename $0) running."
exit 1
fi
exec 3> $lock_file
J'utilise oneliner @ au tout début du script:
#!/bin/bash
if [[ $(pgrep -afc "$(basename "$0")") -gt "1" ]]; then echo "Another instance of "$0" has already been started!" && exit; fi
.
the_beginning_of_actual_script
Il est bon de voir la présence du processus dans la mémoire (quel que soit l'état du processus); mais il fait le travail pour moi.
Le chemin du troupeau est la voie à suivre. Pensez à ce qui se passe lorsque le script meurt soudainement. Dans le cas du troupeau, vous perdez simplement le troupeau, mais ce n'est pas un problème. Notez également qu'une mauvaise astuce est de prendre un troupeau sur le script lui-même ... mais cela vous permet bien sûr de vous lancer à plein régime dans les problèmes de permission.
Rapide et sale?
#!/bin/sh
if [ -f sometempfile ]
echo "Already running... will now terminate."
exit
else
touch sometempfile
fi
..do what you want here..
rm sometempfile