Evite estos problemas limitando los scripts Bash para que se ejecuten una vez a la vez
Conclusiones clave
- Asegúrese de que solo se esté ejecutando una instancia de su secuencia de comandos utilizando pgrep, lsof o grey para evitar problemas de simultaneidad.
- Implemente fácilmente comprobaciones para finalizar automáticamente un script si se detectan otras instancias en ejecución.
- Al aprovechar los comandos exec y env, el comando Flock puede lograr todo esto con una línea de código.
Algunos scripts de Linux tienen tal sobrecarga de ejecución que es necesario evitar la ejecución de varias instancias a la vez. Afortunadamente, hay varias formas de lograr esto en sus propios scripts Bash.
A veces una vez es suficiente
Algunos scripts no deberían iniciarse si todavía se está ejecutando una instancia anterior de ese script. Si su secuencia de comandos consume demasiado tiempo de CPU y RAM, o genera mucho ancho de banda de red o destrucción del disco, limitar su ejecución a una instancia a la vez es de sentido común.
Pero no son sólo los acaparadores de recursos los que necesitan funcionar de forma aislada. Si su secuencia de comandos modifica archivos, puede haber un conflicto entre dos (o más) instancias de la secuencia de comandos mientras luchan por el acceso a los archivos. Es posible que se pierdan las actualizaciones o que el archivo esté dañado.
Una técnica para evitar estos escenarios problemáticos es hacer que el script verifique que no haya otras versiones ejecutándose. Si detecta otras copias en ejecución, el script finaliza automáticamente.
Otra técnica consiste en diseñar el script de tal manera que se bloquee cuando se inicie, evitando que se ejecuten otras copias.
Vamos a ver dos ejemplos de la primera técnica y luego veremos una forma de hacer la segunda.
Usando pgrep para prevenir la concurrencia
El comando pgrep busca entre los procesos que se ejecutan en una computadora Linux y devuelve el ID de proceso de los procesos que coinciden con el patrón de búsqueda.
Tengo un script llamado loop.sh. Contiene un bucle for que imprime la iteración del bucle y luego duerme por un segundo. Lo hace diez veces.
#!/bin/bash
for (( i=1; i<=10; i+=1 ))
do
echo "Loop:" $i
sleep 1
done
exit 0
Configuré dos instancias en ejecución y luego usé pgrep para buscarlo por nombre.
pgrep loop.sh
Localiza las dos instancias e informa sus ID de proceso. Podemos agregar la opción -c (recuento) para que pgrep devuelva el número de instancias.
pregp -c loop.sh
Podemos usar ese recuento de instancias en nuestro script. Si el valor devuelto por pgrep es mayor que uno, debe haber más de una instancia ejecutándose y nuestro script se cerrará.
Crearemos un script que utilice esta técnica. Lo llamaremos pgrep-solo.sh.
La comparación if prueba si el número devuelto por pgrep es mayor que uno. Si es así, el script se cierra.
# count the instances of this script
if [ $(pgrep -c pgrep-solo.sh) -gt 1 ]; then
echo "Another instance of $0 is running. Stopping."
exit 1
fi
Si el número devuelto por pgrep es uno, el script puede continuar. Aquí está el guión completo.
#!/bin/bash
echo "Starting."
# count the instances of this script
if [ $(pgrep -c pgrep-solo.sh) -gt 1 ]; then
echo "Another instance of $0 is running. Stopping."
exit 1
fi
# we're cleared for take off
for (( i=1; i<=10; i+=1 ))
do
echo "Loop:" $i
sleep 1
done
exit 0
Copie esto en su editor favorito y guárdelo como pgrep-solo.sh. Luego hazlo ejecutable con chmod.
chmod +x pgrep-loop.sh
Cuando se ejecuta, se ve así.
./pgrep-solo.sh
Pero si intento iniciarlo con otra copia que ya se está ejecutando en otra ventana de terminal, lo detecta y sale.
./pgrep-solo.sh
Uso de lsof para evitar la concurrencia
Podemos hacer algo muy similar con el comando lsof.
Si agregamos la opción -t (conciso), lsof enumera los ID de proceso.
lsof -t loop.sh
Podemos canalizar la salida de lsof a wc. La opción -l (líneas) cuenta la cantidad de líneas que, en este escenario, es la misma que la cantidad de ID de proceso.
lsof -t loop.sh | wc -l
Podemos usarlo como base de la prueba en la comparación if en nuestro script.
Guarde esta versión como lsof-solo.sh.
#!/bin/bash
echo "Starting."
# count the instances of this script
if [ $(lsof -t "$0" | wc -l) -gt 1 ]; then
echo "Another instance of $0 is running. Stopping."
exit 1
fi
# we're cleared for take off
for (( i=1; i<=10; i+=1 ))
do
echo "Loop:" $i
sleep 1
done
exit 0
Utilice chmod para hacerlo ejecutable.
chmod +x lsof-solo.sh
Ahora, con el script lsof-solo.sh ejecutándose en otra ventana de terminal, no podemos iniciar una segunda copia.
./lsof-solo.sh
El método pgrep solo requiere una única llamada a un programa externo (pgrep), el método lsof requiere dos (lsof y wc). Pero la ventaja que tiene el método lsof sobre el método pgrep es que puedes usar la variable $0 en la comparación if. Este contiene el nombre del script.
Significa que puedes cambiar el nombre del script y seguirá funcionando. No es necesario recordar editar la línea de comparación if e insertar el nuevo nombre del script. La variable $0 incluye './' al comienzo del nombre del script (como ./lsof-solo.sh), y a pgrep no le gusta.
Uso de rebaño para evitar la concurrencia
Nuestra tercera técnica utiliza el comando Flock, que está diseñado para establecer bloqueos de archivos y directorios desde scripts. Mientras esté bloqueado, ningún otro proceso puede acceder al recurso bloqueado.
Este método requiere que se agregue una sola línea en la parte superior de su secuencia de comandos.
[ "${GEEKLOCK}" != "$0" ] && exec env GEEKLOCK="$0" flock -en "$0" "$0" "$@" || :
En breve decodificaremos esos jeroglíficos. Por ahora, comprobemos que funciona. Guarde este como grey-solo.sh.
#!/bin/bash
[ "${GEEKLOCK}" != "$0" ] && exec env GEEKLOCK="$0" flock -en "$0" "$0" "$@" || :
echo "Starting."
# we're cleared for take off
for (( i=1; i<=10; i+=1 ))
do
echo "Loop:" $i
sleep 1
done
exit 0
Por supuesto, necesitamos hacerlo ejecutable.
chmod +x flock-solo.sh
Comencé el script en una ventana de terminal y luego intenté ejecutarlo nuevamente en una ventana de terminal diferente.
./flock-solo
./flock-solo
./flock-solo
No puedo iniciar el script hasta que se haya completado la instancia en la otra ventana de terminal.
Deshagámonos de la línea que hace la magia. En el centro de esto está el mandato del rebaño.
flock -en "$0" "$0" "$@"
El comando grey se utiliza para bloquear un archivo o directorio y luego ejecutar un comando. Las opciones que estamos usando son -e (exclusivo) y -n (sin bloqueo).
La opción exclusiva significa que si logramos bloquear el archivo, nadie más podrá acceder a él. La opción sin bloqueo significa que si no logramos obtener un bloqueo, inmediatamente dejamos de intentarlo. No lo volvemos a intentar durante un período de tiempo, nos retiramos elegantemente de inmediato.
El primer $0 indica el archivo que deseamos bloquear. Esta variable contiene el nombre del script actual.
El segundo $0 es el comando que queremos ejecutar si logramos obtener un bloqueo. Nuevamente, pasamos el nombre de este script. Debido a que el bloqueo bloquea a todos aparte de nosotros, podemos iniciar el archivo de secuencia de comandos.
Podemos pasar parámetros al comando que se lanza. Estamos usando $@ para pasar cualquier parámetro de línea de comando que se pasó a este script, a la nueva invocación del script que Flock ejecutará.
Entonces tenemos este script bloqueando el archivo de script y luego lanzando otra instancia de sí mismo. Eso es casi lo que queremos, pero hay un problema. Cuando finalice la segunda instancia, el primer script reanudará su procesamiento. Sin embargo, tenemos otro truco bajo la manga para solucionarlo, como verás.
Estamos usando una variable de entorno a la que llamamos GEEKLOCK para indicar si un script en ejecución necesita aplicar el bloqueo o no. Si el script se ha iniciado y no hay ningún bloqueo, se debe aplicar el bloqueo. Si el script se inició y hay un bloqueo, no necesita hacer nada, simplemente puede ejecutarse. Con un script en ejecución y el bloqueo activado, no se pueden iniciar otras instancias del script.
[ "${GEEKLOCK}" != "$0" ]
Esta prueba se traduce como "devuelve verdadero si la variable de entorno GEEKLOCK no está configurada con el nombre del script". La prueba se encadena al resto del comando mediante && (y) y || (o). La parte && se ejecuta si la prueba devuelve verdadero y el || La sección se ejecuta si la prueba devuelve falso.
env GEEKLOCK="$0"
El comando env se utiliza para ejecutar otros comandos en entornos modificados. Estamos modificando nuestro entorno creando la variable de entorno GEEKLOCK y configurándola con el nombre del script. El comando que env ejecutará es el comando grey, y el comando grey inicia la segunda instancia del script.
La segunda instancia del script realiza su verificación para ver si la variable de entorno GEEKLOCK no existe, pero descubre que sí. El || Se ejecuta la sección del comando, que no contiene nada más que dos puntos ':' que en realidad es un comando que no hace nada. La ruta de ejecución luego recorre el resto del script.
Pero todavía tenemos el problema de que el primer script continúa su propio procesamiento cuando el segundo script finaliza. La solución a esto es el comando ejecutivo. Esto ejecuta otros comandos reemplazando el proceso de llamada con el proceso recién iniciado.
exec env GEEKLOCK="$0" flock -en "$0" "$0" "$@"
Entonces la secuencia completa es:
- El script se inicia y no puede encontrar la variable de entorno. Se ejecuta la cláusula &&.
- exec inicia env y reemplaza el proceso de script original con el nuevo proceso env.
- El proceso env crea la variable de entorno y lanza Flock.
- Flock bloquea el archivo de secuencia de comandos y lanza una nueva instancia de la secuencia de comandos que detecta la variable de entorno, ejecuta el || cláusula y el guión puede ejecutarse hasta su conclusión.
- Debido a que el script original fue reemplazado por el proceso env, ya no está presente y no puede continuar su ejecución cuando finaliza el segundo script.
- Debido a que el archivo de secuencia de comandos está bloqueado cuando se está ejecutando, no se pueden iniciar otras instancias hasta que la secuencia de comandos iniciada por Flock deje de ejecutarse y libere el bloqueo.
Puede que suene como la trama de Inception, pero funciona a la perfección. Esa línea ciertamente tiene un gran impacto.
Para mayor claridad, es el bloqueo del archivo de script lo que impide que se inicien otras instancias, no la detección de la variable de entorno. La variable de entorno solo le dice a un script en ejecución que establezca el bloqueo o que el bloqueo ya esté en su lugar.
Bloquear y cargar
Es más fácil de lo que cabría esperar garantizar que solo se ejecute una única instancia de un script a la vez. Las tres técnicas funcionan. Aunque es la operación más complicada, la frase del rebaño es la más fácil de incluir en cualquier guión.