resueltos - scripts bash ejemplos
Manera rĂ¡pida y sucia de garantizar que solo se ejecute una instancia de un script de shell a la vez (30)
Quería acabar con ficheros de bloqueo, lockdirs, programas especiales de bloqueo e incluso pidof
ya que no se encuentra en todas las instalaciones de Linux. También quería tener el código más simple posible (o al menos tan pocas líneas como sea posible). Más simple if
declaración, en una línea:
if [[ $(ps axf | awk -v pid=$$ ''$1!=pid && $6~/''$(basename $0)''/{print $1}'') ]]; then echo "Already running"; exit; fi
¿Cuál es una forma rápida y sucia de asegurarse de que solo se ejecute una instancia de un script de shell en un momento dado?
Echar un vistazo a FLOM (Free Lock Manager) http://sourceforge.net/projects/flom/ : puede sincronizar comandos y / o secuencias de comandos que utilizan recursos abstractos que no necesitan archivos de bloqueo en un sistema de archivos. Puede sincronizar los comandos que se ejecutan en diferentes sistemas sin un NAS (Network Attached Storage) como un servidor NFS (Network File System).
Usando el caso de uso más simple, la serialización "comando1" y "comando2" puede ser tan fácil como ejecutar:
flom -- command1
y
flom -- command2
a partir de dos secuencias de comandos shell diferentes.
En realidad, aunque la respuesta de bmdhacks es casi buena, hay una pequeña posibilidad de que el segundo script se ejecute después de que primero verifique el archivo de bloqueo y antes de que lo haya escrito. Entonces ambos escribirán el archivo de bloqueo y ambos se ejecutarán. Aquí es cómo hacer que funcione con seguridad:
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
La opción noclobber
se asegurará de que el comando de redirección falle si el archivo ya existe. Entonces, el comando de redirección es atómico: usted escribe y verifica el archivo con un comando. No es necesario que elimine el archivo de bloqueo al final del archivo, ya que será eliminado por la trampa. Espero que esto ayude a las personas que lo leerán más tarde.
PD. No veo que Mikel ya haya respondido la pregunta correctamente, aunque no incluyó el comando de captura para reducir la posibilidad de que el archivo de bloqueo quede después de detener el script con Ctrl-C, por ejemplo. Entonces esta es la solución completa
El camino rebaño es el camino a seguir. Piense en lo que sucede cuando el guión muere repentinamente. En el rebaño caso que acaba de perder el rebaño, pero eso no es un problema. Además, tenga en cuenta que un mal truco es tomar un rebaño en el guión en sí .. pero por supuesto que le permite ejecutar-vapor a toda máquina en problemas de permisos.
Algunos unixes tienen un lockfile
que es muy similar a la flock
ya mencionada.
De la página de manual:
lockfile se puede usar para crear uno o más archivos de semáforo. Si lock-file no puede crear todos los archivos especificados (en el orden especificado), espera lateptime (predeterminado en 8) segundos y reintenta el último archivo que no tuvo éxito. Puede especificar la cantidad de reintentos que se deben realizar hasta que se devuelva el error. Si el número de reintentos es -1 (por defecto, es decir, -r-1) el archivo de bloqueo volverá a intentarlo para siempre.
Crear un archivo de bloqueo en una ubicación conocida y comprobar la existencia en el inicio del script? Poner el PID en el archivo puede ser útil si alguien intenta rastrear una instancia errante que impida la ejecución del script.
Hay una envoltura alrededor de la llamada al sistema flock (2) llamada, inimaginablemente, flock (1). Esto hace que sea relativamente fácil obtener bloqueos exclusivos sin preocuparse por la limpieza, etc. Hay ejemplos en la página de manual sobre cómo usarlo en un script de shell.
PID y archivos de bloqueo son definitivamente los más confiables. Cuando intenta ejecutar el programa, puede verificar el archivo de bloqueo, y si existe, puede usar ps
para ver si el proceso aún se está ejecutando. Si no es así, la secuencia de comandos puede comenzar, actualizando el PID en el archivo de bloqueo al suyo.
Para que el bloqueo sea confiable, necesita una operación atómica. Muchas de las propuestas anteriores no son atómicas. La utilidad propuesta de lockfile (1) parece prometedora ya que se menciona la página de manual, que es "resistente a NFS". Si su sistema operativo no es compatible con el archivo de bloqueo (1) y su solución tiene que funcionar en NFS, no tiene muchas opciones ....
NFSv2 tiene dos operaciones atómicas:
- enlace simbólico
- rebautizar
Con NFSv3 la llamada de creación también es atómica.
Las operaciones del directorio NO son atómicas en NFSv2 y NFSv3 (consulte el libro ''NFS Illustrated'' de Brent Callaghan, ISBN 0-201-32570-5; Brent es un veterano de NFS en Sun).
Sabiendo esto, puede implementar bloqueos giratorios para archivos y directorios (en shell, no en PHP):
bloquear el directorio actual:
while ! ln -s . lock; do :; done
bloquear un archivo:
while ! ln -s ${f} ${f}.lock; do :; done
Desbloquear el directorio actual (supuesto, el proceso de ejecución realmente adquirió el bloqueo):
mv lock deleteme && rm deleteme
desbloquear un archivo (supuesto, el proceso de ejecución realmente adquirió el bloqueo):
mv ${f}.lock ${f}.deleteme && rm ${f}.deleteme
Eliminar tampoco es atómico, por lo tanto, primero cambie el nombre (que es atómico) y luego elimine.
Para las llamadas simbólicas y de cambio de nombre, ambos nombres de archivo deben residir en el mismo sistema de archivos. Mi propuesta: use solo nombres de archivo simples (sin rutas) y coloque el archivo y el candado en el mismo directorio.
Cuando selecciono una máquina Debian, creo que el paquete lockfile-progs
es una buena solución. procmail
también viene con una herramienta de lockfile
. Sin embargo, a veces no estoy atrapado en ninguno de estos.
Aquí está mi solución que usa mkdir
para atomic-ness y un archivo PID para detectar bloqueos obsoletos. Este código está actualmente en producción en una configuración de Cygwin y funciona bien.
Para usarlo simplemente llame a exclusive_lock_require
cuando necesite acceso exclusivo a algo. Un parámetro de nombre de bloqueo opcional le permite compartir bloqueos entre diferentes scripts. También hay dos funciones de nivel inferior ( exclusive_lock_try
y exclusive_lock_retry
) si necesita algo más complejo.
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
}
Necesitas una operación atómica, como una bandada, de lo contrario esto eventualmente fallará.
Pero qué hacer si el rebaño no está disponible. Bueno, ahí está mkdir. Esa es una operación atómica también. Solo un proceso dará como resultado un mkdir exitoso, todos los demás fallarán.
Entonces el código es:
if mkdir /var/lock/.myscript.exclusivelock
then
# do stuff
:
rmdir /var/lock/.myscript.exclusivelock
fi
Debes ocuparte de los bloqueos obsoletos. De lo contrario, el script nunca volverá a ejecutarse.
Encuentro que la solución de bmdhack es la más práctica, al menos para mi caso de uso. Usando rebaño y LockFile confiar en la eliminación de la lockfile usando rm cuando el guión termina, lo que no siempre se puede garantizar (por ejemplo, matar -9).
Me gustaría cambiar una cosa de menor importancia sobre la solución de bmdhack: Se hace un punto de eliminar el archivo de bloqueo, sin que indica que esto no es necesario para el funcionamiento seguro de este semáforo. Su uso de kill -0 asegura que un viejo fichero de bloqueo para un proceso muerto simplemente se ignorará / sobre-escrita.
Por lo tanto, mi solución simplificada es simplemente añadir lo siguiente a la parte superior de su producto único:
## 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}
Por supuesto, este script todavía tiene el defecto de que los procesos que son propensos a comenzar al mismo tiempo tienen un riesgo de raza, como la prueba de bloqueo y las operaciones establecidas no son una sola acción atómica. Pero la solución propuesta para este lhunath por utilizar mkdir tiene el defecto de que un script muerto puede dejar atrás el directorio, evitando así que otros casos se ejecute.
Otra opción es usar la opción noclobber
de shell ejecutando el set -C
. Entonces >
fallará si el archivo ya existe.
En breve:
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
Esto hace que el shell llame:
open(pathname, O_CREAT|O_EXCL)
que atómicamente crea el archivo o falla si el archivo ya existe.
De acuerdo con un comentario sobre BashFAQ 045 , esto puede fallar en ksh88
, pero funciona en todos mis 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
Es interesante que pdksh
agregue la bandera O_TRUNC
, pero obviamente es redundante:
o bien estás creando un archivo vacío, o no estás haciendo nada.
Cómo lo hace depende de cómo quiere que se manejen las salidas no limpias.
Eliminar en la salida limpia
Las nuevas ejecuciones fallan hasta que se resuelva el problema que causó la salida impura y el archivo de bloqueo se elimina manualmente.
# acquire lock
# do work (code here may call exit, etc.)
rm "$lockfile"
Eliminar en cualquier salida
Las nuevas ejecuciones tienen éxito siempre que el script no se esté ejecutando.
trap ''rm "$lockfile"'' EXIT
Para los scripts de shell, tiendo a ir con el mkdir
sobre el flock
ya que hace que los bloqueos sean más portátiles.
De cualquier manera, usar set -e
no es suficiente. Eso solo sale del script si falla algún comando. Tus bloqueos aún se quedarán atrás.
Para una limpieza adecuada de la cerradura, realmente debe establecer sus trampas a algo así como este código psuedo (levantado, simplificado y no probado, pero a partir de scripts utilizados activamente):
#=======================================================================
# 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
Esto es lo que sucederá. Todas las trampas producirán una salida, por lo que siempre se producirá la función __sig_exit
(salvo un SIGKILL) que limpiará los bloqueos.
Nota: mis valores de salida no son valores bajos. ¿Por qué? Varios sistemas de procesamiento por lotes tienen o tienen expectativas de los números del 0 al 31. Al establecerlos en otra cosa, puedo hacer que mis scripts y secuencias de lotes reaccionen de acuerdo con el trabajo por lotes o la secuencia de comandos anteriores.
Aquí hay una implementación que usa un archivo de bloqueo y hace eco de un PID en él. Esto sirve como protección si el proceso se cancela antes de eliminar el archivo pid :
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}
El truco aquí es el kill -0
que no envía ninguna señal sino que simplemente verifica si existe un proceso con el PID dado. Además, la llamada a trap
asegurará que el archivo de bloqueo se elimine incluso cuando se mate el proceso (excepto kill -9
).
Utilizo un enfoque simple que maneja archivos de bloqueo obsoletos.
Tenga en cuenta que algunas de las soluciones anteriores que almacenan el pid ignoran el hecho de que el pid puede ajustarse. Entonces, solo verificar si hay un proceso válido con el pid almacenado no es suficiente, especialmente para scripts de larga ejecución.
Utilizo noclobber para asegurarme de que solo una secuencia de comandos pueda abrir y escribir en el archivo de bloqueo a la vez. Además, almaceno suficiente información para identificar de manera única un proceso en el archivo de bloqueo. Defino el conjunto de datos para identificar de manera única un proceso para que sea pid, ppid, lstart.
Cuando se inicia una nueva secuencia de comandos, si no puede crear el archivo de bloqueo, entonces verifica que el proceso que creó el archivo de bloqueo todavía se encuentre. De lo contrario, suponemos que el proceso original murió de una manera desafortunada y dejó un archivo de bloqueo obsoleto. El nuevo script toma posesión del archivo de bloqueo, y todo está bien en el mundo, nuevamente.
Debería trabajar con múltiples shells en múltiples plataformas. Rápido, portátil y 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
Basta con añadir [ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en "$0" "$0" "$@" || :
al comienzo de su script. Es un código repetitivo del hombre rebaño. Para darse cuenta de cómo funciona i escribió un script y ejecutarlo simultáneamente desde dos consolas:
#!/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"
No me he dado cuenta completamente cómo funciona, pero parece que funciona sola vez usando como un fichero de bloqueo. FLOCKER
ajustado a "$0"
sólo para establecer un valor notnull razonable. || :
no hacer nada si algo ha ido mal.
Parece que no funciona en Debian 7, pero parece funcionar de nuevo con 2,25 experimental paquete util-linux. Se escribe "rebaño: ... archivo de texto ocupado". Podría ser anulado mediante la desactivación de permiso de escritura en la secuencia de comandos.
Aquí hay un enfoque que combina el bloqueo de directorio atómico con una verificación de bloqueo obsoleto a través de PID y reiniciar si está obsoleto. Además, esto no se basa en ningún bashisms.
#!/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
Este ejemplo se explica en el rebaño del hombre, pero necesita algunas mejoras, porque debemos gestionar los errores y los códigos de salida:
#!/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
Puede usar otro método, enumerar procesos que utilicé en el pasado. Pero esto es más complicado que el método anterior. Debería enumerar los procesos por ps, filtrar por su nombre, grep de filtro adicional -v grep para eliminar el parásito nad, finalmente contarlo por grep -c. y compara con el número. Es complicado e incierto
Use flock(1)
para hacer un bloqueo de ámbito exclusivo a en el descriptor de archivo. De esta forma, puedes incluso sincronizar diferentes partes del guion.
#!/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
Esto garantiza que el código entre (
y )
se ejecute solo por un proceso a la vez y que el proceso no espere demasiado para un bloqueo.
Advertencia: este comando en particular es parte de util-linux
. Si ejecuta un sistema operativo que no sea Linux, puede o no estar disponible.
Todos los enfoques que prueban la existencia de "archivos de bloqueo" son defectuosos.
¿Por qué? Porque no hay forma de verificar si un archivo existe y crearlo en una sola acción atómica. Debido a esto; hay una condición de carrera que hará que tus intentos de exclusión mutua se rompan.
En cambio, necesitas usar mkdir
. mkdir
crea un directorio si todavía no existe y, si lo hace, establece un código de salida. Más importante aún, hace todo esto en una sola acción atómica que lo hace perfecto para este escenario.
if ! mkdir /tmp/myscript.lock 2>/dev/null; then
echo "Myscript is already running." >&2
exit 1
fi
Para todos los detalles, consulte el excelente BashFAQ: http://mywiki.wooledge.org/BashFAQ/045
Si desea cuidar los bloqueos obsoletos, el fusor (1) es útil. El único inconveniente aquí es que la operación dura aproximadamente un segundo, por lo que no es instantánea.
Aquí hay una función que escribí una vez que resuelve el problema usando 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
}
Puedes usarlo en un script como ese:
mutex /var/run/myscript.lock || { echo "Already running." >&2; exit 1; }
Si no le importa la portabilidad (estas soluciones deberían funcionar en prácticamente cualquier caja de UNIX), el fusor de Linux (1) ofrece algunas opciones adicionales y también hay rebajas (1) .
Los semafóricas usos de servicios públicos flock
(como se discutió anteriormente, por ejemplo por presto8) para implementar un semáforo de conteo . Se permite cualquier número específico de procesos concurrentes que desee. La usamos para limitar el nivel de concurrencia de diversos procesos de trabajo de cola.
Es como SEM , pero mucho menor peso. (La revelación completa: La escribí después de encontrar el SEM era demasiado pesada para nuestras necesidades y no había una utilidad simple conteo de semáforos disponibles.)
Muy rápido y realmente sucio? Este one-liner en la parte superior de su script funcionará:
[[ $(pgrep -c "`basename /"$0/"`") -gt 1 ]] && exit
Por supuesto, solo asegúrese de que el nombre de su script sea único. :)
Si las limitaciones de Flock, que ya se han descrito en otro lugar de este hilo, no son un problema para usted, entonces esto debería funcionar:
#!/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
Puede usar GNU Parallel
para esto, ya que funciona como un mutex cuando se llama como sem
. Entonces, en términos concretos, puedes usar:
sem --id SCRIPTSINGLETON yourScript
Si también quieres un tiempo de espera, utiliza:
sem --id SCRIPTSINGLETON --semaphoretimeout -10 yourScript
El tiempo de espera de <0 significa salir sin ejecutar el script si el semáforo no se libera dentro del tiempo de espera, el tiempo de espera de> 0 significa ejecutar el script de todos modos.
Tenga en cuenta que debe darle un nombre (con --id
) else se establece de manera predeterminada en el terminal de control.
GNU Parallel
es una instalación muy simple en la mayoría de las plataformas Linux / OSX / Unix, es solo un script Perl.
Aquí está una más elegante, a prueba de fallos, rápida y sucia método, que combina las respuestas proporcionadas anteriormente.
Uso
- incluir sh_lock_functions.sh
- init usando sh_lock_init
- bloquear el uso de sh_acquire_lock
- comprobar bloqueo con sh_check_lock
- desbloquear usando sh_remove_lock
Archivo de comandos
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
}
ejemplo de uso
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
Caracteristicas
- Utiliza una combinación de archivo, directorio y identificador de proceso para bloquear para asegurarse de que el proceso no se está ejecutando
- Puede detectar si el script se detuvo antes de la retirada de bloqueo (por ejemplo. Matar el proceso, apagado, error, etc.)
- Puede comprobar el archivo de bloqueo, y lo utilizan para desencadenar un proceso de apagado cuando la cerradura está faltante
- Detallado, da salida a los mensajes de error de depuración más fácil
¿Por qué no usamos algo parecido
pgrep -f $cmd || $cmd
if [ 1 -ne $(/bin/fuser "$0" 2>/dev/null | wc -w) ]; then
exit 1
fi
Un ejemplo con rebaño (1), pero sin subcapa. flock () ed archivo / tmp / foo no se elimina, pero eso no importa como se pone flock () y ONU-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
Las respuestas publicadas existentes o bien se basan en la utilidad CLI flock
o no asegurar correctamente el archivo de bloqueo. La utilidad rebaño no está disponible en todos los sistemas no Linux (es decir, FreeBSD), y no funciona correctamente en NFS.
En mis primeros días de la administración de sistemas y desarrollo de sistemas, me dijeron que un método seguro y relativamente portátil de crear un archivo de bloqueo era crear un archivo temporal utilizando mkemp(3)
o mkemp(1)
, escribir la identificación de información para el archivo temporal (es decir, PID), que acceder a la dura el archivo temporal para el archivo de bloqueo. Si el enlace se ha realizado correctamente, entonces usted ha obtenido con éxito la cerradura.
Cuando el uso de bloqueos en secuencias de comandos shell, normalmente de realizar un obtain_lock()
función en un perfil compartido y luego fuente que a partir de los scripts. A continuación se muestra un ejemplo de mi función de bloqueo:
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;
};
El siguiente es un ejemplo de cómo utilizar la función de bloqueo:
#!/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
Recuerde llamar clean_up
en cualquier punto de salida en la secuencia de comandos.
He utilizado el anterior, tanto en entornos Linux y FreeBSD.