Comment s'assurer qu'une seule instance d'un script bash s'exécute?

26

Une solution ne nécessitant pas d'outils supplémentaires serait préférable.

Tobias Kienzler
la source
Et un fichier de verrouillage?
Marco
@Marco J'ai trouvé cette réponse SO en utilisant cela, mais comme indiqué dans un commentaire , cela peut créer une condition de
concurrence
3
C'est BashFAQ 45 .
jw013
@ jw013 merci! Alors peut-être quelque chose comme ln -s my.pid .lockréclamera le verrou (suivi de echo $$ > my.pid) et en cas d'échec peut vérifier si le PID stocké dans .lockest vraiment une instance active du script
Tobias Kienzler

Réponses:

18

Presque comme la réponse de nsg: utilisez un répertoire de verrouillage . La création de répertoire est atomique sous linux et unix et * BSD et beaucoup d'autres OS.

if mkdir $LOCKDIR
then
    # Do important, exclusive stuff
    if rmdir $LOCKDIR
    then
        echo "Victory is mine"
    else
        echo "Could not remove lock dir" >&2
    fi
else
    # Handle error condition
    ...
fi

Vous pouvez mettre le PID du sh de verrouillage dans un fichier dans le répertoire de verrouillage à des fins de débogage, mais ne tombez pas dans le piège de penser que vous pouvez vérifier ce PID pour voir si le processus de verrouillage s'exécute toujours. Beaucoup de conditions de course se trouvent sur cette voie.

Bruce Ediger
la source
1
J'envisagerais d'utiliser le PID stocké pour vérifier si l'instance de verrouillage est toujours en vie. Cependant, voici une affirmation qui mkdirn'est pas atomique sur NFS (ce qui n'est pas le cas pour moi, mais je suppose que l'on devrait le mentionner, si c'est vrai)
Tobias Kienzler
Oui, utilisez certainement le PID stocké pour voir si le processus de verrouillage s'exécute toujours, mais n'essayez pas de faire autre chose que d'enregistrer un message. Le travail de vérification du pid stocké, de création d'un nouveau fichier PID, etc., laisse une grande fenêtre pour les courses.
Bruce Ediger
Ok, comme l' a dit Ihunath , le lockdir serait probablement dans /tmplequel n'est généralement pas partagé NFS, donc ça devrait aller.
Tobias Kienzler
J'utiliserais rm -rfpour supprimer le répertoire de verrouillage. rmdiréchouera si quelqu'un (pas nécessairement vous) a réussi à ajouter un fichier dans le répertoire.
chepner
18

Pour ajouter à la réponse de Bruce Ediger , et inspiré par cette réponse , vous devez également ajouter plus d'intelligence au nettoyage pour éviter la terminaison de script:

#Remove the lock directory
function cleanup {
    if rmdir $LOCKDIR; then
        echo "Finished"
    else
        echo "Failed to remove lock directory '$LOCKDIR'"
        exit 1
    fi
}

if mkdir $LOCKDIR; then
    #Ensure that if we "grabbed a lock", we release it
    #Works for SIGTERM and SIGINT(Ctrl-C)
    trap "cleanup" EXIT

    echo "Acquired lock, running"

    # Processing starts here
else
    echo "Could not create lock directory '$LOCKDIR'"
    exit 1
fi
Igor Zevaka
la source
Vous pouvez également if ! mkdir "$LOCKDIR"; then handle failure to lock and exit; fi trap and do processing after if-statement.
Kusalananda
6

Cela peut être trop simpliste, veuillez me corriger si je me trompe. N'est-ce pas psassez simple ?

#!/bin/bash 

me="$(basename "$0")";
running=$(ps h -C "$me" | grep -wv $$ | wc -l);
[[ $running > 1 ]] && exit;

# do stuff below this comment
terdon
la source
1
Agréable et / ou brillant. :)
Spooky
1
J'ai utilisé cette condition pendant une semaine, et à 2 reprises, cela n'a pas empêché le démarrage d'un nouveau processus. J'ai compris quel était le problème - le nouveau pid est une sous-chaîne de l'ancien et est caché par grep -v $$. exemples réels: ancien - 14532, nouveau - 1453, ancien - 28858, nouveau - 858.
Naktibalda
Je l'ai corrigé en changeant grep -v $$pourgrep -v "^${$} "
Naktibalda
@Naktibalda bonne prise, merci! Vous pouvez également le corriger avec grep -wv "^$$"(voir modifier).
terdon
Merci pour cette mise à jour. Mon modèle a parfois échoué parce que les pids plus courts étaient remplis d'espaces.
Naktibalda
4

J'utiliserais un fichier de verrouillage, comme mentionné par Marco

#!/bin/bash

# Exit if /tmp/lock.file exists
[ -f /tmp/lock.file ] && exit

