web-dev-qa-db-fra.com

Une méthode rapide et efficace pour vous assurer qu'une seule instance d'un script Shell est en cours d'exécution à la fois

Quelle est la méthode la plus simple pour vérifier qu’une seule instance d’un script Shell est en cours d’exécution à un moment donné?

155
raldi

Voici une implémentation qui utilise un lockfile et y renvoie 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 le kill -0 qui ne fournit aucun signal mais vérifie simplement si un processus avec le PID donné existe. De plus, l'appel à trap garantira que le lockfile est supprimé même lorsque votre processus est tué (sauf kill -9).

100
bmdhacks

Utilisez flock(1) pour créer un descripteur de fichier verrouillé exclusif. 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 compris entre ( et ) est exécuté uniquement par un processus à la fois et que le processus n’attend pas trop longtemps le verrouillage.

Avertissement: cette commande particulière fait partie de util-linux . Si vous utilisez un système d'exploitation autre que Linux, il est possible que ce système ne soit pas disponible.

190
Alex B

Toutes les approches qui testent l'existence de "fichiers de verrouillage" 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 critique pour que _ fasse échouer vos tentatives d'exclusion mutuelle.

Au lieu de cela, vous devez utiliser mkdir. mkdir crée un répertoire s'il n'existe pas encore et, le cas échéant, 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, voir l'excellent BashFAQ:  http://mywiki.wooledge.org/BashFAQ/045

Si vous voulez vous occuper des verrous périmés, fuser (1) est pratique. Le seul inconvénient est que l'opération prend environ une seconde et n'est donc pas instantanée.

Voici une fonction que j'ai écrite une fois qui résout le problème en utilisant fuser:

#       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 système UNIX), Linux ' fuser (1) offre quelques options supplémentaires et il existe aussi flock (1) .

147
lhunath

Il y a un wrapper autour de l'appel système flock (2) appelé, sans imagination, flock (1). Il est donc relativement facile d’obtenir de manière fiable des verrous exclusifs sans s’inquiéter du nettoyage, etc. Il existe des exemples dans la page de manuel concernant son utilisation dans un script Shell.

40
Cowan

Vous avez besoin d'une opération atomique, comme flock, sinon cela finira par échouer.

Mais que faire si troupeau n'est pas disponible. Eh bien il y a mkdir. C'est une opération atomique aussi. 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 un crash de votre script ne sera plus jamais exécuté.

27
Gunstick

Pour rendre le verrouillage fiable, vous avez besoin d’une opération atomique. Beaucoup des propositions ci-dessus Ne sont pas atomiques. L’utilitaire lockfile (1) proposé semble prometteur, comme l’indique la page de manuel Si votre système d'exploitation ne prend pas en charge lockfile (1) et 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 reporter 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 pivotants pour les fichiers et les répertoires (dans Shell, pas PHP):

verrouiller le répertoire actuel:

while ! ln -s . lock; do :; done

verrouiller un fichier:

while ! ln -s ${f} ${f}.lock; do :; done

unlock dir actuel (hypothèse, le processus en cours a réellement acquis le verrou):

mv lock deleteme && rm deleteme

déverrouiller un fichier (par hypothèse, le processus en cours a réellement acquis le verrou):

mv ${f}.lock ${f}.deleteme && rm ${f}.deleteme

Remove n’est pas non plus atomique, c’est pourquoi il faut d’abord renommer (qui est atomique) puis supprimer.

Pour les appels de lien symbolique et de renommage, 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 fichier et verrouillez-le dans le même répertoire.

22
Stefan Tramm

Une autre option consiste à utiliser l'option noclobber de Shell en exécutant set -C. Alors > é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 le fichier de manière atomique ou échoue si le fichier existe déjà.


Selon un commentaire sur BashFAQ 045 , cela peut échouer dans 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

Il est intéressant de noter que pdksh ajoute le drapeau O_TRUNC, mais il est évidemment redondant:
soit vous créez un fichier vide, soit vous ne faites rien.


La manière dont vous faites rm dépend de la manière dont vous voulez que les sorties non nettoyées soient gérées.

Supprimer en sortie propre

Les nouvelles exécutions échouent jusqu'à ce que le problème à l'origine de la résolution de la sortie non nettoyée et la suppression manuelle du fichier de verrouillage soient supprimés.

# 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
20
Mikel

