¿Correcto bloqueo en scripts de shell?

66

En ocasiones, debe asegurarse de que solo se ejecute una instancia de un script de shell al mismo tiempo.

Por ejemplo, un trabajo cron que se ejecuta a través de crond que no proporciona bloqueo por sí solo (por ejemplo, el crond de Solaris predeterminado).

Un patrón común para implementar el bloqueo es un código como este:

#!/bin/sh
LOCK=/var/tmp/mylock
if [ -f $LOCK ]; then            # 'test' -> race begin
  echo Job is already running\!
  exit 6
fi
touch $LOCK                      # 'set'  -> race end
# do some work
rm $LOCK

Por supuesto, dicho código tiene una condición de carrera. Hay una ventana de tiempo donde la ejecución de dos instancias puede avanzar después de la línea 3 antes de que uno pueda tocar el $LOCKarchivo.

Para un trabajo cron, esto generalmente no es un problema porque tiene un intervalo de minutos entre dos invocaciones.

Pero las cosas pueden salir mal, por ejemplo, cuando el archivo de bloqueo está en un servidor NFS, que se cuelga. En ese caso, varios trabajos cron pueden bloquearse en la línea 3 y hacer cola. Si el servidor NFS está activo de nuevo, entonces usted tiene rebaño atronadores de puestos de trabajo funcionando en paralelo.

Al buscar en la web encontré la herramienta lockrun que parece una buena solución para ese problema. Con él ejecutas un script que necesita bloquearse así:

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

Puede poner esto en un contenedor o usarlo desde su crontab.

Utiliza lockf()(POSIX) si está disponible y recurre a flock()(BSD). Y el lockf()soporte sobre NFS debería estar relativamente extendido.

¿Hay alternativas a lockrun?

¿Qué pasa con otros demonios cron? ¿Hay crond comunes que admiten el bloqueo de una manera sensata? Un vistazo rápido a la página de manual de Vixie Crond (predeterminado en los sistemas Debian / Ubuntu) no muestra nada sobre el bloqueo.

¿Sería una buena idea incluir una herramienta como lockrunen coreutils ?

En mi opinión, implementa un tema muy similar a timeout, nicey amigos.

maxschlepzig
fuente
44
Tangencialmente, y para el beneficio de otros que pueden considerar su patrón inicial lo suficientemente bueno (tm), ese código de shell posiblemente debería atrapar a TERM para eliminar su archivo de bloqueo cuando se edita kill; y parece ser una buena práctica almacenar el propio pid en el archivo de bloqueo, en lugar de solo tocarlo.
Ulrich Schwarz
@Shawn, en realidad no, no menciona crond y NFS.
maxschlepzig
pregunta relacionada sobre SO: stackoverflow.com/questions/185451/…
maxschlepzig
1
@Ulrich muy tardíamente, almacenar un PID en un archivo de bloqueo NFS agrega muy poco valor. Incluso agregar el nombre de host todavía no ayuda con la verificación de un proceso en vivo
roaima

Respuestas:

45

Aquí hay otra forma de bloquear el script de shell que puede evitar la condición de carrera que describió anteriormente, donde dos trabajos pueden pasar la línea 3. La noclobberopción funcionará en ksh y bash. No lo use set noclobberporque no debería estar creando scripts en csh / tcsh. ;)

lockfile=/var/tmp/mylock

if ( set -o noclobber; echo "$$" > "$lockfile") 2> /dev/null; then

        trap 'rm -f "$lockfile"; exit $?' INT TERM EXIT

        # do stuff here

        # clean up after yourself, and release your trap
        rm -f "$lockfile"
        trap - INT TERM EXIT
else
        echo "Lock Exists: $lockfile owned by $(cat $lockfile)"
fi

YMMV con bloqueo en NFS (ya sabes, cuando los servidores NFS no son accesibles), pero en general es mucho más robusto de lo que solía ser. (Hace 10 años)

Si tiene trabajos cron que hacen lo mismo al mismo tiempo, desde varios servidores, pero solo necesita 1 instancia para ejecutarse, algo como esto podría funcionar para usted.

