Un moyen rapide et efficace de garantir qu'une seule instance d'un script shell est en cours d'exécution à la fois

Réponses:

112

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 -0qui ne délivre aucun signal mais vérifie simplement si un processus avec le PID donné existe. L'appel à trapgarantira également que le fichier de verrouillage est supprimé même lorsque votre processus est tué (sauf kill -9).

bmdhacks
la source
75
Comme déjà mentionné dans un commentaire sur une autre réponse, cela a un défaut fatal - si l'autre script démarre entre la vérification et l'écho, vous êtes grillé.
Paul Tomblin
1
L'astuce du lien symbolique est intéressante, mais si le propriétaire du fichier de verrouillage est kill -9'd ou que le système plante, il y a toujours une condition de concurrence pour lire le lien symbolique, remarquer que le propriétaire est parti, puis le supprimer. Je m'en tiens à ma solution.
bmdhacks
10
Le contrôle et la création atomiques sont disponibles dans le shell en utilisant flock (1) ou lockfile (1). Voir d'autres réponses.
dmckee --- ex-moderator chaton
3
Voir ma réponse pour un moyen portable de faire une vérification atomique et de créer sans avoir à compter sur des utilitaires tels que flock ou lockfile.
lhunath
3
Ce n'est pas atomique et donc inutile. Vous avez besoin d'un mécanisme atomique pour tester et définir.
K Richard Pixley
219

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.

Alex B
la source
12
Qu'est-ce que le 200? Il dit «fd» dans le manul, mais je ne sais pas ce que cela signifie.
chovy
4
@chovy "descripteur de fichier", un descripteur entier désignant un fichier ouvert.
Alex B
6
Si quelqu'un d'autre se demande: la syntaxe ( command A ) command Binvoque 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.
Dr Jan-Philip Gehrcke
1
Je pense que le code à l'intérieur du sous-shell devrait ressembler davantage à: de if flock -x -w 10 200; then ...Do stuff...; else echo "Failed to lock file" 1>&2; fisorte 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?).
Jonathan Leffler
1
Le fichier redirigé vers n'est qu'un dossier de réservation sur lequel le verrou doit agir, il n'y a pas de données significatives. Le exitest 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.
clacke
160

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. mkdircré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) .

lhunath
la source
1
Vous pouvez combiner la if ! mkdirpartie 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.
Tobias Kienzler
4
Il est certainement vrai que ce mkdirn'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 /tmpsera un partage NFS et sera probablement fourni par un fs qui implémente mkdiratomiquement.
lhunath
5
Mais il existe un moyen de vérifier l'existence d'un fichier régulier et de le créer de manière atomique si ce n'est pas le cas: en utilisant lnpour 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.
Juan Cespedes
4
Il existe «un moyen de vérifier si un fichier existe et de le créer en une seule action atomique» - c'est 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).
Toby Speight
5
"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." - Pour le rendre atomique, il faut le faire à au niveau du noyau - et cela se fait au niveau du noyau avec flock (1) linux.die.net/man/1/flock qui, d'après la date de copyright de l'homme, existe depuis au moins 2006. J'ai donc fait un vote défavorable (- 1), rien de personnel, juste une forte conviction que l'utilisation des outils implémentés par le noyau fournis par les développeurs du noyau est correcte.
Craig Hicks
42

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.

Cowan
la source
3
L' flock()appel système n'est pas POSIX et ne fonctionne pas pour les fichiers sur les montages NFS.
maxschlepzig
17
Exécution à partir d'un travail Cron que j'utilise flock -x -n %lock file% -c "%command%"pour m'assurer qu'une seule instance est en cours d'exécution.
Ryall
Aww, au lieu du troupeau sans imagination (1), ils auraient dû aller avec quelque chose comme le troupeau (U). .. .il a une certaine familiarité avec cela. . .semble que je l'ai entendu avant une fois ou deux.
Kent Kruckeberg
Il est à noter que la documentation de flock (2) spécifie une utilisation uniquement avec des fichiers, mais que la documentation de flock (1) spécifie une utilisation avec un fichier ou un répertoire. La documentation flock (1) n'est pas explicite sur la manière d'indiquer la différence lors de la création, mais je suppose que cela se fait en ajoutant un "/" final. Quoi qu'il en soit, si flock (1) peut gérer les répertoires mais que flock (2) ne le peut pas, alors flock (1) n'est pas implémenté uniquement sur flock (2).
Craig Hicks
27

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é.

Gunstick
la source
1
Exécutez ceci plusieurs fois simultanément (comme "./a.sh & ./a.sh & ./a.sh & ./a.sh & ./a.sh & ./a.sh & ./a.sh & ") et le script perdra plusieurs fois.
Nippysaurus
8
@Nippysaurus: Cette méthode de verrouillage ne fuit pas. Ce que vous avez vu était le script initial se terminant avant que toutes les copies ne soient lancées, donc un autre a pu (correctement) obtenir le verrou. Pour éviter ce faux positif, ajoutez un sleep 10avant rmdiret essayez à nouveau de cascade - rien ne «fuira».
Sir Athos
D'autres sources affirment que mkdir n'est pas atomique sur certains systèmes de fichiers comme NFS. Et d'ailleurs, j'ai vu des occasions où sur NFS mkdir récursif simultané conduisait parfois à des erreurs avec les jobs de matrice jenkins. Je suis donc à peu près sûr que c'est le cas. Mais mkdir est assez sympa pour les cas d'utilisation moins exigeants IMO.
akostadinov
Vous pouvez utiliser l'option noclobber de Bash avec des fichiers normaux.
Palec
26

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:

  • lien symbolique
  • Renommer

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.