Pour les scripts Shell, j’ai tendance à utiliser mkdir à flock car cela rend les verrous plus portables.

De toute façon, utiliser set -e ne suffit pas. Cela ne ferme le script que si une commande échoue. Vos serrures seront toujours laissés.

Pour un nettoyage correct des verrous, vous devez définir vos interruptions sur quelque chose comme ce code psuedo (levé, simplifié et non testé mais à partir de scripts utilisés activement):

#=======================================================================
# 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 arriver. Tous les pièges produiront une sortie afin que la fonction __sig_exit soit toujours exécutée (sauf un SIGKILL) qui nettoie vos verrous.

Remarque: mes valeurs de sortie ne sont pas des valeurs basses. Pourquoi? Différents systèmes de traitement par lots établissent ou ont des attentes pour les nombres compris entre 0 et 31. En leur attribuant une autre valeur, je peux faire en sorte que mes scripts et les flux de traitement par lots réagissent en fonction du travail par lots ou du script précédent.

16
Mark Stinson

Vous pouvez utiliser GNU Parallel pour cela car il fonctionne comme un mutex lorsqu'il est appelé sem. Donc, concrètement, vous pouvez utiliser:

sem --id SCRIPTSINGLETON yourScript

Si vous voulez aussi un délai d'attente, utilisez:

sem --id SCRIPTSINGLETON --semaphoretimeout -10 yourScript

Un délai d'expiration inférieur à 0 signifie que vous quittez sans exécuter le script si le sémaphore n'est pas libéré dans le délai imparti, un délai d'expiration supérieur à 0 signifie que le script est toujours exécuté.

Notez que vous devriez lui donner un nom (avec --id), sinon le terminal de contrôle le paramétrera par défaut.

GNU Parallel est une installation très simple sur la plupart des plates-formes Linux/OSX/Unix - il ne s'agit que d'un script Perl.

14
Mark Setchell

Vraiment rapide et vraiment sale? Cette ligne sur le dessus de votre script fonctionnera:

[[ $(pgrep -c "`basename \"$0\"`") -gt 1 ]] && exit

Bien sûr, assurez-vous que votre nom de script est unique. :)

13
Majal

Cet exemple est expliqué dans man flock, mais il nécessite quelques améliorations car nous devrions gérer les bugs 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, la liste des processus que j'ai utilisés dans le passé. Mais c'est plus compliqué que la méthode ci-dessus. Vous devriez 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

5
Znik

Créer un fichier de verrouillage dans un emplacement connu et vérifier son existence au démarrage du script? Inscrire le PID dans le fichier peut être utile si quelqu'un tente de localiser une instance erronée empêchant l'exécution du script.

5
Rob

Voici une approche qui combine le verrouillage de répertoire atomique avec une vérification du verrouillage obsolète via PID et un redémarrage si obsolète. En outre, cela ne repose sur aucun basisme.

#!/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
5
bk138

Si les limitations de flock, qui ont déjà été décrites ailleurs dans ce fil, ne vous concernent pas, 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 
4
presto8

Lorsque je cible une machine Debian, le paquet lockfile-progs est une bonne solution. procmail est également livré avec un outil lockfile. Cependant parfois je suis coincé avec ni l'un ni l'autre.

Voici ma solution qui utilise mkdir pour atomic-ness et un fichier PID pour détecter les verrous périmés. Ce code est actuellement en production sur une configuration Cygwin et fonctionne bien.

Pour l'utiliser, appelez simplement exclusive_lock_require lorsque vous avez besoin d'un accès exclusif à quelque chose. Un paramètre facultatif de nom de verrou vous permet de partager des verrous entre différents scripts. Il existe également deux fonctions de niveau inférieur (exclusive_lock_try et exclusive_lock_retry) si vous avez besoin de quelque chose de plus complexe.