No tengo experiencia con lockrun, pero tener un entorno de bloqueo preestablecido antes de que el script realmente se ejecute podría ayudar. O tal vez no. Solo está configurando la prueba para el archivo de bloqueo fuera de su script en un contenedor, y en teoría, ¿no podría simplemente alcanzar la misma condición de carrera si lockrun llamara a dos trabajos exactamente al mismo tiempo, al igual que con el 'inside- ¿La solución del script?

De todos modos, el bloqueo de archivos es más bien un comportamiento del sistema de honor, y cualquier script que no verifique la existencia del archivo de bloqueo antes de ejecutarse hará lo que sea que vayan a hacer. Simplemente al poner a prueba el archivo de bloqueo y el comportamiento adecuado, resolverá el 99% de los problemas potenciales, si no el 100%.

Si se encuentra mucho en condiciones de carrera de archivo de bloqueo, puede ser un indicador de un problema mayor, como no tener sus trabajos cronometrados correctamente, o tal vez si el intervalo no es tan importante como completar el trabajo, tal vez su trabajo sea más adecuado para ser demonizado .


EDITAR ABAJO - 2016-05-06 (si está utilizando KSH88)


Base en el comentario de @Clint Pachl a continuación, si usa ksh88, use en mkdirlugar de noclobber. Esto mitiga principalmente una posible condición de carrera, pero no la limita por completo (aunque el riesgo es minúsculo). Para obtener más información, lea el enlace que Clint publicó a continuación .

lockdir=/var/tmp/mylock
pidfile=/var/tmp/mylock/pid

if ( mkdir ${lockdir} ) 2> /dev/null; then
        echo $$ > $pidfile
        trap 'rm -rf "$lockdir"; exit $?' INT TERM EXIT
        # do stuff here

        # clean up after yourself, and release your trap
        rm -rf "$lockdir"
        trap - INT TERM EXIT
else
        echo "Lock Exists: $lockdir owned by $(cat $pidfile)"
fi

Y, como ventaja adicional, si necesita crear archivos tmp en su secuencia de comandos, puede usar el lockdirdirectorio para ellos, sabiendo que se limpiarán cuando salga la secuencia de comandos.

Para una fiesta más moderna, el método noclobber en la parte superior debería ser adecuado.

Tim Kennedy
fuente
1
No, con lockrun no tiene ningún problema: cuando un servidor NFS se cuelga, todas las llamadas de lockrun se cuelgan (al menos) en la lockf()llamada del sistema; cuando se realiza una copia de seguridad, todos los procesos se reanudan, pero solo un proceso ganará el bloqueo. Sin condición de carrera. No tengo muchos problemas con los cronjobs, lo contrario es el caso, pero esto es un problema cuando te golpea, tiene el potencial de crear mucho dolor.
maxschlepzig
1
He aceptado esta respuesta porque el método es seguro y hasta ahora el más elegante. Sugiero una pequeña variante: set -o noclobber && echo "$$" > "$lockfile"para obtener una recuperación segura cuando el shell no admite la opción noclobber.
maxschlepzig
3
Buena respuesta, pero también debe 'matar -0' el valor en el archivo de bloqueo para asegurarse de que el proceso que creó el bloqueo aún exista.
Nigel Horne
1
La noclobberopción puede ser propensa a las condiciones de carrera. Vea mywiki.wooledge.org/BashFAQ/045 para obtener algo de reflexión.
Clint Pachl
2
Nota: el uso de noclobber(o -C) en ksh88 no funciona porque ksh88 no se usa O_EXCLpara noclobber. Si está ejecutando con un shell más nuevo, puede estar bien ...
jrw32982 es compatible con Monica el
14

Prefiero usar enlaces duros.

lockfile=/var/lock/mylock
tmpfile=${lockfile}.$$
echo $$ > $tmpfile
if ln $tmpfile $lockfile 2>&-; then
    echo locked
