Forma rápida y sucia de garantizar que solo se ejecute una instancia de un script de shell a la vez


Respuestas:


109

Aquí hay una implementación que usa un archivo de bloqueo y hace eco de un PID. Esto sirve como protección si el proceso se termina 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 -0que no entrega ninguna señal, sino que solo comprueba si existe un proceso con el PID dado. Además, la llamada a trapgarantizará que el archivo de bloqueo se elimine incluso cuando se finalice su proceso (excepto kill -9).


73
Como ya se mencionó en un comentario sobre otra respuesta, esto tiene un defecto fatal: si la otra secuencia de comandos se inicia entre la verificación y el eco, eres un brindis.
Paul Tomblin

1
El truco del enlace simbólico es bueno, pero si el propietario del archivo de bloqueo es kill -9'd o el sistema falla, todavía hay una condición de carrera para leer el enlace simbólico, observe que el propietario se fue y luego elimínelo. Me quedo con mi solución.
bmdhacks

9
La comprobación y creación atómica está disponible en el shell utilizando flock (1) o lockfile (1). Ver otras respuestas.
dmckee --- ex gatito moderador

3
Vea mi respuesta para una forma portátil de hacer una verificación atómica y crear sin tener que depender de utilidades como flock o lockfile.
lhunath

2
Esto no es atómico y, por lo tanto, es inútil. Necesita un mecanismo atómico para probar y configurar.
K Richard Pixley

214

Utilícelo flock(1)para hacer un bloqueo exclusivo con ámbito en un descriptor de archivo. De esta manera, incluso puede sincronizar diferentes partes del 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

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.


11
¿Qué es el 200? Dice "fd" en el manul, pero no sé qué significa eso.
chovy

44
@chovy "descriptor de archivo", un identificador entero que designa un archivo abierto.
Alex B

66
Si alguien más se pregunta: la sintaxis ( command A ) command Binvoca una subshell para command A. Documentado en tldp.org/LDP/abs/html/subshells.html . Todavía no estoy seguro sobre el momento de invocación de la subshell y el comando B.
Dr. Jan-Philip Gehrcke

1
Creo que el código dentro del subconjunto debería ser más parecido a: if flock -x -w 10 200; then ...Do stuff...; else echo "Failed to lock file" 1>&2; fisi el tiempo de espera se produce (algún otro proceso tiene el archivo bloqueado), este script no continúa y modifica el archivo. Probablemente ... el contraargumento es 'pero si ha tomado 10 segundos y el bloqueo aún no está disponible, nunca estará disponible', presumiblemente porque el proceso que mantiene el bloqueo no está finalizando (tal vez se está ejecutando bajo un depurador?).
Jonathan Leffler

1
El archivo redirigido a es solo una carpeta de lugar para que actúe el bloqueo, no hay datos significativos que entren en él. El exites de la parte dentro del ( ). Cuando finaliza el subproceso, el bloqueo se libera automáticamente, porque no hay ningún proceso que lo retenga.
clacke

158

Todos los enfoques que prueban la existencia de "archivos de bloqueo" son defectuosos.

¿Por qué? Porque no hay forma de verificar si existe un archivo y crearlo en una sola acción atómica. Debido a esto; hay una condición de carrera que SERÁ hacer sus intentos de rotura exclusión mutua.

En cambio, necesitas usar mkdir. mkdircrea un directorio si aún 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, vea 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 el fusor:

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

Puede usarlo en un script como este:

mutex /var/run/myscript.lock || { echo "Already running." >&2; exit 1; }

Si no le importa la portabilidad (estas soluciones deberían funcionar en casi cualquier caja UNIX), el fusor de Linux (1) ofrece algunas opciones adicionales y también hay un lote (1) .


1
Puede combinar la if ! mkdirparte con la comprobación de si el proceso con el PID almacenado (en el inicio exitoso) dentro del lockdir se está ejecutando y es idéntico al script para la protección de stalenes. Esto también protegería contra la reutilización del PID después de un reinicio y ni siquiera lo requeriría fuser.
Tobias Kienzler