la source
Quelles pages de NFS Illustrated prennent en charge l'affirmation selon laquelle mkdir n'est pas atomique sur NFS?
maxschlepzig
Merci pour cette technique. Une implémentation de mutex shell est disponible dans ma nouvelle bibliothèque shell: github.com/Offirmo/offirmo-shell-lib , voir "mutex". Il utilise lockfilesi disponible, ou utilise cette symlinkméthode si ce n'est pas le cas.
Offirmo
Agréable. Malheureusement, cette méthode ne permet pas de supprimer automatiquement les verrous périmés.
Richard Hansen
Pour le déverrouillage en deux étapes ( mv, rm), devrait- rm -fon utiliser, plutôt que rmdans 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 mvet rm), enfin P1 tente rmet échoue.
Matt Wallis
1
@MattWallis Ce dernier problème pourrait facilement être atténué en l'incluant $$dans le ${f}.deletemenom de fichier.
Stefan Majewsky
23

Une autre option consiste à utiliser l' noclobberoption 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 pdkshajoute le O_TRUNCdrapeau, mais évidemment c'est redondant:
soit vous créez un fichier vide, soit vous ne faites rien.


La façon dont vous procédez rmdé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
Mikel
la source
Approche très nouvelle ... cela semble être une façon d'atteindre l'atomicité en utilisant un fichier de verrouillage plutôt qu'un répertoire de verrouillage.
Matt Caldwell
Belle approche. :-) Sur le piège EXIT, il devrait restreindre le processus qui peut nettoyer le fichier de verrouillage. Par exemple: trap 'if [[$ (cat "$ lockfile") == "$$"]]; puis rm "$ lockfile"; fi 'EXIT
Kevin Seifert
1
Les fichiers de verrouillage ne sont pas atomiques sur NFS. c'est pourquoi les gens sont passés à l'utilisation de répertoires verrouillés.
K Richard Pixley
20

Vous pouvez l'utiliser GNU Parallelcar 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.

Mark Setchell
la source
Dommage que les gens hésitent à voter contre des réponses inutiles: cela conduit à enfouir de nouvelles réponses pertinentes dans un tas d'ordures.
Dmitry Grigoryev
4
Nous avons juste besoin de beaucoup de votes positifs. C'est une réponse si ordonnée et peu connue. (Bien que pour être pédant, OP voulait rapide et sale alors que c'est rapide et propre!) Plus d'informations sur la semquestion connexe unix.stackexchange.com/a/322200/199525 .
Partiellement nuageux
16

Pour les scripts shell, j'ai tendance à aller avec le mkdirdessus flockcar cela rend les verrous plus portables.

Quoi qu'il en soit, l'utilisation set -ene 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_exitse 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.

Mark Stinson
la source
2
Votre script est beaucoup trop détaillé, aurait pu être beaucoup plus court je pense, mais dans l'ensemble, oui, vous devez mettre en place des pièges pour le faire correctement. J'ajouterais aussi SIGHUP.
mojuba
Cela fonctionne bien, sauf qu'il semble vérifier $ LOCK_DIR alors qu'il supprime $ __ lockdir. Peut-être devrais-je suggérer que lors de la suppression du verrou, vous feriez rm -r $ LOCK_DIR?
bevada
Merci pour la suggestion. Le code ci-dessus a été levé et placé à la manière d'un code psuedo, il devra donc être réglé en fonction de l'utilisation des gens. Cependant, j'ai délibérément opté pour rmdir dans mon cas car rmdir supprime en toute sécurité les répertoires uniquement_s'ils sont vides. Si les gens y placent des ressources telles que des fichiers PID, etc., ils devraient modifier leur nettoyage de verrouillage au plus agressif rm -r $LOCK_DIRou 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é.
Mark Stinson
Avez-vous testé exit 1002?
Gilles Quenot
13

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. :)

Majal
la source
Comment simuler cela pour le tester? Existe-t-il un moyen de démarrer un script deux fois en une seule ligne et peut-être obtenir un avertissement, s'il est déjà en cours d'exécution?
rubo77
2
Cela ne fonctionne pas du tout! Pourquoi vérifier -gt 2? grep ne se trouve pas toujours dans le résultat de ps!
rubo77 du
pgrepn'est pas dans POSIX. Si vous voulez que cela fonctionne de manière portable, vous avez besoin de POSIX pset de traiter sa sortie.
Palec
Sur OSX -cn'existe pas, vous devrez utiliser | wc -l. À propos de la comparaison des nombres: -gt 1est vérifiée puisque la première instance se voit.
Benjamin Peter
6

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
bk138
la source
5