else
    echo locked by $(<$lockfile)
    rm $tmpfile
    exit
fi
trap "rm ${tmpfile} ${lockfile}" 0 1 2 3 15
# do what you need to

Los enlaces duros son atómicos sobre NFS y, en su mayor parte, mkdir también lo es . Usando mkdir(2)o link(2)son casi lo mismo, en un nivel práctico; Simplemente prefiero usar enlaces duros porque más implementaciones de NFS permitieron enlaces duros atómicos que atómicos mkdir. Con los lanzamientos modernos de NFS, no debería tener que preocuparse por usar ninguno.

Arcege
fuente
12

Entiendo que mkdires atómico, así que quizás:

lockdir=/var/tmp/myapp
if mkdir $lockdir; then
  # this is a new instance, store the pid
  echo $$ > $lockdir/PID
else
  echo Job is already running, pid $(<$lockdir/PID) >&2
  exit 6
fi

# then set traps to cleanup upon script termination 
# ref http://www.shelldorado.com/goodcoding/tempfiles.html
trap 'rm -r "$lockdir" >/dev/null 2>&1' 0
trap "exit 2" 1 2 3 13 15
Glenn Jackman
fuente
Ok, pero no pude encontrar información mkdir()sobre si NFS (> = 3) está estandarizado para ser atómico.
maxschlepzig
2
@maxschlepzig RFC 1813 no exige explícitamente mkdirque sea atómico (sí lo hace rename). En la práctica, se sabe que algunas implementaciones no lo son. Relacionado: un hilo interesante, que incluye una contribución del autor de GNU arch .
Gilles 'SO- deja de ser malvado'
8

Una manera fácil es usar lockfileel procmailpaquete que viene generalmente .

LOCKFILE="/tmp/mylockfile.lock"
# try once to get the lock else exit
lockfile -r 0 "$LOCKFILE" || exit 0

# here the actual job

rm -f "$LOCKFILE"
jofel
fuente
5

semque viene como parte de las parallelherramientas de GNU puede ser lo que estás buscando:

sem [--fg] [--id <id>] [--semaphoretimeout <secs>] [-j <num>] [--wait] command

Como en:

sem --id my_semaphore --fg "echo 1 ; date ; sleep 3" &
sem --id my_semaphore --fg "echo 2 ; date ; sleep 3" &
sem --id my_semaphore --fg "echo 3 ; date ; sleep 3" &

salida:

1
Thu 10 Nov 00:26:21 UTC 2016
2
Thu 10 Nov 00:26:24 UTC 2016
3
Thu 10 Nov 00:26:28 UTC 2016

Tenga en cuenta que el orden no está garantizado. Además, la salida no se muestra hasta que finaliza (¡irritante!). Pero aun así, es la forma más concisa que conozco para evitar la ejecución simultánea, sin preocuparme por los archivos de bloqueo, los reintentos y la limpieza.

Parcialmente nublado
fuente
¿El bloqueo ofrecido por el semmango se derriba a mitad de la ejecución?
roaima
2

Yo uso dtach.

$ dtach -n /tmp/socket long_running_task ; echo $?
0
$ dtach -n /tmp/socket long_running_task ; echo $?
dtach: /tmp/socket: Address already in use
1
AndresVia
fuente
1

Utilizo la herramienta de línea de comandos "flock" para administrar bloqueos en mis scripts de bash, como se describe aquí y aquí . He usado este método simple desde la página de manual de flock, para ejecutar algunos comandos en una subshell ...

   (
     flock -n 9
     # ... commands executed under lock ...
   ) 9>/var/lock/mylockfile

En ese ejemplo, falla con el código de salida de 1 si no puede adquirir el archivo de bloqueo. Pero el lote también se puede usar de manera que no requiera que los comandos se ejecuten en un sub-shell :-)