44
Es cierto que mkdirno se define como una operación atómica y, como tal, el "efecto secundario" es un detalle de implementación del sistema de archivos. Le creo completamente si dice que NFS no lo implementa de manera atómica. Aunque no sospecho /tmpque será un recurso compartido de NFS y probablemente será proporcionado por un fs que se implementa mkdiratómicamente.
lhunath

55
Pero hay una manera de verificar la existencia de un archivo normal y crearlo atómicamente si no lo hace: usar lnpara crear un enlace duro desde otro archivo. Si tiene sistemas de archivos extraños que no garantizan eso, puede verificar el inodo del nuevo archivo luego para ver si es el mismo que el archivo original.
Juan Céspedes

44
No es 'una manera de comprobar si existe un archivo y crear en una sola acción atómica' - es open(... O_CREAT|O_EXCL). Solo necesita un programa de usuario adecuado para hacerlo, como lockfile-create(in lockfile-progs) o dotlockfile(in liblockfile-bin). Y asegúrese de limpiar correctamente (p trap EXIT. Ej. ), O pruebe si hay bloqueos rancios (p --use-pid. Ej. Con ).
Toby Speight

55
"Todos los enfoques que prueban la existencia de" archivos de bloqueo "son defectuosos. ¿Por qué? Porque no hay forma de verificar si existe un archivo y crearlo en una sola acción atómica". Para hacerlo atómico, debe hacerse en el nivel del kernel - y se hace a nivel del kernel con flock (1) linux.die.net/man/1/flock que parece tener la fecha de copyright del hombre desde al menos 2006. Así que hice un voto negativo (- 1), nada personal, solo tengo la firme convicción de que usar las herramientas implementadas por el kernel proporcionadas por los desarrolladores del kernel es correcto.
Craig Hicks

42

Hay un contenedor alrededor de la llamada al sistema flock (2) llamado, sin imaginación, flock (1). Esto hace que sea relativamente fácil obtener bloqueos exclusivos de manera confiable sin preocuparse por la limpieza, etc. Hay ejemplos en la página de manual sobre cómo usarlo en un script de shell.


3
La flock()llamada al sistema no es POSIX y no funciona para archivos en montajes NFS.
maxschlepzig

17
Ejecutando desde un trabajo Cron que uso flock -x -n %lock file% -c "%command%"para asegurarme de que solo se ejecute una instancia.
Ryall

Aww, en lugar de la bandada poco imaginativa (1) deberían haber ido con algo como la bandada (U). ... tiene algo de familiaridad. . Parece que he escuchado eso antes de una o dos veces.
Kent Kruckeberg

Es notable que la documentación de flock (2) especifica el uso solo con archivos, pero la documentación de flock (1) especifica el uso con archivos o directorios. La documentación de flock (1) no es explícita sobre cómo indicar la diferencia durante la creación, pero supongo que se hace agregando un "/" final. De todos modos, si flock (1) puede manejar directorios pero flock (2) no, entonces flock (1) no se implementa solo en flock (2).
Craig Hicks

27

Necesita una operación atómica, como una bandada, de lo contrario, eventualmente fallará.

Pero qué hacer si el lote no está disponible. Bueno, hay mkdir. Esa también es una operación atómica. 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

Debe cuidar los bloqueos obsoletos, de lo contrario, después de un bloqueo, su script nunca volverá a ejecutarse.


1
Ejecute esto varias veces al mismo tiempo (como "./a.sh & ./a.sh & ./a.sh & ./a.sh & ./a.sh & ./a.sh & ./a.sh & ") y la secuencia de comandos se filtrará varias veces.
Nippysaurus

77
@Nippysaurus: este método de bloqueo no tiene fugas. Lo que vio fue que la secuencia de comandos inicial terminaba antes de que se lanzaran todas las copias, por lo que otra pudo (correctamente) obtener el bloqueo. Para evitar este falso positivo, agregue un sleep 10antes rmdire intente volver a conectarse en cascada; nada se "escapará".
Sir Athos

