Corriger le verrouillage dans les scripts shell?


66

Parfois, vous devez vous assurer qu'une seule instance d'un script shell est en cours d'exécution en même temps.

Par exemple, un travail cron exécuté via crond ne fournit pas de verrouillage (par exemple, le crond Solaris par défaut).

Un modèle commun pour implémenter le verrouillage est le code suivant:

#!/bin/sh
LOCK=/var/tmp/mylock
if [ -f $LOCK ]; then            # 'test' -> race begin
  echo Job is already running\!
  exit 6
fi
touch $LOCK                      # 'set'  -> race end
# do some work
rm $LOCK

Bien sûr, un tel code a une condition de concurrence. Il existe une fenêtre temporelle dans laquelle l'exécution de deux instances peut avancer après la ligne 3 avant que l'une d'elles puisse toucher le $LOCKfichier.

Pour un travail cron, cela ne pose généralement pas de problème, car vous disposez d'un intervalle de minutes entre deux invocations.

Mais des problèmes peuvent survenir - par exemple, lorsque le fichier de verrouillage est sur un serveur NFS -, cela se bloque. Dans ce cas, plusieurs tâches cron peuvent bloquer sur la ligne 3 et se mettre en file d'attente. Si le serveur NFS est à nouveau actif, vous avez un troupeau de travaux en parallèle.

En cherchant sur le Web, j'ai trouvé l'outil lockrun qui semble être une bonne solution à ce problème. Avec lui, vous exécutez un script qui nécessite un verrouillage comme ceci:

$ lockrun --lockfile=/var/tmp/mylock myscript.sh

Vous pouvez le mettre dans un wrapper ou l'utiliser depuis votre crontab.

Il utilise lockf()(POSIX) s'il est disponible et revient à flock()(BSD). Et le lockf()support sur NFS devrait être relativement répandu.

Y a-t-il des alternatives à lockrun?

Qu'en est-il des autres démons cron? Y a-t-il des clients communs qui soutiennent le verrouillage de manière saine? Un rapide coup d’œil dans la page de manuel de Vixie Crond (par défaut sur les systèmes Debian / Ubuntu) n’affiche rien sur le verrouillage.

Serait-ce une bonne idée d'inclure un outil comme lockrundans coreutils ?

À mon avis, il met en œuvre un thème très similaire à timeout, niceet amis.


4
Tangentiellement, et pour le bénéfice d’autres personnes qui pourraient considérer votre modèle initial Good Enough (tm), ce code de shell devrait éventuellement piéger TERM afin de supprimer son fichier de verrouillage une fois killédité; et il semble être une bonne pratique de stocker son propre pid dans le fichier verrou plutôt que de le toucher.
Ulrich Schwarz


@Shawn, pas vraiment, ne mentionne pas crond et NFS.
maxschlepzig


1
@ Ulrich très tardivement, le stockage d'un PID dans un fichier de verrouillage NFS n'ajoute que très peu de valeur. Même l'ajout du nom d'hôte n'aide toujours pas à vérifier le processus en direct
roaima

Réponses:


45

Voici un autre moyen de verrouiller un script shell afin d’empêcher la situation de concurrence critique décrite ci-dessus, selon laquelle deux tâches peuvent toutes deux passer à la ligne 3. L’ noclobberoption fonctionnera dans ksh et bash. Ne pas utiliser set noclobbercar vous ne devriez pas utiliser de script dans csh / tcsh. ;)

lockfile=/var/tmp/mylock

if ( set -o noclobber; echo "$$" > "$lockfile") 2> /dev/null; then

        trap 'rm -f "$lockfile"; exit $?' INT TERM EXIT

        # do stuff here

        # clean up after yourself, and release your trap
        rm -f "$lockfile"
        trap - INT TERM EXIT
else
        echo "Lock Exists: $lockfile owned by $(cat $lockfile)"
fi

YMMV avec verrouillage sur NFS (vous savez, lorsque les serveurs NFS ne sont pas accessibles), mais en général, il est beaucoup plus robuste que par le passé. (Il ya 10 ans)