dru8274
fuente
3
La flock()llamada al sistema no funciona a través de NFS .
maxschlepzig
1
BSD tiene una herramienta similar, "lockf".
dubiousjim
2
@dubiousjim, BSD lockf también llama flock()y, por lo tanto, es problemático en NFS. Por cierto, mientras tanto, flock () en Linux ahora recurre a fcntl()cuando el archivo se encuentra en un montaje NFS, por lo tanto, en un entorno NFS solo para Linux flock()ahora funciona sobre NFS.
maxschlepzig
1

No uses un archivo.

Si su script se ejecuta así, por ejemplo:

bash my_script

Puede detectar si se está ejecutando usando:

running_proc=$(ps -C bash -o pid=,cmd= | grep my_script);
if [[ "$running_proc" != "$$ bash my_script" ]]; do 
  echo Already locked
  exit 6
fi
frogstarr78
fuente
Hm, el código de comprobación de ps se ejecuta desde dentro my_script? En el caso de que se ejecute otra instancia, ¿no running_proccontiene dos líneas coincidentes? Me gusta la idea, pero, por supuesto, obtendrás resultados falsos cuando otro usuario ejecute un script con el mismo nombre ...
maxschlepzig
3
También incluye una condición de carrera: si 2 instancias ejecutan la primera línea en paralelo, ninguna obtiene el 'bloqueo' y ambas salen con el estado 6. Esto sería una especie de inanición mutua . Por cierto, no estoy seguro de por qué lo usas en $!lugar de $$en tu ejemplo.
maxschlepzig
@maxschlepzig de hecho lo siento por los $ incorrectos! vs. $$
frogstarr78
@maxschlepzig para manejar múltiples usuarios que ejecutan el script, agregue euser = al argumento -o.
frogstarr78
@maxschlepzig para evitar varias líneas, también puede cambiar los argumentos a grep o "filtros" adicionales (por ejemplo grep -v $$). Básicamente estaba intentando proporcionar un enfoque diferente al problema.
frogstarr78
1

Para uso real, debe usar la respuesta más votada .

Sin embargo, quiero hablar sobre varios enfoques rotos y semi-viables que usan psy las muchas advertencias que tienen, ya que sigo viendo que la gente los usa.

Esta respuesta es realmente la respuesta a "¿Por qué no usar psy grepmanejar el bloqueo en el shell?"

Enfoque roto # 1

Primero, un enfoque dado en otra respuesta que tiene algunos votos positivos a pesar de que no funciona (y nunca podría funcionar) y claramente nunca se probó:

running_proc=$(ps -C bash -o pid=,cmd= | grep my_script);
if [[ "$running_proc" != "$$ bash my_script" ]]; do 
  echo Already locked
  exit 6
fi

Arreglemos los errores de sintaxis y los psargumentos rotos y obtengamos:

running_proc=$(ps -C bash -o pid,cmd | grep "$0");
echo "$running_proc"
if [[ "$running_proc" != "$$ bash $0" ]]; then
  echo Already locked
  exit 6
fi

Este script siempre saldrá de 6, siempre, sin importar cómo lo ejecute.

Si lo ejecuta con ./myscript, entonces la pssalida será 12345 -bash, que no coincide con la cadena requerida 12345 bash ./myscript, por lo que fallará.

Si lo ejecutas bash myscript, las cosas se ponen más interesantes. El proceso bash se bifurca para ejecutar la canalización, y el shell secundario ejecuta el psy grep. Tanto el shell original como el shell hijo aparecerán en la pssalida, algo así:

25793 bash myscript
25795 bash myscript

Esa no es la salida esperada $$ bash $0, por lo que su script se cerrará.

Enfoque roto # 2

Ahora, para ser justos con el usuario que escribió el enfoque roto # 1, hice algo similar cuando probé esto por primera vez:

if otherpids="$(pgrep -f "$0" | grep -vFx "$$")" ; then
  echo >&2 "There are other copies of the script running; exiting."
  ps >&2 -fq "${otherpids//$'\n'/ }" # -q takes about a tenth the time as -p
  exit 1
fi

Esto casi funciona. Pero el hecho de bifurcarse para hacer correr la tubería arroja esto. Así que este siempre saldrá, también.