Otras fuentes afirman que mkdir no es atómico en algunos sistemas de archivos como NFS. Y, por cierto, he visto ocasiones en las que, en NFS, mkdir recursivo concurrente conduce a errores a veces con trabajos de matriz jenkins. Así que estoy bastante seguro de que ese es el caso. Pero mkdir es bastante bueno para los casos de uso menos exigentes de la OMI.
akostadinov

Puede usar la opción Bash'es noclobber con archivos normales.
Palec

26

Para que el bloqueo sea confiable, necesita una operación atómica. Muchas de las propuestas anteriores no son atómicas. La utilidad lockfile (1) propuesta parece prometedora como la página de manual mencionada, que es "resistente a NFS". Si su sistema operativo no admite 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 de 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 spin-locks para archivos y directorios (en shell, no 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 (suposición, el proceso en ejecución realmente adquirió el bloqueo):

mv lock deleteme && rm deleteme

desbloquear un archivo (suposición, el proceso en 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 de enlace simbólico y cambio de nombre, ambos nombres de archivo deben residir en el mismo sistema de archivos. Mi propuesta: usar solo nombres de archivo simples (sin rutas) y colocar el archivo y bloquearlo en el mismo directorio.


¿Qué páginas de NFS Illustrated respaldan la afirmación de que mkdir no es atómico sobre NFS?
maxschlepzig

Gracias por esta técnica. Una implementación de shell mutex está disponible en mi nuevo shell lib: github.com/Offirmo/offirmo-shell-lib , vea "mutex". Utiliza lockfilesi está disponible, o recurre a este symlinkmétodo si no.
Offirmo

Agradable. Lamentablemente, este método no proporciona una forma de eliminar automáticamente bloqueos obsoletos.
Richard Hansen

Para el desbloqueo de dos etapas ( mv, rm), ¿ rm -fse debe usar, en lugar de rmen el caso de que dos procesos P1, P2 estén corriendo? Por ejemplo, P1 comienza a desbloquear con mv, luego P2 bloquea, luego P2 desbloquea (ambos mvy rm), finalmente P1 intenta rmy falla.
Matt Wallis

1
@MattWallis Ese último problema podría mitigarse fácilmente si se incluye $$en el ${f}.deletemenombre del archivo.
Stefan Majewsky

23

Otra opción es usar la noclobberopción de shell ejecutando 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 crea atómicamente el archivo o falla si el archivo ya existe.


Según un comentario en BashFAQ 045 , esto puede fallar 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 pdkshagregue la O_TRUNCbandera, pero obviamente es redundante:
o estás creando un archivo vacío o no estás haciendo nada.


Cómo lo haga rmdepende de cómo desee que se manejen las salidas impuras.

Eliminar al salir limpio

Las nuevas ejecuciones fallan hasta que se resuelve 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

Se ejecutan nuevas ejecuciones siempre que el script no se esté ejecutando.

trap 'rm "$lockfile"' EXIT

Enfoque muy novedoso ... esta parece ser una forma de lograr la atomicidad utilizando un archivo de bloqueo en lugar de un directorio de bloqueo.
Matt Caldwell

Buen enfoque. :-) En la trampa EXIT, debería restringir qué proceso puede limpiar el archivo de bloqueo. Por ejemplo: trap 'if [[$ (cat "$ lockfile") == "$$"]]; entonces rm "$ lockfile"; fi 'EXIT
Kevin Seifert

1
Los archivos de bloqueo no son atómicos sobre NFS. Es por eso que la gente se movió a usar directorios de bloqueo.
K Richard Pixley

20

Puede usar GNU Parallelesto ya que funciona como un mutex cuando se llama como sem. Entonces, en términos concretos, puede usar:

sem --id SCRIPTSINGLETON yourScript

Si también quieres un tiempo de espera, usa:

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), de lo contrario, el valor predeterminado es 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 de Perl.