Si vous avez des tâches cron qui font la même chose au même moment, à partir de plusieurs serveurs, mais que vous n'avez besoin que d'une seule instance pour être exécutée, une tâche comme celle-ci pourrait fonctionner pour vous.

Je n'ai aucune expérience de lockrun, mais avoir un environnement de verrouillage prédéfini avant le script en cours d'exécution peut aider. Ou peut-être pas. Vous définissez simplement le test du fichier lock en dehors de votre script dans un wrapper et, théoriquement, vous ne pourriez pas simplement atteindre la même situation de concurrence critique si deux tâches étaient appelées par lockrun exactement au même moment, comme avec le la solution du script?

De toute façon, le verrouillage de fichier est en quelque sorte un respect du comportement du système, et tous les scripts qui ne vérifient pas l'existence du fichier verrou avant son exécution feront tout ce qu'ils vont faire. En mettant en place le test du fichier verrou et un comportement correct, vous résoudrez 99% des problèmes potentiels, voire 100%.

Si vous rencontrez beaucoup de problèmes de situation de fichiers verrouillés, cela peut être le signe d'un problème plus important, comme le fait que votre travail ne soit pas correctement chronométré, ou si l'intervalle n'est pas aussi important que le travail terminé, peut-être que votre travail est mieux adapté pour être démonisé. .


EDIT BELOW - 2016-05-06 (si vous utilisez KSH88)


Sur la base du commentaire de @Clint Pachl ci-dessous, si vous utilisez ksh88, utilisez mkdirplutôt que noclobber. Cela atténue généralement une éventuelle condition de concurrence, mais ne la limite pas totalement (même si le risque est minime). Pour plus d'informations, lisez le lien que Clint a posté ci-dessous .

lockdir=/var/tmp/mylock
pidfile=/var/tmp/mylock/pid

if ( mkdir ${lockdir} ) 2> /dev/null; then
        echo $$ > $pidfile
        trap 'rm -rf "$lockdir"; exit $?' INT TERM EXIT
        # do stuff here

        # clean up after yourself, and release your trap
        rm -rf "$lockdir"
        trap - INT TERM EXIT
else
        echo "Lock Exists: $lockdir owned by $(cat $pidfile)"
fi

De plus, si vous avez besoin de créer des fichiers tmp dans votre script, vous pouvez utiliser le lockdirrépertoire pour eux, sachant qu'ils seront nettoyés à la fin du script.

Pour les bashs plus modernes, la méthode noclobber au sommet devrait convenir.


1
Non, avec lockrun vous n'avez pas de problème - lorsqu'un serveur NFS se bloque, tous les appels lockrun seront bloqués (au moins) dans l' lockf()appel système - quand il est sauvegardé, tous les processus sont repris mais un seul processus remporte le verrouillage. Aucune condition de concurrence. Je ne rencontre pas souvent de tels problèmes avec les tâches cron, le contraire est vrai, mais c'est un problème quand il vous frappe, il peut potentiellement causer beaucoup de douleur.
maxschlepzig

1
J'ai accepté cette réponse parce que la méthode est sûre et jusqu'à présent la plus élégante. Je suggère une petite variante: set -o noclobber && echo "$$" > "$lockfile"obtenir un repli sûr lorsque le shell ne supporte pas l'option noclobber.
maxschlepzig

3
Bonne réponse, mais vous devriez également "tuer -0" la valeur dans lockfile pour vous assurer que le processus qui a créé le verrou existe toujours.
Nigel Horne

1
L' noclobberoption peut être sujette à des conditions de concurrence. Voir mywiki.wooledge.org/BashFAQ/045 pour des pistes de réflexion.
Clint Pachl le

2
Remarque: utiliser noclobber(ou -C) dans ksh88 ne fonctionne pas car ksh88 n’utilise pas O_EXCLpour noclobber. Si vous utilisez un nouveau shell
ira

14

Je préfère utiliser des liens durs.

lockfile=/var/lock/mylock
tmpfile=${lockfile}.$$
echo $$ > $tmpfile
if ln $tmpfile $lockfile 2>&-; then
    echo locked