Enfoque poco confiable # 3

pids_this_script="$(pgrep -f "$0")"
if not_this_process="$(echo "$pids_this_script" | grep -vFx "$$")"; then
  echo >&2 "There are other copies of this script running; exiting."
  ps -fq "${not_this_process//$'\n'/ }"
  exit 1
fi

Esta versión evita el problema de la bifurcación de la tubería en el enfoque n. ° 2 al obtener primero todos los PID que tienen el script actual en sus argumentos de línea de comando y luego filtrar esa lista pid, por separado, para omitir el PID del script actual.

Esto podría funcionar ... siempre que ningún otro proceso tenga una línea de comando que coincida con $0, y siempre que el script se llame de la misma manera (por ejemplo, si se llama con una ruta relativa y luego una ruta absoluta, la última instancia no notará la primera )

Enfoque poco confiable # 4

Entonces, ¿qué pasa si omitimos la verificación de la línea de comando completa, ya que eso podría no indicar que un script se está ejecutando realmente y, en su lsoflugar , verificamos para encontrar todos los procesos que tienen abierto este script?

Bueno, sí, este enfoque en realidad no es tan malo:

if otherpids="$(lsof -t "$0" | grep -vFx "$$")"; then
  echo >&2 "Error: There are other processes that have this script open - most likely other copies of the script running.  Exiting to avoid conflicts."
  ps >&2 -fq "${otherpids//$'\n'/ }"
  exit 1
fi

Por supuesto, si se está ejecutando una copia del script, la nueva instancia se iniciará perfectamente y tendrá dos copias ejecutándose.

O si se modifica la secuencia de comandos en ejecución (por ejemplo, con Vim o con a git checkout), entonces la "nueva" versión de la secuencia de comandos se iniciará sin problemas, ya que tanto Vim como git checkoutun nuevo archivo (un nuevo inodo) en lugar del el viejo.

Sin embargo, si el script nunca se modifica y nunca se copia, entonces esta versión es bastante buena. No hay condición de carrera porque el archivo de secuencia de comandos ya debe estar abierto antes de que se pueda alcanzar la verificación.

Todavía puede haber falsos positivos si otro proceso tiene el archivo de script abierto, pero tenga en cuenta que incluso si está abierto para editar en Vim, vim no mantiene abierto el archivo de script, por lo que no dará lugar a falsos positivos.

Pero recuerde, no use este enfoque si el script se puede editar o copiar, ya que obtendrá falsos negativos, es decir, se ejecutarán varias instancias a la vez, por lo que el hecho de editar con Vim no da falsos positivos. para ti. Lo menciono, sin embargo, porque el enfoque # 3 no dar falsos positivos (es decir, se niega a iniciar) si tiene el guión abierto con Vim.

Entonces, ¿qué hacer, entonces?

La respuesta más votada a esta pregunta ofrece un buen enfoque sólido.

Quizás pueda escribir uno mejor ... pero si no comprende todos los problemas y advertencias con todos los enfoques anteriores, es probable que no escriba un método de bloqueo que los evite a todos.

Comodín
fuente
0

Aquí hay algo que a veces agrego en un servidor para manejar fácilmente las condiciones de carrera para cualquier trabajo en la máquina. Es similar a la publicación de Tim Kennedy, pero de esta manera obtienes el manejo de la carrera al agregar solo una fila a cada script de bash que lo necesita.

Ponga el contenido a continuación en, por ejemplo, / opt / racechecker / racechecker:

ZPROGRAMNAME=$(readlink -f $0)
EZPROGRAMNAME=`echo $ZPROGRAMNAME | sed 's/\//_/g'`
EZMAIL="/usr/bin/mail"
EZCAT="/bin/cat"

if  [ -n "$EZPROGRAMNAME" ] ;then
        EZPIDFILE=/tmp/$EZPROGRAMNAME.pid
        if [ -e "$EZPIDFILE" ] ;then
                EZPID=$($EZCAT $EZPIDFILE)
                echo "" | $EZMAIL -s "$ZPROGRAMNAME already running with pid $EZPID"  alarms@someemail.com >>/dev/null
                exit -1
        fi
        echo $$ >> $EZPIDFILE
        function finish {
          rm  $EZPIDFILE
        }
        trap finish EXIT