Lástima que las personas sean reacias a rechazar las respuestas inútiles: esto lleva a que nuevas respuestas relevantes sean enterradas en una pila de basura.
Dmitry Grigoryev

44
Solo necesitamos muchos votos a favor. Esta es una respuesta tan ordenada y poco conocida. (¡Aunque para ser pedante, OP quería algo rápido y sucio mientras que esto es rápido y limpio!) Más información semen la pregunta relacionada unix.stackexchange.com/a/322200/199525 .
Parcialmente nublado

16

Para los scripts de shell, tiendo a ir con el mkdirover flockya que hace que los bloqueos sean más portátiles.

De cualquier manera, usar set -eno es suficiente. Eso solo sale del script si falla algún comando. Tus cerraduras aún se quedarán atrás.

Para una limpieza de bloqueo adecuada, realmente debe configurar sus trampas en algo como este código psuedo (levantado, simplificado y no probado pero de secuencias de comandos utilizadas 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 la función __sig_exitsiempre sucederá (salvo SIGKILL) que limpiará tus bloqueos.

Nota: mis valores de salida no son valores bajos. ¿Por qué? Varios sistemas de procesamiento por lotes crean o tienen expectativas de los números del 0 al 31. Al establecerlos en otra cosa, puedo hacer que mis secuencias de comandos y secuencias de lotes reaccionen de acuerdo con el trabajo o secuencia de comandos anterior.


2
Su guión es demasiado detallado, creo que podría haber sido mucho más corto, pero en general, sí, debe configurar trampas para hacerlo correctamente. También agregaría SIGHUP.
mojuba

Esto funciona bien, excepto que parece verificar $ LOCK_DIR mientras que elimina $ __ lockdir. ¿Tal vez debería sugerir que al eliminar el bloqueo hagas rm -r $ LOCK_DIR?
bevada

Gracias por la sugerencia. Lo anterior se levantó el código y se colocó en forma de código psuedo, por lo que necesitará ajustes según el uso de la gente. Sin embargo, deliberadamente fui con rmdir en mi caso, ya que rmdir elimina de forma segura los directorios solo si están vacíos. Si la gente está colocando recursos en ellos, como archivos PID, etc., deben alterar la limpieza de su cerradura para que sea más agresiva rm -r $LOCK_DIRo incluso forzarla según sea necesario (como también lo he hecho en casos especiales, como la retención de archivos de memoria virtual). Salud.
Mark Stinson

¿Has probado exit 1002?
Gilles Quenot

13

¿Muy rápido y muy sucio? Esta línea 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. :)


¿Cómo simulo esto para probarlo? ¿Hay alguna manera de iniciar un script dos veces en una línea y tal vez recibir una advertencia, si ya se está ejecutando?
rubo77

2
¡Esto no está funcionando en absoluto! ¿Por qué verificar -gt 2? grep no siempre se encuentra en el resultado de ps!
rubo77

pgrepno está en POSIX. Si desea que esto funcione de forma portátil, necesita POSIX psy procesar su salida.
Palec

En OSX -cno existe, tendrá que usar | wc -l. Acerca de la comparación de números: -gt 1se verifica ya que la primera instancia se ve a sí misma.
Benjamin Peter

6

Aquí hay un enfoque que combina el bloqueo de directorio atómico con una comprobación de bloqueo obsoleto a través de PID y reiniciar si está obsoleto. Además, esto no depende de ningún basismo.

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

¿Crear un archivo de bloqueo en una ubicación conocida y verificar la existencia al inicio del script? Poner el PID en el archivo puede ser útil si alguien intenta rastrear una instancia errónea que impide la ejecución del script.


5

Este ejemplo se explica en la bandada de hombres, pero necesita algunas mejoras, porque debemos manejar 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 los procesos que usé en el pasado. Pero esto es más complicado que el método anterior. Debe enumerar los procesos por ps, filtrar por su nombre, filtro adicional grep -v grep para eliminar el parásito y finalmente contarlo por grep -c. y compara con el número. Es complicado e incierto


