¿Cómo asegurarse de que solo se ejecute una instancia de un script bash?

26

Se preferiría una solución que no requiera herramientas adicionales.

Tobias Kienzler
fuente
¿Qué pasa con un archivo de bloqueo?
Marco
@Marco Encontré esta respuesta SO usando eso, pero como se indicó en un comentario , esto puede crear una condición de carrera
Tobias Kienzler
3
Esto es BashFAQ 45 .
jw013
@ jw013 gracias! Entonces, tal vez algo como ln -s my.pid .lockreclamar el bloqueo (seguido de echo $$ > my.pid) y en caso de falla puede verificar si el PID almacenado .lockes realmente una instancia activa del script
Tobias Kienzler,

Respuestas:

18

Casi como la respuesta de nsg: use un directorio de bloqueo . La creación de directorios es atómica en Linux y Unix y * BSD y muchos otros sistemas operativos.

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

Puede poner el PID del bloqueo sh en un archivo en el directorio de bloqueo para fines de depuración, pero no caiga en la trampa de pensar que puede verificar ese PID para ver si el proceso de bloqueo aún se ejecuta. Muchas condiciones de carrera se encuentran en ese camino.

Bruce Ediger
fuente
1
Consideraría usar el PID almacenado para verificar si la instancia de bloqueo aún está activa. Sin embargo, aquí hay una afirmación que mkdirno es atómica en NFS (que no es el caso para mí, pero creo que uno debería mencionar eso, si es cierto)
Tobias Kienzler
Sí, utilice el PID almacenado para ver si el proceso de bloqueo aún se ejecuta, pero no intente hacer nada más que registrar un mensaje. El trabajo de verificar el pid almacenado, crear un nuevo archivo PID, etc., deja una gran ventana para las carreras.
Bruce Ediger
Ok, como dijo Ihunath , lo más probable es /tmpque el lockdir esté en el que generalmente no se comparte NFS, por lo que debería estar bien.
Tobias Kienzler
Lo usaría rm -rfpara eliminar el directorio de bloqueo. rmdirfallará si alguien (no necesariamente usted) logró agregar un archivo al directorio.
chepner
18

Para agregar a la respuesta de Bruce Ediger , e inspirado por esta respuesta , también debe agregar más inteligencia a la limpieza para evitar la terminación del 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
fuente
Alternativamente, if ! mkdir "$LOCKDIR"; then handle failure to lock and exit; fi trap and do processing after if-statement.
Kusalananda
6

Esto puede ser demasiado simplista, corrígeme si me equivoco. ¿No es lo pssuficientemente simple ?

#!/bin/bash 

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

# do stuff below this comment
terdon
fuente
1
Agradable y / o brillante. :)
Spooky
1
He usado esta condición durante una semana, y en 2 ocasiones no evitó que se iniciara un nuevo proceso. Me di cuenta de cuál es el problema: el nuevo pid es una subcadena del antiguo y se oculta grep -v $$. ejemplos reales: antiguo - 14532, nuevo - 1453, antiguo - 28858, nuevo - 858.
Naktibalda
Lo arreglé cambiando grep -v $$agrep -v "^${$} "
Naktibalda
@Naktibalda buena captura, gracias! También puedes arreglarlo con grep -wv "^$$"(ver edición).
terdon
Gracias por esa actualización Mi patrón ocasionalmente fallaba porque los pids más cortos se dejaban rellenados con espacios.
Naktibalda
4

Usaría un archivo de bloqueo, como lo menciona 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
fuente
1
(Creo que olvidó crear el archivo de bloqueo) ¿Qué pasa con las condiciones de carrera ?
Tobias Kienzler
ops :) Sí, las condiciones de carrera son un problema en mi ejemplo, generalmente escribo trabajos cron por hora o por día y las condiciones de carrera son raras.
nsg
Tampoco deberían ser relevantes en mi caso, pero es algo que uno debe tener en cuenta. ¿Quizás usar lsof $0no es malo tampoco?
Tobias Kienzler
Puede disminuir la condición de carrera escribiendo su $$en el archivo de bloqueo. Luego, sleeppor un breve intervalo y léelo de nuevo. Si el PID sigue siendo suyo, adquirió el bloqueo con éxito. No necesita absolutamente ninguna herramienta adicional.
manatwork
1
Nunca he usado lsof para este propósito, esto debería funcionar. Tenga en cuenta que lsof es realmente lento en mi sistema (1-2 segundos) y lo más probable es que haya mucho tiempo para las condiciones de carrera.
nsg
3

Si quieres asegurarte de que solo se esté ejecutando una instancia de tu script, mira:

Bloquee su script (contra ejecución paralela)

De lo contrario, puede verificar pso invocar lsof <full-path-of-your-script>, ya que no los llamaría herramientas adicionales.


Suplemento :

en realidad pensé en hacerlo así:

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