fi

Aquí está cómo usarlo. Tenga en cuenta la fila después del shebang:

     #/bin/bash
     . /opt/racechecker/racechecker
     echo "script are running"
     sleep 120

La forma en que funciona es que determina el nombre del archivo bashscript principal y crea un archivo pid en "/ tmp". También agrega un oyente a la señal de finalización. El oyente eliminará el archivo pid cuando el script principal esté terminando correctamente.

En cambio, si existe un archivo pid cuando se inicia una instancia, se ejecutará la instrucción if que contiene el código dentro de la segunda instrucción if. En este caso, he decidido lanzar un correo de alarma cuando esto sucede.

¿Qué pasa si el script falla

Otro ejercicio sería manejar los accidentes. Idealmente, el archivo pid debería eliminarse incluso si el script principal falla por algún motivo, esto no se hace en mi versión anterior. Eso significa que si el script falla, el archivo pid debería eliminarse manualmente para restaurar la funcionalidad.

En caso de bloqueo del sistema

Es una buena idea almacenar el archivo pidfile / lockfile en, por ejemplo, / tmp. De esta forma, sus secuencias de comandos continuarán ejecutándose definitivamente después de un bloqueo del sistema, ya que los archivos pid siempre se eliminarán en el arranque.

ziggestardust
fuente
A diferencia del ansatz de Tim Kennedy, su guión SÍ contiene una condición de carrera. Esto se debe a que su comprobación de la presencia del PIDFILE y su creación condicional no se realiza en una operación atómica.
maxschlepzig
+1 en eso! Tomaré esto en consideración y modificaré mi script.
ziggestardust
-2

Comprueba mi guión ...

Te puede ENCANTAR ...

[rambabu@Server01 ~]$ sh Prevent_cron-OR-Script_against_parallel_run.sh
Parallel RUN Enabled
Now running
Task completed in Parallel RUN...
[rambabu@Server01 ~]$ cat Prevent_cron-OR-Script_against_parallel_run.sh
#!/bin/bash
#Created by RambabuKella
#Date : 12-12-2013

#LOCK file name
Parallel_RUN="yes"
#Parallel_RUN="no"
PS_GREP=0
LOCK=/var/tmp/mylock_`whoami`_"$0"
#Checking for the process
PS_GREP=`ps -ef |grep "sh $0" |grep -v grep|wc -l`
if [ "$Parallel_RUN" == "no" ] ;then
echo "Parallel RUN Disabled"

 if [ -f $LOCK ] || [ $PS_GREP -gt 2   ] ;then
        echo -e "\nJob is already running OR LOCK file exists. "
        echo -e "\nDetail are : "
        ps -ef |grep  "$0" |grep -v grep
        cat "$LOCK"
  exit 6
 fi
echo -e "LOCK file \" $LOCK \" created on : `date +%F-%H-%M` ." &> $LOCK
# do some work
echo "Now running"
echo "Task completed on with single RUN ..."
#done

rm -v $LOCK 2>/dev/null
exit 0
else

echo "Parallel RUN Enabled"

# do some work
echo "Now running"
echo "Task completed in Parallel RUN..."
#done

exit 0
fi
echo "some thing wrong"
exit 2
[rambabu@Server01 ~]$
usuario54178
fuente
-3

Ofrezco la siguiente solución, en un script llamado 'flocktest'

#!/bin/bash
export LOGFILE=`basename $0`.logfile
logit () {
echo "$1" >>$LOGFILE
}
PROGPATH=$0
(
flock -x -n 257
(($?)) && logit "'$PROGPATH' is already running!" && exit 0
logit "'$PROGPATH', proc($$): sleeping 30 seconds"
sleep 30
)257<$PROGPATH
Newton T Hammet Jr
fuente