1
Puede usar ln -s, porque esto puede crear un enlace simbólico solo cuando no exista ningún archivo o enlace simbólico, igual que mkdir. muchos procesos del sistema usaban enlaces simbólicos en el pasado, por ejemplo init o inetd. synlink mantiene la identificación del proceso, pero realmente no señala nada. Por los años este comportamiento fue cambiado. procesos utiliza bandadas y semáforos.
Znik

5

Las respuestas existentes publicadas dependen de la utilidad CLI flocko no protegen adecuadamente el archivo de bloqueo. La utilidad flock no está disponible en todos los sistemas que no son Linux (es decir, FreeBSD), y no funciona correctamente en NFS.

En mis primeros días de administración del sistema y desarrollo del sistema, me dijeron que un método seguro y relativamente portátil para crear un archivo de bloqueo era crear un archivo temporal utilizando mkemp(3)o mkemp(1), escribir información de identificación en el archivo temporal (es decir, PID), luego enlace duro el archivo temporal al archivo de bloqueo. Si el enlace fue exitoso, entonces ha obtenido con éxito el bloqueo.

Cuando uso bloqueos en scripts de shell, normalmente coloco una obtain_lock()función en un perfil compartido y luego la obtengo 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 usar 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_upa cualquier punto de salida en su secuencia de comandos.

He usado lo anterior en entornos Linux y FreeBSD.


4

Al apuntar a una máquina Debian, encuentro que el lockfile-progspaquete es una buena solución. procmailTambién viene con una lockfileherramienta. Sin embargo, a veces estoy atrapado con ninguno de estos.

Aquí está mi solución que usa mkdiratomic-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 exclusive_lock_requirecuando necesite obtener 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_tryy 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
}

Gracias, lo probé en cygwin y pasó pruebas simples.
ndemou

4

Si las limitaciones de la bandada, que ya se han descrito en otra parte 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 

3
Solo pensé en señalar que -x (bloqueo de escritura) ya está configurado de forma predeterminada.
Keldon Alleyne

y -nserá exit 1de inmediato si no puede obtener el bloqueo
Anentropic

Gracias @ KeldonAlleyne, actualicé el código para eliminar "-x" ya que es el predeterminado.
presto8

3

Algunos tienen Unixes lockfileque es muy similar a los ya mencionados flock.

Desde la página del manual:

lockfile se puede usar para crear uno o más archivos de semáforos. Si el archivo de bloqueo no puede crear todos los archivos especificados (en el orden especificado), espera el tiempo de sueño (el valor predeterminado es 8) segundos y vuelve a intentar el último archivo que no tuvo éxito. Puede especificar el número de reintentos a realizar hasta que se devuelva el error. Si el número de reintentos es -1 (predeterminado, es decir, -r-1), lockfile volverá a intentarlo para siempre.


¿Cómo conseguimos la lockfileutilidad?
Offirmo

lockfileSe distribuye con procmail. También hay una alternativa dotlockfileque va con el liblockfilepaquete. Ambos afirman que funcionan de manera confiable en NFS.
Mr. Deathless el

3

En realidad, aunque la respuesta de bmdhacks es casi buena, existe una pequeña posibilidad de que el segundo script se ejecute después de comprobar primero el archivo de bloqueo y antes de escribirlo. 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 noclobberopción asegurará que el comando de redireccionamiento fallará si el archivo ya existe. Entonces, el comando de redirección es en realidad atómico: usted escribe y verifica el archivo con un comando. No necesita eliminar el archivo de bloqueo al final del archivo: la trampa lo eliminará. Espero que esto ayude a las personas que lo leerán más tarde.

PD: No vi que Mikel ya respondió la pregunta correctamente, aunque no incluyó el comando trap para reducir la posibilidad de que quede el archivo de bloqueo después de detener el script con Ctrl-C, por ejemplo. Entonces esta es la solución completa


3

