bash - manualmente - Manera rápida y sucia de garantizar que solo se ejecute una instancia de un script de shell a la vez
ejecutar crontab manualmente (30)
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.
¿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?
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
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
).
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.
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
}
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
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
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 flock(1) cómo usarlo en un script de shell.
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.
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 , 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
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 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.
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 (suposición, el proceso en 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.
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.
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
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:
Si desea cuidar los bloqueos obsoletos, el fuser(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 fuser(1) Linux fuser(1) ofrece algunas opciones adicionales y también hay flock(1) .
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.
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
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. :)
An example with flock(1) but without subshell. flock()ed file /tmp/foo is never removed, but that doesn''t matter as it gets flock() and 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
Here is a more elegant, fail-safe, quick & dirty method, combining the answers provided above.
Uso
- include sh_lock_functions.sh
- init using sh_lock_init
- lock using sh_acquire_lock
- check lock using sh_check_lock
- unlock using sh_remove_lock
Script File
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
}
Usage example
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
- Uses a combination of file, directory and process id to lock to make sure that the process is not already running
- You can detect if the script stopped before lock removal (eg. process kill, shutdown, error etc.)
- You can check the lock file, and use it to trigger a process shutdown when the lock is missing
- Verbose, outputs error messages for easier debug
I find that bmdhack''s solution is the most practical, at least for my use case. Using flock and lockfile rely on removing the lockfile using rm when the script terminates, which can''t always be guaranteed (eg, kill -9).
I would change one minor thing about bmdhack''s solution: It makes a point of removing the lock file, without stating that this is unnecessary for the safe working of this semaphore. His use of kill -0 ensures that an old lockfile for a dead process will simply be ignored/over-written.
My simplified solution is therefore to simply add the following to the top of your 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}
Of course, this script still has the flaw that processes that are likely to start at the same time have a race hazard, as the lock test and set operations are not a single atomic action. But the proposed solution for this by lhunath to use mkdir has the flaw that a killed script may leave behind the directory, thus preventing other instances from running.
I wanted to do away with lockfiles, lockdirs, special locking programs and even pidof
since it isn''t found on all Linux installations. Also wanted to have the simplest code possible (or at least as few lines as possible). Simplest if
statement, in one line:
if [[ $(ps axf | awk -v pid=$$ ''$1!=pid && $6~/''$(basename $0)''/{print $1}'') ]]; then echo "Already running"; exit; fi
Simply add [ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en "$0" "$0" "$@" || :
at the beginning of your script. It''s a boilerplate code from man flock. To realize how it works i wrote a script and run it simultaneously from two consoles:
#!/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"
I have not fully realized how it works, but it seems it runs itself again using itself as a lockfile. FLOCKER
set to "$0"
just to set some notnull reasonable value. || :
to do nothing if something went wrong.
It seems to not work on Debian 7, but seems to work back again with experimental util-linux 2.25 package. It writes "flock: ... Text file busy". It could be overridden by disabling write permission on your script.
Take a look to FLOM (Free LOck Manager) http://sourceforge.net/projects/flom/ : you can synchronize commands and/or scripts using abstract resources that does not need lock files in a filesystem. You can synchronize commands running in different systems without a NAS (Network Attached Storage) like an NFS (Network File System) server.
Using the simplest use case, serializing "command1" and "command2" may be as easy as executing:
flom -- command1
y
flom -- command2
from two different shell scripts.
The semaphoric utility uses flock
(as discussed above, eg by presto8) to implement a counting semaphore . It enables any specific number of concurrent processes you want. We use it to limit the level of concurrency of various queue worker processes.
It''s like sem but much lighter-weight. (Full disclosure: I wrote it after finding the sem was way too heavy for our needs and there wasn''t a simple counting semaphore utility available.)
The existing answers posted either rely on the CLI utility flock
or do not properly secure the lock file. The flock utility is not available on all non-Linux systems (ie FreeBSD), and does not work properly on NFS.
In my early days of system administration and system development, I was told that a safe and relatively portable method of creating a lock file was to create a temp file using mkemp(3)
or mkemp(1)
, write identifying information to the temp file (ie PID), then hard link the temp file to the lock file. If the link was successful, then you have successfully obtained the lock.
When using locks in shell scripts, I typically place an obtain_lock()
function in a shared profile and then source it from the scripts. Below is an example of my lock function:
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;
};
The following is an example of how to use the lock function:
#!/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
Remember to call clean_up
at any exit points in your script.
I''ve used the above in both Linux and FreeBSD environments.
The flock path is the way to go. Think about what happens when the script suddenly dies. In the flock-case you just loose the flock, but that is not a problem. Also, note that an evil trick is to take a flock on the script itself .. but that of course lets you run full-steam-ahead into permission problems.
why dont we use something like
pgrep -f $cmd || $cmd
if [ 1 -ne $(/bin/fuser "$0" 2>/dev/null | wc -w) ]; then
exit 1
fi