function exclusive_lock_try() # [lockname]
{

    local LOCK_NAME="${1:-`basename $0`}"

    LOCK_DIR="/tmp/.${LOCK_NAME}.lock"
    local LOCK_PID_FILE="${LOCK_DIR}/${LOCK_NAME}.pid"

    if [ -e "$LOCK_DIR" ]
    then
        local LOCK_PID="`cat "$LOCK_PID_FILE" 2> /dev/null`"
        if [ ! -z "$LOCK_PID" ] && kill -0 "$LOCK_PID" 2> /dev/null
        then
            # locked by non-dead process
            echo "\"$LOCK_NAME\" lock currently held by PID $LOCK_PID"
            return 1
        else
            # orphaned lock, take it over
            ( echo $$ > "$LOCK_PID_FILE" ) 2> /dev/null && local LOCK_PID="$$"
        fi
    fi
    if [ "`trap -p EXIT`" != "" ]
    then
        # already have an EXIT trap
        echo "Cannot get lock, already have an EXIT trap"
        return 1
    fi
    if [ "$LOCK_PID" != "$$" ] &&
        ! ( umask 077 && mkdir "$LOCK_DIR" && umask 177 && echo $$ > "$LOCK_PID_FILE" ) 2> /dev/null
    then
        local LOCK_PID="`cat "$LOCK_PID_FILE" 2> /dev/null`"
        # unable to acquire lock, new process got in first
        echo "\"$LOCK_NAME\" lock currently held by PID $LOCK_PID"
        return 1
    fi
    trap "/bin/rm -rf \"$LOCK_DIR\"; exit;" EXIT

    return 0 # got lock

}

function exclusive_lock_retry() # [lockname] [retries] [delay]
{

    local LOCK_NAME="$1"
    local MAX_TRIES="${2:-5}"
    local DELAY="${3:-2}"

    local TRIES=0
    local LOCK_RETVAL

    while [ "$TRIES" -lt "$MAX_TRIES" ]
    do

        if [ "$TRIES" -gt 0 ]
        then
            sleep "$DELAY"
        fi
        local TRIES=$(( $TRIES + 1 ))

        if [ "$TRIES" -lt "$MAX_TRIES" ]
        then
            exclusive_lock_try "$LOCK_NAME" > /dev/null
        else
            exclusive_lock_try "$LOCK_NAME"
        fi
        LOCK_RETVAL="${PIPESTATUS[0]}"

        if [ "$LOCK_RETVAL" -eq 0 ]
        then
            return 0
        fi

    done

    return "$LOCK_RETVAL"

}

function exclusive_lock_require() # [lockname] [retries] [delay]
{
    if ! exclusive_lock_retry "$@"
    then
        exit 1
    fi
}
4
Jason Weathered

Je voulais supprimer les fichiers de verrouillage, les répertoires de verrouillage, les programmes de verrouillage spéciaux et même pidof, car ils ne sont pas présents sur toutes les installations Linux. Voulais aussi avoir le code le plus simple possible (ou au moins le moins de lignes possible). Déclaration la plus simple if, sur une ligne:

if [[ $(ps axf | awk -v pid=$$ '$1!=pid && $6~/'$(basename $0)'/{print $1}') ]]; then echo "Already running"; exit; fi
3
linux_newbie

Certains unixes ontlockfile, ce qui est très similaire auflockdéjà mentionné.

De la page de manuel:

lockfile peut être utilisé pour créer un ou plusieurs fichiers sémaphores. Si lock - Le fichier ne peut pas créer tous les fichiers .__ spécifiés. fichiers (dans l’ordre spécifié), il attend le sommeil (valeur par défaut de 8) secondes et réessaie le dernier fichier que n'a pas réussi. Vous pouvez spécifier le nombre de tentatives à faire jusqu'à l'échec est retourné. Si le nombre de tentatives est -1 (par défaut, c'est-à-dire, -r-1), lockfile réessayera pour toujours.

3
dmckee

Les réponses existantes publiées reposent sur l'utilitaire CLI flock ou ne sécurisent pas correctement le fichier de verrouillage. L'utilitaire flock n'est pas disponible sur tous les systèmes non Linux (à savoir, FreeBSD) et ne fonctionne pas correctement sur NFS.

À mes débuts en administration système et en développement système, on m'a dit qu'une méthode sûre et relativement portable de création d'un fichier de verrouillage consistait à créer un fichier temporaire à l'aide de mkemp(3) ou mkemp(1), à écrire des informations d'identification dans le fichier temporaire (c.-à-d. PID), puis lier durement le fichier temporaire au fichier verrou. Si le lien a réussi, vous avez réussi à obtenir le verrou.

Lorsque vous utilisez des verrous dans les scripts Shell, je place généralement une fonction obtain_lock() dans un profil partagé, puis 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 à tout point de sortie de votre script.

J'ai utilisé ce qui précède dans les environnements Linux et FreeBSD.

