web-dev-qa-db-fra.com

Verrouillage correct dans les scripts Shell?

Parfois, vous devez vous assurer qu'une seule instance d'un script Shell s'exécute en même temps.

Par exemple, un travail cron qui est exécuté via crond qui ne fournit pas de verrouillage par lui-même (par exemple le crond Solaris par défaut).

Un modèle courant pour implémenter le verrouillage est un code comme celui-ci:

#!/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, ce code a une condition de concurrence. Il existe une fenêtre temporelle dans laquelle l'exécution de deux instances peut à la fois avancer après la ligne 3 avant de pouvoir toucher le fichier $LOCK.

Pour un travail cron, ce n'est généralement pas un problème car vous disposez d'un intervalle de minutes entre deux appels.

Mais les choses peuvent mal tourner - par exemple lorsque le fichier de verrouillage se trouve sur un serveur NFS - qui se bloque. Dans ce cas, plusieurs tâches cron peuvent bloquer sur la ligne 3 et faire la queue. Si le serveur NFS est de nouveau actif, vous avez troupeau de tonnerre de travaux en cours d'exécution en parallèle.

En recherchant 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 doit être verrouillé comme ceci:

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

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

Il utilise lockf() (POSIX) si disponible et revient à flock() (BSD). Et la prise en charge de lockf() sur NFS devrait être relativement répandue.

Existe-t-il des alternatives à lockrun?

Qu'en est-il des autres démons cron? Existe-t-il des crondes communes qui prennent en charge le verrouillage de manière saine? Un rapide coup d'œil à la page de manuel de Vixie Crond (par défaut sur les systèmes Debian/Ubuntu) ne montre rien sur le verrouillage.

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

À mon avis, il implémente un thème très similaire à timeout , Nice et à ses amis.

67
maxschlepzig

Voici une autre façon de verrouiller le script Shell qui peut empêcher la condition de concurrence critique que vous décrivez ci-dessus, où deux travaux peuvent tous deux passer la ligne 3. L'option noclobber fonctionnera dans ksh et bash. N'utilisez pas set noclobber parce que vous ne devriez pas faire 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 qu'auparavant. (Il ya 10 ans)

Si vous avez des tâches cron qui font la même chose en même temps, à partir de plusieurs serveurs, mais que vous n'avez besoin que d'une seule instance pour s'exécuter, quelque chose comme ça pourrait fonctionner pour vous.

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

Le verrouillage de fichiers est à peu près tout à fait conforme au comportement du système, et tous les scripts qui ne vérifient pas l'existence du fichier de verrouillage avant l'exécution feront tout ce qu'ils vont faire. En mettant le test du fichier de verrouillage et un comportement correct, vous résoudrez 99% des problèmes potentiels, sinon 100%.

Si vous rencontrez souvent des problèmes de concurrence avec les fichiers verrouillés, cela peut être un indicateur d'un problème plus important, comme le fait de ne pas synchroniser correctement vos travaux, ou peut-être si l'intervalle n'est pas aussi important que l'achèvement du travail, peut-être que votre travail est mieux adapté pour être démonifié .


MODIFIER CI-DESSOUS - 2016-05-06 (si vous utilisez KSH88)


Sur la base du commentaire de @Clint Pachl ci-dessous, si vous utilisez ksh88, utilisez mkdir au lieu de noclobber. Cela atténue principalement une condition de concurrence potentielle, mais ne la limite pas entièrement (bien que le risque soit minuscule). 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

Et, comme avantage supplémentaire, si vous devez créer des fichiers tmp dans votre script, vous pouvez utiliser le répertoire lockdir pour eux, sachant qu'ils seront nettoyés à la fin du script.

Pour des bash plus modernes, la méthode du noclobber au sommet devrait convenir.

45
Tim Kennedy

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 durs sont atomiques sur NFS et pour la plupart, mkdir l'est aussi . L'utilisation de mkdir(2) ou link(2) est à peu près la même, à un niveau pratique; Je préfère simplement utiliser des liens durs car plus d'implémentations de NFS autorisent les liens durs atomiques que atomic mkdir. Avec les versions modernes de NFS, vous ne devriez pas avoir à vous soucier de l’utilisation non plus.

14
Arcege

Je comprends que mkdir 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
12
glenn jackman

Un moyen simple consiste à utiliser lockfile fourni généralement avec le package procmail.

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"
8
jofel

sem qui fait partie des outils GNU parallel peut être ce que vous recherchez:

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 la commande n'est pas garantie. De plus, la sortie n'est pas affichée tant qu'elle n'est pas terminée (irritant!). Mais même ainsi, c'est le moyen le plus concis que je connaisse pour me prémunir contre l'exécution simultanée, sans se soucier des fichiers de verrouillage et des tentatives et du nettoyage.

5
Partly Cloudy

J'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
2
AndresVia

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 à partir de 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 1 s'il ne peut pas acquérir le fichier de verrouillage. Mais flock peut également être utilisé d'une manière qui ne nécessite pas l'exécution de commandes dans un sous-shell :-)