else
    echo locked by $(<$lockfile)
    rm $tmpfile
    exit
fi
trap "rm ${tmpfile} ${lockfile}" 0 1 2 3 15
# do what you need to

Les liens physiques sont atomiques par rapport à NFS et, pour la plupart, mkdir l’est également . Utiliser mkdir(2)ou link(2)sont à peu près les mêmes, à un niveau pratique; Je préfère simplement utiliser des liens durs, car davantage d'implémentations de NFS permettaient des liens durs atomiques que des liaisons atomiques mkdir. Avec les versions modernes de NFS, vous ne devriez plus avoir à vous soucier de cela.


12

Je comprends que mkdirc'est atomique, alors peut-être:

lockdir=/var/tmp/myapp
if mkdir $lockdir; then
  # this is a new instance, store the pid
  echo $$ > $lockdir/PID
else
  echo Job is already running, pid $(<$lockdir/PID) >&2
  exit 6
fi

# then set traps to cleanup upon script termination 
# ref http://www.shelldorado.com/goodcoding/tempfiles.html
trap 'rm -r "$lockdir" >/dev/null 2>&1' 0
trap "exit 2" 1 2 3 13 15

Ok, mais je n'ai pas pu trouver d'informations mkdir()sur si NFS (> = 3) est normalisé pour être atomique.
maxschlepzig

2
@maxschlepzig La RFC 1813 n'appelle pas explicitement pour mkdirêtre atomique (elle le fait pour rename). En pratique, on sait que certaines implémentations ne le sont pas. En relation: un fil intéressant, comprenant une contribution de l'auteur de GNU arch .
Gilles 'SO- arrête d'être méchant'

8

Un moyen facile consiste à utiliser lockfilegénéralement le procmailpaquet.

LOCKFILE="/tmp/mylockfile.lock"
# try once to get the lock else exit
lockfile -r 0 "$LOCKFILE" || exit 0

# here the actual job

rm -f "$LOCKFILE"

5

semce qui fait partie des paralleloutils GNU peut être ce que vous cherchez:

sem [--fg] [--id <id>] [--semaphoretimeout <secs>] [-j <num>] [--wait] command

Un péché:

sem --id my_semaphore --fg "echo 1 ; date ; sleep 3" &
sem --id my_semaphore --fg "echo 2 ; date ; sleep 3" &
sem --id my_semaphore --fg "echo 3 ; date ; sleep 3" &

sortie:

1
Thu 10 Nov 00:26:21 UTC 2016
2
Thu 10 Nov 00:26:24 UTC 2016
3
Thu 10 Nov 00:26:28 UTC 2016

Notez que l'ordre n'est pas garanti. De plus, la sortie n'est affichée qu'une fois terminée (irritant!). Mais même dans ce cas, c’est le moyen le plus concis que je connaisse pour se protéger des exécutions simultanées, sans se soucier des fichiers verrouillés, des tentatives et du nettoyage.


Le verrouillage offert par la sempoignée est-il abattu à mi-exécution?
Roaima

2

Je l'utilise dtach.

$ dtach -n /tmp/socket long_running_task ; echo $?
0
$ dtach -n /tmp/socket long_running_task ; echo $?
dtach: /tmp/socket: Address already in use
1

1

J'utilise l'outil de ligne de commande "flock" pour gérer les verrous dans mes scripts bash, comme décrit ici et ici . J'ai utilisé cette méthode simple depuis la page de manuel flock, pour exécuter certaines commandes dans un sous-shell ...

   (
     flock -n 9
     # ... commands executed under lock ...
   ) 9>/var/lock/mylockfile

Dans cet exemple, il échoue avec le code de sortie de 1 s'il ne peut pas acquérir le fichier de verrouillage. Mais flock peut également être utilisé de manière à ne pas nécessiter que des commandes soient exécutées dans un sous-shell :-)


3
L' flock()appel système ne fonctionne pas sur NFS .
maxschlepzig

1
BSD a un outil similaire, "lockf".
dubiousjim