Quería eliminar los archivos de bloqueo, los lockdirs, los programas de bloqueo especiales e incluso pidofporque no se encuentra en todas las instalaciones de Linux. También quería tener el código más simple posible (o al menos la menor cantidad de líneas posible). ifDeclaración más simple , en una línea:

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

1
Esto es sensible a la salida 'ps', en mi máquina (Ubuntu 14.04, / bin / ps de procps-ng versión 3.3.9) el comando 'ps axf' imprime caracteres de árbol ascii que interrumpen los números de campo. Esto funcionó para mí: /bin/ps -a --format pid,cmd | awk -v pid=$$ '/'$(basename $0)'/ { if ($1!=pid) print $1; }'
qneill

2

Utilizo un enfoque simple que maneja los 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, simplemente verificar si hay un proceso válido con el pid almacenado no es suficiente, especialmente para los scripts de larga ejecución.

Uso noclobber para asegurarme de que solo se pueda abrir un script y escribir en el archivo de bloqueo a la vez. Además, almaceno suficiente información para identificar de forma exclusiva un proceso en el archivo de bloqueo. Defino el conjunto de datos para identificar unívocamente un proceso para que sea pid, ppid, lstart.

Cuando se inicia una nueva secuencia de comandos, si no puede crear el archivo de bloqueo, verifica que el proceso que creó el archivo de bloqueo todavía está presente. Si no, asumimos que el proceso original tuvo una muerte sin gracia y dejó un archivo de bloqueo obsoleto. El nuevo script toma posesión del archivo de bloqueo, y todo vuelve a estar bien en el mundo.

Debería funcionar 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

Te di +1 para una solución diferente. Aunque tampoco funciona en AIX (> ps -eo pid, ppid, lstart $$ | tail -1 ps: lista no válida con -o.) No HP-UX (> ps -eo pid, ppid, lstart $$ | cola -1 ps: opción ilegal - o). Gracias.
Tagar

2

Agregue esta línea al comienzo de su secuencia de comandos

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

Es un código repetitivo de Man Flock.

Si desea más registros, use este

[ "${FLOCKER}" != "$0" ] && { echo "Trying to start build from queue... "; exec bash -c "FLOCKER='$0' flock -E $E_LOCKED -en '$0' '$0' '$@' || if [ \"\$?\" -eq $E_LOCKED ]; then echo 'Locked.'; fi"; } || echo "Lock is free. Completing."

Esto establece y verifica los bloqueos utilizando la flockutilidad. Este código detecta si se ejecutó por primera vez comprobando la variable FLOCKER, si no está configurado con el nombre del script, luego intenta iniciar el script nuevamente de forma recursiva usando flock y con la variable FLOCKER inicializada, si FLOCKER está configurado correctamente, luego flock en la iteración anterior tuvo éxito y está bien proceder. Si el bloqueo está ocupado, falla con el código de salida configurable.

Parece que no funciona en Debian 7, pero parece funcionar de nuevo con el paquete experimental util-linux 2.25. Escribe "rebaño: ... Archivo de texto ocupado". Podría anularse deshabilitando el permiso de escritura en su secuencia de comandos.


1

PID y archivos de bloqueo son definitivamente los más confiables. Cuando intenta ejecutar el programa, puede verificar el archivo de bloqueo que, y si existe, puede usar pspara ver si el proceso aún se está ejecutando. Si no es así, el script puede comenzar, actualizando el PID en el archivo de bloqueo a su propio.


1

Encuentro que la solución de bmdhack es la más práctica, al menos para mi caso de uso. El uso de flock y lockfile se basa en eliminar el lockfile usando rm cuando finaliza el script, lo que no siempre se puede garantizar (por ejemplo, kill -9).

Cambiaría una cosa menor acerca de la solución de bmdhack: se encarga de eliminar el archivo de bloqueo, sin afirmar que esto es innecesario para el funcionamiento seguro de este semáforo. Su uso de kill -0 asegura que un antiguo archivo de bloqueo para un proceso muerto simplemente se ignorará / se sobrescribirá.