1
dru8274

N'utilisez pas de fichier.

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

bash my_script

Vous pouvez détecter s'il 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
1
frogstarr78

Pour une utilisation réelle, vous devez utiliser la réponse la mieux notée .

Cependant, je veux discuter de diverses approches brisées et semi-réalisables en utilisant ps et les nombreuses mises en garde qu'elles ont, car je continue de voir des gens les utiliser.

Cette réponse est vraiment la réponse à "Pourquoi ne pas utiliser ps et grep pour gérer le verrouillage dans le shell?"

Approche brisée # 1

Tout d'abord, une approche donnée dans ne autre réponse qui a quelques votes positifs malgré le fait qu'elle ne fonctionne pas (et ne pourrait jamais) fonctionner et n'a 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 arguments ps 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 toujours quitte 6, à chaque fois, peu importe comment vous l'exécutez.

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

Si vous l'exécutez avec bash myscript, les choses deviennent plus intéressantes. Le processus bash forks pour exécuter le pipeline, et le shell child exécute ps et grep. Le shell d'origine et le shell enfant s'afficheront dans la sortie ps, quelque chose comme ceci:

25793 bash myscript
25795 bash myscript

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

Approche brisée # 2

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

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 presque fonctionne. Mais le fait de bifurquer pour faire fonctionner le tuyau jette cela. Donc celui-ci sortira toujours aussi.

Approche peu fiable # 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 bifurcation du pipeline dans l'approche # 2 en obtenant d'abord tous les PID qui ont le script actuel dans leurs arguments de ligne de commande, et alors filtrant cette liste de pid, séparément, pour omettre le PID du script actuel.

Cela pourrait fonctionner ... à condition qu'aucun autre processus ne dispose d'une ligne de commande correspondant à $0, et fournir le script est 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 la première).

Approche peu fiable # 4

Que se passe-t-il si nous sautons la vérification de la ligne de commande complète, car cela pourrait ne pas indiquer un script en cours d'exécution, et vérifions lsof à la place pour trouver tous les processus ayant ce script ouvert?

Eh bien, oui, cette approche n'est en fait pas trop mauvaise:

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, alors la nouvelle instance démarrera très bien 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 un git checkout), la "nouvelle" version du script démarrera sans problème, car Vim et git checkout résulte en un nouveau fichier (un nouvel inode) à la place de l'ancien.

Cependant, si le script n'est jamais modifié et jamais copié, alors 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 la vérification puisse être atteinte.

Il peut toujours y avoir des faux positifs si un autre processus a le fichier de script ouvert, mais notez que même s'il est ouvert pour modification dans Vim, vim ne maintient pas le fichier de script ouvert et n'entraînera donc pas de faux positifs.

Mais rappelez-vous, n'utilisez pas cette approche si le script peut être modifié ou copié car vous obtiendrez faux négatifs c'est-à-dire plusieurs instances s'exécutant à la fois - donc le fait que l'édition avec Vim ne fonctionne pas donner des faux positifs ne devrait pas vous intéresser. Je le mentionne cependant, car l'approche # 3 le fait donne des faux positifs (c'est-à-dire refuse de démarrer) si vous avez le script ouvert avec Vim.

Que faire alors?

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

Peut-être que vous pouvez en écrire un meilleur ... mais si vous ne comprenez pas tous les problèmes et mises en garde avec toutes les approches ci-dessus, vous n'êtes pas susceptible d'écrire une méthode de verrouillage qui les évite tous.

1
Wildcard

En utilisant l'outil FLOM (Free LOck Manager) , la sérialisation des commandes devient aussi simple que l'exécution

flom -- command_to_serialize

FLOM vous permet d'implémenter des cas d'utilisation plus souples (verrouillage distribué, lecteurs/écrivains, ressources numériques, etc ...) comme expliqué ici: http://sourceforge.net/p/flom/wiki/FLOM%20by % 20exemples /

0
tiian

Voici quelque chose que j'ajoute parfois sur un serveur pour gérer facilement les conditions de concurrence pour n'importe quel travail sur la machine. C'est similaire au message de Tim Kennedy, mais de cette façon, vous obtenez la gestion de la course en n'ajoutant qu'une ligne à chaque script bash qui en a besoin.

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

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"  [email protected] >>/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 du fichier bashscript principal et crée un fichier pid sous "/ tmp". Il ajoute également un écouteur au signal d'arrivée. L'auditeur supprimera le fichier pid lorsque le script principal se terminera correctement.

Au lieu de cela, 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 e-mail d'alarme lorsque cela se produit.

Que faire si le script plante

Un autre exercice serait de gérer les accidents. Idéalement, le pidfile devrait être supprimé même si le script principal se bloque pour une raison quelconque, cela n'est pas fait dans ma version ci-dessus. Cela signifie que si le script plante, le pidfile devra être supprimé manuellement pour restaurer la fonctionnalité.

En cas de plantage du système

C'est une bonne idée de stocker le 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 car les fichiers pid seront toujours supprimés au démarrage.

0
ziggestardust