2
David M. Syzdek

Les PID et lockfiles sont certainement les plus fiables. Lorsque vous essayez d'exécuter le programme, il peut vérifier le fichier verrou qui, s'il existe, peut utiliser ps pour voir si le processus est toujours en cours d'exécution. Si ce n'est pas le cas, le script peut démarrer en mettant à jour le PID du fichier de verrouillage.

2
Drew Stephens

En fait, bien que la réponse de bmdhacks soit presque bonne, il existe une faible chance que le second script s'exécute après la première vérification du fichier verrou et avant son écriture. Ainsi, ils écriront tous les deux le fichier de verrouillage et s'exécuteront 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

L'option noclobber s'assurera que la commande de redirection échouera si le fichier existe déjà. Donc, la commande de redirection est réellement 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.

P.S. 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 afin de réduire les chances que le fichier de verrouillage reste après l'arrêt du script avec Ctrl-C par exemple. C'est donc la solution complète

2
NickSoft

J'utilise une approche simple qui gère les fichiers de verrouillage obsolètes.

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 de longue durée.

J'utilise noclobber pour m'assurer qu'un seul script peut ouvrir et écrire dans le fichier de verrouillage à la fois. En outre, je stocke suffisamment d'informations pour identifier de manière unique un processus dans le fichier verrou. Je définis l'ensemble de données pour identifier de manière unique un processus pid, ppid, lstart. 

Lorsqu'un nouveau script démarre, s'il ne parvient pas à créer le fichier de verrouillage, il vérifie que le processus qui a créé le fichier de verrouillage est toujours actif. Sinon, nous supposons que le processus original est mort d'une mort sans grâce et a laissé un fichier de verrouillage périmé. Le nouveau script prend alors possession du fichier de verrouillage, et tout va bien dans le monde, encore une fois.

Devrait fonctionner avec plusieurs coques 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
2
rouble

Un exemple avec flock (1) mais sans sous-shell. Le fichier flock () ed/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
1
sivann

Ajoutez simplement [ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en "$0" "$0" "$@" || : au début de votre script. C'est un code passe-partout de man flock . Pour comprendre son fonctionnement, j'ai écrit un script et l'ai exécuté simultanément à partir de deux consoles:

#!/bin/bash

if [ "${FLOCKER}" != "$0" ]; then
        echo "FLOCKER=$FLOCKER \$0=$0 ($$)"
        exec env FLOCKER="$0" flock -en "$0" "$0" "$@" || :
else
        echo "FLOCKER equals \$0 = $FLOCKER ($$)"
fi

sleep 10
echo "Process $$ finished"

Je n'ai pas encore pleinement compris son fonctionnement, mais il semble qu'il se remette à fonctionner en utilisant lui-même un fichier de verrouillage. FLOCKER défini sur "$0" juste pour définir une valeur raisonnable non négligeable. || : ne rien faire en cas de problème.

Cela ne semble pas fonctionner sous Debian 7, mais semble fonctionner de nouveau avec le paquet expérimental util-linux 2.25. Il écrit "flock: ... Fichier texte occupé". Il pourrait être remplacé en désactivant l'autorisation d'écriture sur votre script.

1
user3132194

L'utilisation du verrou du processus est beaucoup plus forte et prend en charge les sorties inusitées également . Lock_file est maintenu ouvert aussi longtemps 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 
1
Sudhir Kumar

Je trouve que la solution de bmdhack est la plus pratique, du moins pour mon cas d'utilisation. L'utilisation de flock et de lockfile repose sur la suppression du fichier lockfile à l'aide de rm lorsque le script se termine, ce qui ne peut pas toujours être garanti (par exemple, kill -9).

Je voudrais changer une chose mineure à propos de la solution de bmdhack: elle supprime le fichier de verrouillage sans indiquer que cela est inutile 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 inactif sera simplement ignoré/écrasé.

Ma solution simplifiée consiste donc simplement à ajouter les éléments suivants 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 concurrence critique, car le test de verrouillage et les opérations de définition ne constituent pas une action atomique unique. Mais la solution proposée par lhunath pour utiliser mkdir a la faille qu'un script tué peut laisser derrière le répertoire, empêchant ainsi d'autres instances de s'exécuter.

1
thecowster

L'utilitaire sémaphorique utilise flock (comme indiqué ci-dessus, par exemple par presto8) pour implémenter un sémaphore de comptage Il permet d'activer un nombre spécifique de processus simultanés. Nous l'utilisons pour limiter le niveau de simultanéité de divers processus de travail en file d'attente.

C'est comme sem mais beaucoup plus léger. (Divulgation complète: je l'ai écrit après avoir constaté que le sem était trop lourd pour nos besoins et qu'il n'existait pas d'utilitaire de comptage simple.)

1
Tim Bunce

Répondu déjà un million de fois, mais d'une autre manière, sans nécessité 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, le démarrage du script vérifie si un processus est en cours d'exécution avec le dernier PID.

1
Filidor Wiese

Je ne l’ai trouvé mentionné nulle part, il utilise read, je ne sais pas exactement si read est réellement atomique mais il m’a bien servi jusqu’à présent ..., c’est juteux parce que ce n’est que des basins intégrés, c’est un processus en cours. Lors de la mise en œuvre, vous démarrez le coprocess de locker et utilisez son e/s pour gérer les verrous. De la même manière, vous pouvez le faire interprocessus en échangeant simplement l'entrée/sortie cible des descripteurs de fichier locker par un descripteur de fichier système (exec 3<>/file && exec 4</file)

## gives locks
locker() {
    locked=false
    while read l; do
        case "$l" in
            lock)
                if $locked; then
                    echo false
                else
                    locked=true
                    echo true
                fi
                ;;
            unlock)
                if $locked; then
                    locked=false
                    echo true
                else
                    echo false
                fi
                ;;
            *)
                echo false
                ;;
        esac
    done
}
## locks
lock() {
    local response
    echo lock >&${locker[1]}
    read -ru ${locker[0]} response
    $response && return 0 || return 1
}