Créer un fichier de verrouillage dans un emplacement connu et vérifier son existence au démarrage du script? Mettre le PID dans le fichier peut être utile si quelqu'un tente de retrouver une instance errante qui empêche l'exécution du script.

Rob
la source
5

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

Znik
la source
1
Vous pouvez utiliser ln -s, car cela ne peut créer de lien symbolique que lorsqu'aucun fichier ou lien symbolique n'existe, comme mkdir. de nombreux processus système utilisaient des liens symboliques dans le passé, par exemple init ou inetd. synlink conserve l'identifiant du processus, mais ne pointe vraiment vers rien. pendant des années, ce comportement a changé. processus utilise des flocs et des sémaphores.
Znik
5

Les réponses existantes publiées reposent sur l'utilitaire CLI flockou 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.

David M. Syzdek
la source
4

Lorsque je cible une machine Debian, je trouve que le lockfile-progspaquet est une bonne solution. procmailest également livré avec un lockfileoutil. Cependant, parfois je suis coincé avec aucun de ces derniers.

Voici ma solution qui utilise mkdirpour 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_requirelorsque 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_tryet 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
}
Jason patiné
la source
Merci, je l'ai essayé sur cygwin moi-même et il a passé des tests simples.
ndemou le
4

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 
presto8
la source
3
Je pensais juste que je soulignerais que -x (verrouillage en écriture) est déjà défini par défaut.
Keldon Alleyne
et le -nfera exit 1immédiatement s'il ne peut pas obtenir la serrure
Anentropic
Merci @KeldonAlleyne, j'ai mis à jour le code pour supprimer "-x" car il est par défaut.
presto8
3

Certains unix ont lockfilece 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.

dmckee --- chaton ex-modérateur
la source
comment obtenir l' lockfileutilitaire ??
Offirmo
lockfileest distribué avec procmail. Il existe également une alternative dotlockfilequi va avec le liblockfilepackage. Ils prétendent tous deux travailler de manière fiable sur NFS.
Mr. Deathless
3

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

NickSoft
la source
3

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
rouble
la source
Je vous ai donné +1 pour une solution différente. Bien que cela ne fonctionne pas non plus sous AIX (> ps -eo pid, ppid, lstart $$ | tail -1 ps: liste invalide avec -o.) Pas HP-UX (> ps -eo pid, ppid, lstart $$ | tail -1 ps: option illégale - o). Merci.
Tagar
3

Je voulais supprimer les fichiers de verrouillage, les lockdirs, les programmes de verrouillage spéciaux et même pidofparce 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
linux_newbie
la source
1
Ceci est sensible à la sortie 'ps', sur ma machine (Ubuntu 14.04, / bin / ps de procps-ng version 3.3.9) la commande 'ps axf' imprime des caractères d'arbre ascii qui perturbent les numéros de champ. Cela a fonctionné pour moi: /bin/ps -a --format pid,cmd | awk -v pid=$$ '/'$(basename $0)'/ { if ($1!=pid) print $1; }'
qneill
3

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.

user3132194
la source
1

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 pspour 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.

Drew Stephens
la source
1

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.

thecowster
la source
1

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.)

Tim Bunce
la source
1

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
Sivann
la source
1

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.

Filidor Wiese
la source
1
Sans l'appel d'interruption (ou au moins un nettoyage vers la fin pour le cas normal), vous avez le bogue faux positif où le fichier de verrouillage est laissé après la dernière exécution et le PID a été réutilisé par un autre processus plus tard. (Et dans le pire des cas, il a été doué pour un processus de longue durée comme Apache ....)
Philippe Chaintreuil
1
Je suis d'accord, mon approche est imparfaite, elle a besoin d'un piège. J'ai mis à jour ma solution. Je préfère toujours ne pas avoir de dépendances externes.
Filidor Wiese
1

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 
Sudhir Kumar
la source
1

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.

Z KC
la source
0

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.

JE DONNE DES RÉPONSES CRAP
la source
0

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
Aupajo
la source
7
Cela peut ou non être un problème, selon la façon dont il est utilisé, mais il y a une condition de concurrence entre le test du verrou et sa création, de sorte que deux scripts puissent tous les deux être démarrés en même temps. Si l'un se termine en premier, l'autre continuera de fonctionner sans fichier de verrouillage.
TimB
3
C News, qui m'a beaucoup appris sur les scripts shell portables, avait l'habitude de créer un fichier lock. $$, puis d'essayer de le lier avec "lock" - si le lien réussit, vous aviez le verrou, sinon vous avez supprimé le verrou. $$ et est sorti.
Paul Tomblin
C'est un très bon moyen de le faire, sauf que vous souffrez toujours de la nécessité de supprimer le fichier de verrouillage manuellement si quelque chose ne va pas et que le fichier de verrouillage n'est pas supprimé.
Matthew Scharley
2
Rapide et sale, c'est ce qu'il a demandé :)
Aupajo