esto asegura que solo el proceso con el más bajo pidsiga ejecutándose incluso si bifurca y ejecuta varias instancias de forma <your_script>simultánea.

usuario1146332
fuente
1
Gracias por el enlace, pero ¿podría incluir las partes esenciales en su respuesta? Es una política común en SE evitar la pudrición de enlaces ... Pero algo como esto [[(lsof $0 | wc -l) > 2]] && exitpodría ser suficiente, ¿o esto también es propenso a las condiciones de carrera?
Tobias Kienzler
Tienes razón, faltaba la parte esencial de mi respuesta y solo publicar enlaces es bastante lamentable. Agregué mi propia sugerencia a la respuesta.
user1146332
3

Otra forma de asegurarse de que se ejecute una sola instancia de script bash:

#!/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 obtiene el PID del script existente si ya se está ejecutando o sale con el código de error 1 si no se está ejecutando ningún otro script

Sethu
fuente
3

Aunque ha pedido una solución sin herramientas adicionales, esta es mi forma favorita de usar flock:

#!/bin/sh

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

echo "servus!"
sleep 10

Esto viene de la sección de ejemplos de man flock, que explica además:

Este es un código repetitivo útil para los scripts de shell. Póngalo en la parte superior del script de shell que desea bloquear y se bloqueará automáticamente en la primera ejecución. Si env var $ FLOCKER no está configurado para el script de shell que se está ejecutando, ejecute flock y tome un bloqueo exclusivo sin bloqueo (usando el script en sí como el archivo de bloqueo) antes de volver a ejecutarlo con los argumentos correctos. También establece FLOCKER env var en el valor correcto para que no vuelva a ejecutarse.

Puntos a considerar:

  • Requiere flock, el script de ejemplo termina con un error si no se puede encontrar
  • No necesita un archivo de bloqueo adicional
  • Puede no funcionar si el script está en NFS (consulte /server/66919/file-locks-on-an-nfs )

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

Schieferstapel
fuente
1

Esta es una versión modificada de la Respuesta de Anselmo . La idea es crear un descriptor de archivo de solo lectura usando el script bash y usarlo flockpara manejar el bloqueo.

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 principal diferencia con todas las demás respuestas es que este código no modifica el sistema de archivos, utiliza una huella muy baja y no necesita ninguna limpieza, ya que el descriptor de archivos se cierra tan pronto como el script finaliza independientemente del estado de salida. Por lo tanto, no importa si el script falla o tiene éxito.

John Doe
fuente
Siempre debe citar todas las referencias de variables de shell a menos que tenga una buena razón para no hacerlo y esté seguro de saber lo que está haciendo. Entonces deberías estar haciendo exec 6< "$SCRIPT".
Scott
@ Scott, he cambiado el código según tus sugerencias. Muchas gracias.
John Doe
1

Estoy usando cksum para verificar que mi script realmente esté ejecutando una sola instancia, incluso cambio el nombre del archivo y la ruta del archivo .

No estoy usando trap & lock file, porque si mi servidor se apaga repentinamente, necesito eliminar manualmente el archivo de bloqueo después de que el servidor se enciende.

Nota: #! / Bin / bash en primera línea se requiere para 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

O puede codificar cksum dentro de su secuencia de comandos, de modo que no se preocupe nuevamente si desea cambiar el nombre de archivo, la ruta o el contenido de su secuencia de comandos .

#!/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
fuente
1
Explique exactamente cómo codificar la suma de comprobación es una buena idea.
Scott
no codifica la suma de comprobación, solo crea la clave de identidad de su script, cuando se ejecuta otra instancia, verificará otro proceso de script de shell y capturará primero el archivo, si su clave de identidad está en ese archivo, entonces significa que su instancia ya se está ejecutando.
Arputra
OKAY; Por favor, editar su respuesta a explicar eso. Y, en el futuro, no publique múltiples bloques de código de 30 líneas que parezcan (casi) idénticos sin decir y explicar cómo son diferentes. Y no diga cosas como "puede codificar [sic] cksum dentro de su script", y no continúe usando nombres de variables mysumy fsum, cuando ya no esté hablando de una suma de verificación.
Scott
Parece interesante, gracias! Y bienvenidos a unix.stackexchange :)
Tobias Kienzler
0

Puede usar esto: https://github.com/sayanarijit/pidlock

sudo pip install -U pidlock

pidlock -n sleepy_script -c 'sleep 10'
Arijit Basu
fuente
> Se preferiría una solución que no requiera herramientas adicionales.
Dhag
0

Mi código para ti

#!/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}

Basado en man flock, mejorando solo:

  • el nombre del archivo de bloqueo, que se basará en el nombre completo del script
  • el mensaje executing

Donde puse aquí sleep 10, puedes poner todo el guión principal.

Anselmo Blanco Dominguez
fuente