## unlocks
unlock() {
    local response
    echo unlock >&${locker[1]}
    read -ru ${locker[0]} response
    $response && return 0 || return 1
}
0
untore

Le chemin de troupeau est le chemin à parcourir. Pensez à ce qui se passe lorsque le script meurt subitement. Dans le cas d'un troupeau, vous perdez simplement le troupeau, mais ce n'est pas un problème. Notez également qu’un truc diabolique est de prendre un troupeau sur le script lui-même. Mais cela vous permet bien sûr de lancer à la va-vite les problèmes d’autorisation.

0

pourquoi ne pas utiliser quelque chose comme 

pgrep -f $cmd || $cmd
0
Jabir Ahmed

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
0
Aupajo

Jetez un coup d’œil à FLOM (Free LOck Manager) http://sourceforge.net/projects/flom/ : vous pouvez synchroniser des commandes et/ou des scripts à l’aide de ressources abstraites ne nécessitant pas le verrouillage de fichiers dans un système de fichiers. Vous pouvez synchroniser des commandes exécutées sur différents systèmes sans un NAS (stockage associé au réseau), contrairement à un serveur NFS (Network File System).

En utilisant le cas d'utilisation le plus simple, la sérialisation de "commande1" et "commande2" peut être aussi simple que d'exécuter:

flom -- command1

et

flom -- command2

à partir de deux scripts shell différents.

0
tiian
if [ 1 -ne $(/bin/fuser "$0" 2>/dev/null | wc -w) ]; then
    exit 1
fi
0
Rudolf Lörcks

Cette réponse à une ligne provient d'une personne proche Demandez à Ubuntu Q & A :

[ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en "$0" "$0" "$@" || :
#     This is useful boilerplate code for Shell scripts.  Put it at the top  of
#     the  Shell script you want to lock and it'll automatically lock itself on
#     the first run.  If the env var $FLOCKER is not set to  the  Shell  script
#     that  is being run, then execute flock and grab an exclusive non-blocking
#     lock (using the script itself as the lock file) before re-execing  itself
#     with  the right arguments.  It also sets the FLOCKER env var to the right
#     value so it doesn't run again.
0
WinEunuuchs2Unix

J'ai une solution simple basée sur le nom du fichier

#!/bin/bash

MY_FILENAME=`basename "$BASH_SOURCE"`

MY_PROCESS_COUNT=$(ps a -o pid,cmd | grep $MY_FILENAME | grep -v grep | grep -v $$ | wc -
l)

if [ $MY_PROCESS_COUNT -ne 0  ]; then
  echo found another process
  exit 0
if

# Follows the code to get the job done.
0
Gianluca Casati

Voici un plus élégant, sûr, rapide & sale méthode, en combinant les réponses fournies ci-dessus.

Usage

  1. include sh_lock_functions.sh
  2. init utilisant sh_lock_init
  3. verrouiller avec sh_acquire_lock
  4. vérifier le verrouillage avec sh_check_lock
  5. déverrouiller avec sh_remove_lock

Fichier de script

sh_lock_functions.sh

#!/bin/bash

function sh_lock_init {
    sh_lock_scriptName=$(basename $0)
    sh_lock_dir="/tmp/${sh_lock_scriptName}.lock" #lock directory
    sh_lock_file="${sh_lock_dir}/lockPid.txt" #lock file
}

function sh_acquire_lock {
    if mkdir $sh_lock_dir 2>/dev/null; then #check for lock
        echo "$sh_lock_scriptName lock acquired successfully.">&2
        touch $sh_lock_file
        echo $$ > $sh_lock_file # set current pid in lockFile
        return 0
    else
        touch $sh_lock_file
        read sh_lock_lastPID < $sh_lock_file
        if [ ! -z "$sh_lock_lastPID" -a -d /proc/$sh_lock_lastPID ]; then # if lastPID is not null and a process with that pid exists
            echo "$sh_lock_scriptName is already running.">&2
            return 1
        else
            echo "$sh_lock_scriptName stopped during execution, reacquiring lock.">&2
            echo $$ > $sh_lock_file # set current pid in lockFile
            return 2
        fi
    fi
    return 0
}

function sh_check_lock {
    [[ ! -f $sh_lock_file ]] && echo "$sh_lock_scriptName lock file removed.">&2 && return 1
    read sh_lock_lastPID < $sh_lock_file
    [[ $sh_lock_lastPID -ne $$ ]] && echo "$sh_lock_scriptName lock file pid has changed.">&2  && return 2
    echo "$sh_lock_scriptName lock still in place.">&2
    return 0
}

function sh_remove_lock {
    rm -r $sh_lock_dir
}

Exemple d'utilisation

sh_lock_usage_example.sh

#!/bin/bash
. /path/to/sh_lock_functions.sh # load sh lock functions

sh_lock_init || exit $?

sh_acquire_lock
lockStatus=$?
[[ $lockStatus -eq 1 ]] && exit $lockStatus
[[ $lockStatus -eq 2 ]] && echo "lock is set, do some resume from crash procedures";

#monitoring example
cnt=0
while sh_check_lock # loop while lock is in place
do
    echo "$sh_scriptName running (pid $$)"
    sleep 1
    let cnt++
    [[ $cnt -gt 5 ]] && break
done

#remove lock when process finished
sh_remove_lock || exit $?

exit 0

Caractéristiques

  • Utilise une combinaison de fichier, répertoire et identifiant de processus à verrouiller pour s'assurer que le processus n'est pas déjà en cours d'exécution
  • Vous pouvez détecter si le script s'est arrêté avant la suppression du verrou (par exemple, processus d'arrêt, arrêt, erreur, etc.)
  • Vous pouvez vérifier le fichier de verrouillage et l'utiliser pour déclencher un arrêt du processus lorsque le verrou est manquant.
  • Verbose, génère des messages d'erreur pour faciliter le débogage
0
Stefan Rogin

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

Sudo pip install -U pidlock

pidlock -n sleepy_script -c 'sleep 10'
0
Arijit Basu

Tard à la fête, utilisant l’idée de @Majal, c’est mon script pour ne démarrer qu’une seule instance de l'interface graphique emacsclient. Grâce à cela, je peux configurer raccourci clavier pour ouvrir ou revenir au même client emacs. J'ai un autre script pour appeler emacsclient dans les terminaux lorsque j'en ai besoin. L'utilisation de emacsclient ici est juste pour montrer un exemple de travail, on peut choisir autre chose. Cette approche est rapide et suffisante pour mes scripts minuscules. Dis-moi où c'est sale :)

#!/bin/bash

# if [ $(pgrep -c $(basename $0)) -lt 2 ]; then # this works but requires script name to be unique
if [ $(pidof -x "$0"|wc -w ) -lt 3 ]; then
    echo -e "Starting $(basename $0)"
    emacsclient --alternate-editor="" -c "$@"
else
    echo -e "$0 is running already"
fi
0
biocyberman