2
@dubiousjim, BSD lockf appelle également flock()et est donc problématique sur NFS. Btw, dans l'intervalle, flock () sous Linux revient maintenant à fcntl()lorsque le fichier est situé sur un montage NFS. Par conséquent, dans un environnement NFS uniquement Linux, fonctionne flock()désormais sur NFS.
maxschlepzig

1

N'utilisez pas de fichier.

Si votre script est exécuté comme ceci, par exemple:

bash my_script

Vous pouvez détecter si cela fonctionne en utilisant:

running_proc=$(ps -C bash -o pid=,cmd= | grep my_script);
if [[ "$running_proc" != "$$ bash my_script" ]]; do 
  echo Already locked
  exit 6
fi

Hm, le code de vérification ps fonctionne de l'intérieur my_script? Dans le cas où une autre instance est en cours d'exécution - ne running_proccontient pas deux lignes correspondantes? J'aime l'idée, mais bien sûr, vous obtiendrez de faux résultats lorsqu'un autre utilisateur exécute un script du même nom ...
maxschlepzig

3
Il inclut également une condition de concurrence critique: si 2 instances exécutent la première ligne en parallèle, aucune ne reçoit le «verrou» et les deux sortent avec le statut 6. Il s'agirait d'une sorte de famine mutuelle à un tour . Au fait, je ne sais pas pourquoi vous utilisez $!plutôt que $$dans votre exemple.
maxschlepzig

@maxschlepzig en effet désolé pour le $ incorrect! $$
frogstarr78

@maxschlepzig pour gérer plusieurs utilisateurs exécutant le script, ajoutez euser = à l'argument -o.
Frogstarr78

@maxschlepzig Pour éviter les lignes multiples, vous pouvez également modifier les arguments en grep, ou des "filtres" supplémentaires (par exemple grep -v $$). Fondamentalement, j'essayais de proposer une approche différente du problème.
Frogstarr78

1

Pour une utilisation réelle, vous devez utiliser la réponse la plus votée .

Cependant, je souhaite aborder diverses approches brisées et semi-exploitables, psainsi que leurs nombreuses mises en garde, car je constate que de nombreuses personnes les utilisent.

Cette réponse est vraiment la réponse à "Pourquoi ne pas utiliser pset grepgérer le verrouillage dans la coque?"

Approche brisée # 1

Premièrement, une approche donnée dans une autre réponse qui a eu quelques votes positifs malgré le fait que cela ne fonctionnait pas (et ne pourrait jamais) et n’avait clairement jamais été testée:

running_proc=$(ps -C bash -o pid=,cmd= | grep my_script);
if [[ "$running_proc" != "$$ bash my_script" ]]; do 
  echo Already locked
  exit 6
fi

Corrigeons les erreurs de syntaxe et les psarguments cassés et obtenons:

running_proc=$(ps -C bash -o pid,cmd | grep "$0");
echo "$running_proc"
if [[ "$running_proc" != "$$ bash $0" ]]; then
  echo Already locked
  exit 6
fi

Ce script quittera toujours 6 à chaque fois, quelle que soit la manière dont vous l'exécutez.

Si vous l'exécutez avec ./myscript, la pssortie sera simplement 12345 -bash, ce qui ne correspond pas à la chaîne requise 12345 bash ./myscript, donc cela échouera.

Si vous le lancez avec bash myscript, les choses deviennent plus intéressantes. Le processus bash fourches pour exécuter le pipeline, et l' enfant shell court le pset grep. Le shell original et le shell enfant apparaîtront dans la pssortie, comme ceci:

25793 bash myscript
25795 bash myscript

Ce n'est pas la sortie attendue $$ bash $0, votre script va donc se fermer.

Approche brisée # 2

Maintenant, en toute justice pour l'utilisateur qui a écrit l'approche cassée n ° 1, j'ai fait quelque chose de similaire moi-même lorsque j'ai essayé pour la première fois:

if otherpids="$(pgrep -f "$0" | grep -vFx "$$")" ; then
  echo >&2 "There are other copies of the script running; exiting."
  ps >&2 -fq "${otherpids//$'\n'/ }" # -q takes about a tenth the time as -p
  exit 1
fi