Por lo tanto, mi solución simplificada es simplemente agregar lo siguiente a la parte superior de su 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}

Por supuesto, este script todavía tiene la falla de que los procesos que probablemente comiencen al mismo tiempo tienen un riesgo de carrera, ya que la prueba de bloqueo y las operaciones de configuración no son una sola acción atómica. Pero la solución propuesta para esto por parte de lhunath de usar mkdir tiene la falla de que una secuencia de comandos eliminada puede dejar atrás el directorio, evitando así que se ejecuten otras instancias.


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 . Permite cualquier número específico de procesos concurrentes que desee. Lo usamos para limitar el nivel de concurrencia de varios procesos de trabajo en cola.

Es como sem pero mucho más ligero. (Divulgación completa: lo escribí después de encontrar que el sem era demasiado pesado para nuestras necesidades y no había una simple utilidad de conteo de semáforos disponible).


1

Un ejemplo con flock (1) pero sin subshell. El archivo flock () ed / tmp / foo nunca se elimina, pero eso no importa, ya que obtiene flock () y 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

Respondió un millón de veces ya, pero de otra manera, sin la necesidad de dependencias externas:

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

Cada vez que escribe el PID actual ($$) en el archivo de bloqueo y al iniciar el script, comprueba si un proceso se está ejecutando con el último PID.


1
Sin la llamada de captura (o al menos una limpieza cerca del final para el caso normal), tiene el error de falso positivo donde se deja el archivo de bloqueo después de la última ejecución y el PID ha sido reutilizado por otro proceso más tarde. (Y en el peor de los casos, ha sido dotado para un proceso de larga duración como apache ...)
Philippe Chaintreuil

1
Estoy de acuerdo, mi enfoque es defectuoso, necesita una trampa. He actualizado mi solución. Todavía prefiero no tener dependencias externas.
Filidor Wiese

1

Usar el bloqueo del proceso es mucho más fuerte y también se ocupa de las salidas ingratas. lock_file se mantiene abierto mientras se ejecuta el proceso. Se cerrará (por shell) una vez que el proceso exista (incluso si se mata). Encontré que esto es muy eficiente:

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

Yo uso oneliner @ al comienzo del script:

#!/bin/bash

if [[ $(pgrep -afc "$(basename "$0")") -gt "1" ]]; then echo "Another instance of "$0" has already been started!" && exit; fi
.
the_beginning_of_actual_script

Es bueno ver la presencia de proceso en la memoria (no importa cuál sea el estado del proceso); Pero hace el trabajo por mí.


0

El camino del rebaño es el camino a seguir. Piensa en lo que sucede cuando el guión muere repentinamente. En el caso de la bandada, simplemente se pierde la bandada, pero eso no es un problema. Además, tenga en cuenta que un truco malvado es tomar una bandada en el script en sí mismo ... pero eso, por supuesto, le permite correr a toda máquina en problemas de permisos.


0

¿Rápido y sucio?

#!/bin/sh

if [ -f sometempfile ]
  echo "Already running... will now terminate."
  exit
else
  touch sometempfile
fi

..do what you want here..

rm sometempfile

77
Esto puede o no ser un problema, dependiendo de cómo se use, pero hay una condición de carrera entre probar el bloqueo y crearlo, de modo que se puedan iniciar dos scripts al mismo tiempo. Si uno termina primero, el otro permanecerá ejecutándose sin ningún archivo de bloqueo.
TimB

3
C News, que me enseñó mucho sobre las secuencias de comandos de shell portátiles, solía hacer un archivo lock. $$ y luego intentaba vincularlo con "lock"; si el enlace se realizó correctamente, tenía el bloqueo; de lo contrario, eliminó el bloqueo. $$ y salió
Paul Tomblin

Esa es una muy buena manera de hacerlo, excepto que aún sufres la necesidad de eliminar el archivo de bloqueo manualmente si algo sale mal y el archivo de bloqueo no se elimina.
Matthew Scharley

2
Rápido y sucio, eso es lo que pidió :)
Aupajo
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.