# Create lock file, sleep 1 sec and verify lock
echo $$ > /tmp/lock.file
sleep 1
[ "x$(cat /tmp/lock.file)" == "x"$$ ] || exit

# Do stuff
sleep 60

# Remove lock file
rm /tmp/lock.file
NS g
la source
1
(Je pense que vous avez oublié de créer le fichier de verrouillage) Qu'en est-il des conditions de course ?
Tobias Kienzler
ops :) Oui, les conditions de course sont un problème dans mon exemple, j'écris habituellement des tâches cron horaires ou quotidiennes et les conditions de course sont rares.
nsg
Ils ne devraient pas non plus être pertinents dans mon cas, mais c'est quelque chose qu'il faut garder à l'esprit. Peut-être que l'utilisation lsof $0n'est pas mauvaise non plus?
Tobias Kienzler
Vous pouvez diminuer la condition de concurrence en écrivant votre $$dans le fichier de verrouillage. Ensuite, sleeppendant un court intervalle et relisez-le. Si le PID est toujours le vôtre, vous avez réussi à acquérir le verrou. Ne nécessite absolument aucun outil supplémentaire.
manatwork
1
Je n'ai jamais utilisé lsof à cet effet, je pense que cela devrait fonctionner. Notez que lsof est vraiment lent dans mon système (1-2 sec) et très probablement il y a beaucoup de temps pour les conditions de course.
nsg
3

Si vous voulez vous assurer qu'une seule instance de votre script est en cours d'exécution, jetez un œil à:

Verrouillez votre script (contre l'exécution parallèle)

Sinon, vous pouvez vérifier psou invoquer lsof <full-path-of-your-script>, car je ne les appellerais pas des outils supplémentaires.


Supplément :

en fait j'ai pensé à faire comme ça:

for LINE in `lsof -c <your_script> -F p`; do 
    if [ $$ -gt ${LINE#?} ] ; then
        echo "'$0' is already running" 1>&2
        exit 1;
    fi
done

cela garantit que seul le processus avec le plus bas pidcontinue de fonctionner même si vous forkez et exécutez plusieurs instances de <your_script>simultanément.

user1146332
la source
1
Merci pour le lien, mais pourriez-vous inclure les éléments essentiels dans votre réponse? C'est une politique commune au SE d'empêcher la pourriture des liens ... Mais quelque chose comme ça [[(lsof $0 | wc -l) > 2]] && exitpourrait être suffisant, ou est-ce aussi sujet aux conditions de course?
Tobias Kienzler
Vous avez raison, la partie essentielle de ma réponse manquait et seuls les liens de publication sont assez boiteux. J'ai ajouté ma propre suggestion à la réponse.
user1146332
3

Une autre façon de vous assurer qu'une seule instance du script bash s'exécute:

#!/bin/bash

# Check if another instance of script is running
pidof -o %PPID -x $0 >/dev/null && echo "ERROR: Script $0 already running" && exit 1

...

pidof -o %PPID -x $0 obtient le PID du script existant s'il est déjà en cours d'exécution ou se termine avec le code d'erreur 1 si aucun autre script n'est en cours d'exécution

Sethu
la source
3

Bien que vous ayez demandé une solution sans outils supplémentaires, c'est ma façon préférée d'utiliser flock:

#!/bin/sh

[ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en "$0" "$0" "$@" || :

echo "servus!"
sleep 10

Cela vient de la section des exemples de man flock, qui explique plus en détail:

Il s'agit d'un code passe-partout utile pour les scripts shell. Placez-le en haut du script shell que vous souhaitez verrouiller et il se verrouillera automatiquement lors de la première exécution. Si env var $ FLOCKER n'est pas défini sur le script shell en cours d'exécution, exécutez flock et récupérez un verrou exclusif non bloquant (en utilisant le script lui-même comme fichier de verrouillage) avant de le réexécuter avec les bons arguments. Il définit également la variable env FLOCKER à la bonne valeur afin qu'il ne s'exécute plus.

Points à considérer:

  • Requiert flock, l'exemple de script se termine par une erreur s'il est introuvable
  • Ne nécessite aucun fichier de verrouillage supplémentaire
  • Peut ne pas fonctionner si le script est sur NFS (voir /server/66919/file-locks-on-an-nfs )

Voir également /programming/185451/quick-and-dirty-way-to-ensure-only-one-instance-of-a-shell-script-is-running-at .

schieferstapel
la source
1

Il s'agit d'une version modifiée de la réponse d' Anselmo . L'idée est de créer un descripteur de fichier en lecture seule à l'aide du script bash lui-même et à utiliser flockpour gérer le verrou.

SCRIPT=`realpath $0`     # get absolute path to the script itself
exec 6< "$SCRIPT"        # open bash script using file descriptor 6
flock -n 6 || { echo "ERROR: script is already running" && exit 1; }   # lock file descriptor 6 OR show error message if script is already running

echo "Run your single instance code here"

La principale différence avec toutes les autres réponses est que ce code ne modifie pas le système de fichiers, utilise un encombrement très faible et n'a pas besoin de nettoyage puisque le descripteur de fichier est fermé dès que le script se termine indépendamment de l'état de sortie. Ainsi, peu importe si le script échoue ou réussit.

John Doe
la source
Vous devez toujours citer toutes les références de variables de shell, sauf si vous avez une bonne raison de ne pas le faire, et vous êtes sûr de savoir ce que vous faites. Vous devriez donc faire exec 6< "$SCRIPT".
Scott
@Scott J'ai changé le code selon vos suggestions. Merci beaucoup.
John Doe
1

J'utilise cksum pour vérifier que mon script exécute vraiment une seule instance, même si je change le nom et le chemin du fichier .

Je n'utilise pas le fichier trap & lock, car si mon serveur tombe soudainement en panne, je dois supprimer le fichier de verrouillage manuellement après la montée du serveur.

Remarque: #! / Bin / bash en première ligne est requis pour grep ps

#!/bin/bash

checkinstance(){
   nprog=0
   mysum=$(cksum $0|awk '{print $1}')
   for i in `ps -ef |grep /bin/bash|awk '{print $2}'`;do 
        proc=$(ls -lha /proc/$i/exe 2> /dev/null|grep bash) 
        if [[ $? -eq 0 ]];then 
           cmd=$(strings /proc/$i/cmdline|grep -v bash)
                if [[ $? -eq 0 ]];then 
                   fsum=$(cksum /proc/$i/cwd/$cmd|awk '{print $1}')
                   if [[ $mysum -eq $fsum ]];then
                        nprog=$(($nprog+1))
                   fi
                fi
        fi
   done

   if [[ $nprog -gt 1 ]];then
        echo $0 is already running.
        exit
   fi
}

checkinstance 

#--- run your script bellow 

echo pass
while true;do sleep 1000;done

Ou vous pouvez coder en dur le cksum dans votre script, donc ne vous inquiétez plus si vous voulez changer le nom de fichier, le chemin ou le contenu de votre script .

#!/bin/bash

mysum=1174212411

checkinstance(){
   nprog=0
   for i in `ps -ef |grep /bin/bash|awk '{print $2}'`;do 
        proc=$(ls -lha /proc/$i/exe 2> /dev/null|grep bash) 
        if [[ $? -eq 0 ]];then 
           cmd=$(strings /proc/$i/cmdline|grep -v bash)
                if [[ $? -eq 0 ]];then 
                   fsum=$(grep mysum /proc/$i/cwd/$cmd|head -1|awk -F= '{print $2}')
                   if [[ $mysum -eq $fsum ]];then
                        nprog=$(($nprog+1))
                   fi
                fi
        fi
   done

   if [[ $nprog -gt 1 ]];then
        echo $0 is already running.
        exit
   fi
}

checkinstance

#--- run your script bellow

echo pass
while true;do sleep 1000;done
arputra
la source
1
Veuillez expliquer exactement en quoi le codage en dur de la somme de contrôle est une bonne idée.
Scott
pas la somme de contrôle de codage en dur, sa seule clé d'identité de création de votre script, quand une autre instance s'exécutera, il vérifiera l'autre processus de script shell et cat le fichier en premier, si votre clé d'identité est sur ce fichier, donc cela signifie que votre instance est déjà en cours d'exécution.
arputra
D'ACCORD; veuillez modifier votre réponse pour l'expliquer. Et, à l'avenir, veuillez ne pas publier plusieurs longs blocs de code de 30 lignes qui semblent (presque) identiques sans dire et expliquer en quoi ils sont différents. Et ne dites pas des choses comme «vous pouvez coder en dur [sic] cksum dans votre script», et ne continuez pas à utiliser des noms de variable mysumet fsum, lorsque vous ne parlez plus de somme de contrôle.
Scott
Semble intéressant, merci! Et bienvenue sur unix.stackexchange :)
Tobias Kienzler
0

Vous pouvez utiliser ceci: https://github.com/sayanarijit/pidlock

sudo pip install -U pidlock

pidlock -n sleepy_script -c 'sleep 10'
Arijit Basu
la source
> Une solution ne nécessitant pas d'outils supplémentaires serait préférable.
dhag
0

Mon code pour toi

#!/bin/bash

script_file="$(/bin/readlink -f $0)"
lock_file=${script_file////_}

function executing {
  echo "'${script_file}' already executing"
  exit 1
}

(
  flock -n 9 || executing

  sleep 10

) 9> /var/lock/${lock_file}

Basé sur man flock, améliorant uniquement:

  • le nom du fichier de verrouillage, basé sur le nom complet du script
  • le message executing

Où je mets ici le sleep 10, vous pouvez mettre tout le script principal.

Anselmo Blanco Dominguez
la source