Cela fonctionne presque . Mais le fait de forcer pour faire fonctionner le tuyau jette cela au large. Donc, celui-ci sortira toujours, aussi.

Approche peu fiable n ° 3

pids_this_script="$(pgrep -f "$0")"
if not_this_process="$(echo "$pids_this_script" | grep -vFx "$$")"; then
  echo >&2 "There are other copies of this script running; exiting."
  ps -fq "${not_this_process//$'\n'/ }"
  exit 1
fi

Cette version évite le problème de forçage de pipeline dans l'approche n ° 2 en obtenant d'abord tous les PID qui ont le script actuel dans leurs arguments de ligne de commande, puis en filtrant cette pidlist, séparément, pour omettre le PID du script actuel.

Cela pourrait fonctionner ... à condition qu'aucun autre processus n'ait une ligne de commande correspondant à l'élément $0et que le script soit toujours appelé de la même manière (par exemple, s'il est appelé avec un chemin relatif puis un chemin absolu, la dernière instance ne remarquera pas le premier. )

Approche peu fiable n ° 4

Alors, que se passe-t-il si nous omettons de vérifier la ligne de commande complète, car cela n’indiquera peut-être pas un script en cours d’exécution, et lsofrechercherons à la place tous les processus pour lesquels ce script est ouvert?

Eh bien, oui, cette approche n’est pas si mauvaise en réalité:

if otherpids="$(lsof -t "$0" | grep -vFx "$$")"; then
  echo >&2 "Error: There are other processes that have this script open - most likely other copies of the script running.  Exiting to avoid conflicts."
  ps >&2 -fq "${otherpids//$'\n'/ }"
  exit 1
fi

Bien sûr, si une copie du script est en cours d'exécution, la nouvelle instance démarrera correctement et vous aurez deux copies en cours d'exécution.

Ou si le script en cours d'exécution est modifié (par exemple avec Vim ou avec a git checkout), la "nouvelle" version du script démarrera sans problème, car Vim et le git checkoutrésultat, un nouveau fichier (un nouvel inode) à la place du fichier le vieux.

Cependant, si le script n'est jamais modifié ni copié, cette version est plutôt bonne. Il n'y a pas de condition de concurrence, car le fichier de script doit déjà être ouvert avant que le contrôle puisse être atteint.

Il peut toujours y avoir des faux positifs si le fichier de script est ouvert par un autre processus, mais notez que même s'il est ouvert à l'édition dans Vim, vim ne tient pas le fichier de script ouvert et ne génère pas de faux positifs.

Mais rappelez-vous, n'utilisez pas cette approche si le script peut être édité ou copié car vous obtiendrez de faux négatifs, c'est-à-dire plusieurs instances s'exécutant en même temps. Le fait que l'édition avec Vim ne génère pas de faux positifs ne devrait donc pas avoir d'importance. à toi. Je le mentionne cependant car l'approche n ° 3 ne donne de faux positifs (c. -à- refuse de démarrer) si vous avez le script ouvert avec Vim.

Alors que faire, alors?

La réponse la plus votée à cette question donne une bonne approche solide.

Peut-être pourriez-vous en écrire un meilleur ... mais si vous ne comprenez pas tous les problèmes et toutes les mises en garde concernant toutes les approches ci-dessus, il est peu probable que vous écriviez une méthode de verrouillage qui les évite toutes.



0

Voici quelque chose que j'ajoute parfois sur un serveur pour gérer facilement les conditions de concurrence pour tous les travaux de la machine. Il en va de même pour le message de Tim Kennedy, mais vous obtenez ainsi le traitement de la course en ajoutant seulement une ligne à chaque script bash qui en a besoin.

Placez le contenu ci-dessous dans / opt / racechecker / racechecker, par exemple:

ZPROGRAMNAME=$(readlink -f $0)
EZPROGRAMNAME=`echo $ZPROGRAMNAME | sed 's/\//_/g'`
EZMAIL="/usr/bin/mail"
EZCAT="/bin/cat"

if  [ -n "$EZPROGRAMNAME" ] ;then
        EZPIDFILE=/tmp/$EZPROGRAMNAME.pid
        if [ -e "$EZPIDFILE" ] ;then
                EZPID=$($EZCAT $EZPIDFILE)
                echo "" | $EZMAIL -s "$ZPROGRAMNAME already running with pid $EZPID"  alarms@someemail.com >>/dev/null
                exit -1
        fi
        echo $$ >> $EZPIDFILE
        function finish {
          rm  $EZPIDFILE
        }
        trap finish EXIT
fi

Voici comment l'utiliser. Notez la ligne après le shebang:

     #/bin/bash
     . /opt/racechecker/racechecker
     echo "script are running"
     sleep 120

La façon dont cela fonctionne est qu’il détermine le nom principal du fichier bashscript et crée un fichier pid sous "/ tmp". Il ajoute également un auditeur au signal d'arrivée. L'écouteur supprimera le pidfile lorsque le script principal se terminera correctement.

À la place, si un fichier pid existe lors du lancement d'une instance, l'instruction if contenant le code à l'intérieur de la deuxième instruction if sera exécutée. Dans ce cas, j'ai décidé de lancer un mail d'alarme lorsque cela se produit.

Que se passe-t-il si le script plante?

Un autre exercice consisterait à gérer les collisions. Idéalement, le fichier pid devrait être supprimé même si le script principal se bloque pour une raison quelconque, cela ne se fait pas dans ma version ci-dessus. Cela signifie que si le script se bloque, le pidfile devra être supprimé manuellement pour restaurer la fonctionnalité.

En cas de crash du système

C'est une bonne idée de stocker le fichier pidfile / lockfile sous par exemple / tmp. De cette façon, vos scripts continueront définitivement à s'exécuter après un plantage du système puisque les pidfiles seront toujours supprimés au démarrage.


Contrairement à l'ansatz de Tim Kennedy, votre script contient une condition de concurrence critique. En effet, votre vérification de la présence de PIDFILE et sa création conditionnelle ne sont pas effectuées dans une opération atomique.
maxschlepzig

+1 sur ça! Je vais prendre cela en considération et modifier mon script.
ziggestardust

-2

Vérifiez mon script ...

Vous pouvez l' aimer ....

[rambabu@Server01 ~]$ sh Prevent_cron-OR-Script_against_parallel_run.sh
Parallel RUN Enabled
Now running
Task completed in Parallel RUN...
[rambabu@Server01 ~]$ cat Prevent_cron-OR-Script_against_parallel_run.sh
#!/bin/bash
#Created by RambabuKella
#Date : 12-12-2013

#LOCK file name
Parallel_RUN="yes"
#Parallel_RUN="no"
PS_GREP=0
LOCK=/var/tmp/mylock_`whoami`_"$0"
#Checking for the process
PS_GREP=`ps -ef |grep "sh $0" |grep -v grep|wc -l`
if [ "$Parallel_RUN" == "no" ] ;then
echo "Parallel RUN Disabled"

 if [ -f $LOCK ] || [ $PS_GREP -gt 2   ] ;then
        echo -e "\nJob is already running OR LOCK file exists. "
        echo -e "\nDetail are : "
        ps -ef |grep  "$0" |grep -v grep
        cat "$LOCK"
  exit 6
 fi
echo -e "LOCK file \" $LOCK \" created on : `date +%F-%H-%M` ." &> $LOCK
# do some work
echo "Now running"
echo "Task completed on with single RUN ..."
#done

rm -v $LOCK 2>/dev/null
exit 0
else

echo "Parallel RUN Enabled"

# do some work
echo "Now running"
echo "Task completed in Parallel RUN..."
#done

exit 0
fi
echo "some thing wrong"
exit 2
[rambabu@Server01 ~]$

-3

Je propose la solution suivante, dans un script nommé 'flocktest'

#!/bin/bash
export LOGFILE=`basename $0`.logfile
logit () {
echo "$1" >>$LOGFILE
}
PROGPATH=$0
(
flock -x -n 257
(($?)) && logit "'$PROGPATH' is already running!" && exit 0
logit "'$PROGPATH', proc($$): sleeping 30 seconds"
sleep 30
)257<$PROGPATH
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.