Respostas:
Aqui está uma implementação que usa um arquivo de bloqueio e faz eco de um PID nele. Isso serve como uma proteção se o processo for morto antes de remover o pidfile :
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}
O truque aqui é o kill -0
que não entrega nenhum sinal, mas apenas verifica se existe um processo com o PID especificado. Além disso, a chamada para trap
garantirá que o arquivo de bloqueio seja removido mesmo quando seu processo for encerrado (exceto kill -9
).
Use flock(1)
para tornar um bloqueio de escopo exclusivo um descritor de arquivo. Dessa forma, você pode até sincronizar diferentes partes do 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
Isso garante que o código entre (
e )
seja executado apenas por um processo por vez e que o processo não espere muito tempo por um bloqueio.
Advertência: este comando em particular faz parte util-linux
. Se você executar um sistema operacional diferente do Linux, ele pode ou não estar disponível.
( command A ) command B
chama um subshell para command A
. Documentado em tldp.org/LDP/abs/html/subshells.html . Eu ainda não estou certo sobre o momento da invocação do subshell e comando B.
if flock -x -w 10 200; then ...Do stuff...; else echo "Failed to lock file" 1>&2; fi
para que, se ocorrer o tempo limite (algum outro processo tiver o arquivo bloqueado), esse script não avance e modifique o arquivo. Provavelmente ... o contra-argumento é 'mas se demorou 10 segundos e o bloqueio ainda não está disponível, nunca estará disponível', provavelmente porque o processo que contém o bloqueio não está terminando (talvez esteja sendo executado sob um depurador?).
exit
é da parte dentro do (
)
. Quando o subprocesso termina, o bloqueio é liberado automaticamente, porque não há processo mantendo-o.
Todas as abordagens que testam a existência de "arquivos de bloqueio" são falhas.
Por quê? Porque não há como verificar se existe um arquivo e criá-lo em uma única ação atômica. Por causa disso; há uma condição de corrida que vai fazer as suas tentativas de quebra de exclusão mútua.
Em vez disso, você precisa usar mkdir
. mkdir
cria um diretório se ele ainda não existe e, se existir, define um código de saída. Mais importante, ele faz tudo isso em uma única ação atômica, tornando-o perfeito para esse cenário.
if ! mkdir /tmp/myscript.lock 2>/dev/null; then
echo "Myscript is already running." >&2
exit 1
fi
Para todos os detalhes, consulte o excelente BashFAQ: http://mywiki.wooledge.org/BashFAQ/045
Se você deseja cuidar de bloqueios obsoletos, o fusor (1) é útil. A única desvantagem aqui é que a operação leva cerca de um segundo, portanto não é instantânea.
Aqui está uma função que escrevi uma vez que resolve o problema usando o 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
}
Você pode usá-lo em um script como este:
mutex /var/run/myscript.lock || { echo "Already running." >&2; exit 1; }
Se você não se importa com portabilidade (essas soluções devem funcionar com praticamente qualquer caixa UNIX), o fusor do Linux (1) oferece algumas opções adicionais e também há o flock (1) .
if ! mkdir
peça com a verificação de se o processo com o PID armazenado (na inicialização bem-sucedida) dentro do lockdir está realmente em execução e é idêntico ao script para proteção dos stalenes. Isso também protegeria contra a reutilização do PID após uma reinicialização e nem sequer exigiria fuser
.
mkdir
não está definido como uma operação atômica e, como tal, "efeito colateral" é um detalhe de implementação do sistema de arquivos. Eu acredito plenamente nele, se ele disser que o NFS não o implementa de maneira atômica. Embora eu não suspeite que você /tmp
seja um compartilhamento NFS e provavelmente será fornecido por um fs que é implementado mkdir
atomicamente.
ln
para criar um link físico a partir de outro arquivo. Se você tiver sistemas de arquivos estranhos que não garantem isso, poderá verificar o inode do novo arquivo posteriormente para ver se é o mesmo que o arquivo original.
open(... O_CREAT|O_EXCL)
. Você só precisa de um programa de usuário adequado, como lockfile-create
(in lockfile-progs
) ou dotlockfile
(in liblockfile-bin
). E certifique-se de limpar corretamente (por exemplo trap EXIT
) ou testar bloqueios obsoletos (por exemplo, com --use-pid
).
Há um invólucro ao redor da chamada do sistema flock (2) chamado, sem imaginação, flock (1). Isso torna relativamente fácil obter bloqueios exclusivos de maneira confiável, sem se preocupar com a limpeza etc. Existem exemplos na página do manual sobre como usá-lo em um script de shell.
flock()
chamada do sistema não é POSIX e não funciona para arquivos em montagens NFS.
flock -x -n %lock file% -c "%command%"
para garantir que apenas uma instância esteja em execução.
Você precisa de uma operação atômica, como o rebanho, caso contrário, isso acabará por falhar.
Mas o que fazer se o rebanho não estiver disponível. Bem, há mkdir. Essa é uma operação atômica também. Apenas um processo resultará em um mkdir bem-sucedido, todos os outros falharão.
Então o código é:
if mkdir /var/lock/.myscript.exclusivelock
then
# do stuff
:
rmdir /var/lock/.myscript.exclusivelock
fi
Você precisa cuidar dos bloqueios obsoletos após uma falha, seu script nunca será executado novamente.
sleep 10
antes rmdir
e tente cascatear novamente - nada "vazará".
Para tornar o bloqueio confiável, você precisa de uma operação atômica. Muitas das propostas acima não são atômicas. O utilitário lockfile (1) proposto parece promissor, como mencionado na página de manual, que é "resistente ao NFS". Se o seu sistema operacional não suporta o arquivo de bloqueio (1) e sua solução precisa funcionar no NFS, você não tem muitas opções ....
O NFSv2 possui duas operações atômicas:
Com o NFSv3, a chamada de criação também é atômica.
As operações de diretório NÃO são atômicas nos NFSv2 e NFSv3 (consulte o livro 'NFS Illustrated' de Brent Callaghan, ISBN 0-201-32570-5; Brent é um veterano do NFS na Sun).
Sabendo disso, você pode implementar bloqueios de rotação para arquivos e diretórios (no shell, não no PHP):
dir dir atual:
while ! ln -s . lock; do :; done
bloquear um arquivo:
while ! ln -s ${f} ${f}.lock; do :; done
desbloquear dir atual (suposição, o processo em execução realmente adquiriu o bloqueio):
mv lock deleteme && rm deleteme
desbloquear um arquivo (suposição, o processo em execução realmente adquiriu o bloqueio):
mv ${f}.lock ${f}.deleteme && rm ${f}.deleteme
Remover também não é atômico; portanto, primeiro renomeie (que é atômico) e depois remova.
Para as chamadas de link simbólico e renomeação, os dois nomes de arquivos devem residir no mesmo sistema de arquivos. Minha proposta: use apenas nomes de arquivos simples (sem caminhos) e coloque o arquivo e bloqueie no mesmo diretório.
lockfile
se disponível, ou fallback para esse symlink
método, se não.
mv
, rm
), deve rm -f
ser usado, e não rm
no caso de dois processos P1, P2 estarem correndo? Por exemplo, P1 começa a desbloquear com mv
, em seguida, P2 bloqueia e, em seguida, P2 desbloqueia (ambos mv
e rm
), finalmente P1 tenta rm
e falha.
$$
no ${f}.deleteme
nome do arquivo.
Outra opção é usar a noclobber
opção do shell executando set -C
. Então >
falhará se o arquivo já existir.
Em resumo:
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
Isso faz com que o shell chame:
open(pathname, O_CREAT|O_EXCL)
que cria atomicamente o arquivo ou falha se o arquivo já existe.
De acordo com um comentário no BashFAQ 045 , isso pode falhar ksh88
, mas funciona em todas as minhas conchas:
$ 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
Interessante que pdksh
adiciona a O_TRUNC
flag, mas obviamente é redundante:
você está criando um arquivo vazio ou não está fazendo nada.
Como você faz isso rm
depende de como você deseja que saídas impuras sejam manipuladas.
Excluir na saída limpa
Novas execuções falham até que o problema que causou a saída impura seja resolvido e o arquivo de bloqueio seja removido manualmente.
# acquire lock
# do work (code here may call exit, etc.)
rm "$lockfile"
Excluir em qualquer saída
Novas execuções são bem-sucedidas, desde que o script ainda não esteja em execução.
trap 'rm "$lockfile"' EXIT
Você pode usar GNU Parallel
isso, pois ele funciona como um mutex quando chamado como sem
. Portanto, em termos concretos, você pode usar:
sem --id SCRIPTSINGLETON yourScript
Se você deseja um tempo limite também, use:
sem --id SCRIPTSINGLETON --semaphoretimeout -10 yourScript
Tempo limite de <0 significa sair sem executar o script se o semáforo não for liberado dentro do tempo limite, tempo limite de> 0 significa executar o script de qualquer maneira.
Observe que você deve dar um nome (com --id
) ou o padrão é o terminal de controle.
GNU Parallel
é uma instalação muito simples na maioria das plataformas Linux / OSX / Unix - é apenas um script Perl.
sem
a questão relacionada unix.stackexchange.com/a/322200/199525 .
Para scripts de shell, costumo usar o mkdir
over flock
, pois torna os bloqueios mais portáteis.
De qualquer maneira, usar set -e
não é suficiente. Isso sai do script apenas se algum comando falhar. Seus bloqueios ainda serão deixados para trás.
Para uma limpeza adequada dos bloqueios, você realmente deve definir seus traps para algo como este código psuedo (elevado, simplificado e não testado, mas com scripts usados ativamente):
#=======================================================================
# 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
Aqui está o que vai acontecer. Todas as armadilhas produzirão uma saída, para que a função __sig_exit
sempre aconteça (com exceção de um SIGKILL), que limpa seus bloqueios.
Nota: meus valores de saída não são baixos. Por quê? Vários sistemas de processamento em lote atendem ou têm expectativas dos números de 0 a 31. Configurando-os para outra coisa, posso fazer com que meus scripts e fluxos de lote reajam de acordo com o trabalho ou script em lote anterior.
rm -r $LOCK_DIR
ou até forçá-los conforme necessário (como fiz também em casos especiais, como manter arquivos de rascunho relativos). Felicidades.
exit 1002
?
Muito rápido e muito sujo? Esta linha única na parte superior do seu script funcionará:
[[ $(pgrep -c "`basename \"$0\"`") -gt 1 ]] && exit
Obviamente, apenas verifique se o nome do seu script é único. :)
-gt 2
? O grep nem sempre se encontra no resultado do ps!
pgrep
não está no POSIX. Se você deseja que isso funcione de maneira portável, é necessário o POSIX ps
e processar sua saída.
-c
não existe, você terá que usar | wc -l
. Sobre a comparação de números: -gt 1
está marcado desde que a primeira instância se vê.
Aqui está uma abordagem que combina o bloqueio de diretório atômico com uma verificação de bloqueio obsoleto via PID e reinicia se obsoleto. Além disso, isso não depende de nenhum 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
Este exemplo é explicado no man rebanho, mas precisa de alguns aprimoramentos, porque devemos gerenciar bugs e códigos de saída:
#!/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
Você pode usar outro método, listar processos que eu usei no passado. Mas isso é mais complicado que o método acima. Você deve listar os processos por ps, filtrar por seu nome, filtro adicional grep -v grep para remover o parasita e, finalmente, contá-lo por grep -c. e compare com o número. É complicado e incerto
As respostas existentes publicadas dependem do utilitário CLI flock
ou não protegem adequadamente o arquivo de bloqueio. O utilitário flock não está disponível em todos os sistemas não Linux (por exemplo, FreeBSD) e não funciona corretamente no NFS.
Nos meus primeiros dias de administração e desenvolvimento do sistema, me disseram que um método seguro e relativamente portátil de criar um arquivo de bloqueio era criar um arquivo temporário usando mkemp(3)
ou mkemp(1)
, gravando informações de identificação no arquivo temporário (ou seja, PID) e, em seguida, vinculando o hardware o arquivo temporário para o arquivo de bloqueio. Se o link foi bem-sucedido, você obteve o bloqueio com êxito.
Ao usar bloqueios em scripts de shell, normalmente coloco uma obtain_lock()
função em um perfil compartilhado e depois a origino dos scripts. Abaixo está um exemplo da minha função de bloqueio:
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;
};
A seguir, é apresentado um exemplo de como usar a função de bloqueio:
#!/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
Lembre-se de ligar clean_up
em qualquer ponto de saída do seu script.
Eu usei o acima em ambos os ambientes Linux e FreeBSD.
Ao direcionar uma máquina Debian, acho o lockfile-progs
pacote uma boa solução. procmail
também vem com uma lockfile
ferramenta. No entanto, às vezes eu estou preso com nenhum deles.
Aqui está minha solução que usa mkdir
atômica e um arquivo PID para detectar bloqueios obsoletos. Atualmente, esse código está em produção em uma configuração do Cygwin e funciona bem.
Para usá-lo, basta ligar exclusive_lock_require
quando precisar obter acesso exclusivo a algo. Um parâmetro opcional de nome de bloqueio permite compartilhar bloqueios entre diferentes scripts. Há também duas funções de nível inferior ( exclusive_lock_try
e exclusive_lock_retry
), caso você precise de algo mais complexo.
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
}
Se as limitações do rebanho, que já foram descritas em outras partes deste segmento, não são um problema para você, isso deve 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
-n
irá exit 1
imediatamente se não puder obter o bloqueio
Alguns unixes possuem lockfile
muito semelhante ao já mencionado flock
.
Na página de manual:
O lockfile pode ser usado para criar um ou mais arquivos de semáforo. Se o arquivo de bloqueio não puder criar todos os arquivos especificados (na ordem especificada), ele aguardará o tempo de suspensão (o padrão é 8) segundos e tentará novamente o último arquivo que não teve êxito. Você pode especificar o número de novas tentativas até que a falha seja retornada. Se o número de tentativas for -1 (padrão, ou seja, -r-1), o arquivo de bloqueio tentará novamente para sempre.
lockfile
utilitário?
lockfile
é distribuído com procmail
. Também há uma alternativa dotlockfile
que acompanha o liblockfile
pacote. Ambos afirmam trabalhar de maneira confiável no NFS.
Na verdade, embora a resposta do bmdhacks seja quase boa, há uma pequena chance de o segundo script ser executado depois de verificar primeiro o arquivo de bloqueio e antes de escrevê-lo. Então, ambos escreverão o arquivo de bloqueio e estarão executando. Aqui está como fazê-lo funcionar com certeza:
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
A noclobber
opção garantirá que o comando de redirecionamento falhe se o arquivo já existir. Portanto, o comando de redirecionamento é realmente atômico - você escreve e verifica o arquivo com um comando. Você não precisa remover o arquivo de bloqueio no final do arquivo - ele será removido pela armadilha. Espero que isso ajude as pessoas que o lerão mais tarde.
PS: Não vi que Mikel já respondesse a pergunta corretamente, embora ele não incluísse o comando trap para reduzir a chance de o arquivo de bloqueio ser deixado após a interrupção do script com Ctrl-C, por exemplo. Portanto, esta é a solução completa
Eu queria acabar com arquivos de bloqueio, lockdirs, programas especiais de bloqueio e, mesmo pidof
que não sejam encontrados em todas as instalações do Linux. Também queria ter o código mais simples possível (ou pelo menos o menor número possível de linhas). if
Declaração mais simples , em uma linha:
if [[ $(ps axf | awk -v pid=$$ '$1!=pid && $6~/'$(basename $0)'/{print $1}') ]]; then echo "Already running"; exit; fi
/bin/ps -a --format pid,cmd | awk -v pid=$$ '/'$(basename $0)'/ { if ($1!=pid) print $1; }'
Eu uso uma abordagem simples que lida com arquivos de bloqueio obsoletos.
Observe que algumas das soluções acima que armazenam o pid, ignoram o fato de que o pid pode ser contornado. Portanto, apenas verificar se existe um processo válido com o pid armazenado não é suficiente, especialmente para scripts de execução longa.
Eu uso o noclobber para garantir que apenas um script possa abrir e gravar no arquivo de bloqueio de uma vez. Além disso, armazeno informações suficientes para identificar exclusivamente um processo no arquivo de bloqueio. Defino o conjunto de dados para identificar exclusivamente um processo a ser pid, ppid, lstart.
Quando um novo script é iniciado, se não conseguir criar o arquivo de bloqueio, ele verifica se o processo que criou o arquivo de bloqueio ainda está por aí. Caso contrário, presumimos que o processo original tenha sofrido uma morte sem graça e deixamos um arquivo de bloqueio obsoleto. O novo script assume a propriedade do arquivo de bloqueio e tudo está bem no mundo novamente.
Deve funcionar com vários shells em várias plataformas. Rápido, portátil e simples.
#!/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
Adicione esta linha no início do seu script
[ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en "$0" "$0" "$@" || :
É um código padrão do man rebanho.
Se você quiser mais 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."
Isso define e verifica bloqueios usando flock
utilitário. Esse código detecta se foi executado pela primeira vez verificando a variável FLOCKER, se não estiver definido como o nome do script, tenta iniciar o script novamente recursivamente usando flock e com a variável FLOCKER inicializada, se FLOCKER estiver definido corretamente, e em seguida na iteração anterior foi bem-sucedido e não há problema em prosseguir. Se o bloqueio estiver ocupado, ele falhará com o código de saída configurável.
Parece não funcionar no Debian 7, mas parece funcionar novamente com o pacote experimental util-linux 2.25. Ele escreve "rebanho: ... Arquivo de texto ocupado". Isso pode ser substituído desativando a permissão de gravação no seu script.
PID e arquivos de bloqueio são definitivamente os mais confiáveis. Quando você tenta executar o programa, ele pode verificar o arquivo de bloqueio que, se existir, pode ser usado ps
para verificar se o processo ainda está em execução. Caso contrário, o script pode iniciar, atualizando o PID no arquivo de bloqueio para seu próprio.
Acho que a solução do bmdhack é a mais prática, pelo menos para o meu caso de uso. O uso de flock e lockfile depende da remoção do arquivo de bloqueio usando rm quando o script termina, o que nem sempre pode ser garantido (por exemplo, kill -9).
Eu mudaria um pouco da solução do bmdhack: faz questão de remover o arquivo de bloqueio, sem afirmar que isso é desnecessário para o trabalho seguro desse semáforo. Seu uso de kill -0 garante que um arquivo de bloqueio antigo para um processo morto seja simplesmente ignorado / sobrescrito.
Minha solução simplificada é, portanto, simplesmente adicionar o seguinte ao topo do seu 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}
Obviamente, esse script ainda tem a falha de que os processos que provavelmente iniciarão ao mesmo tempo apresentam um risco de corrida, pois o teste de bloqueio e as operações de conjunto não são uma ação atômica única. Mas a solução proposta pelo lhunath para usar o mkdir tem a falha que um script morto pode deixar para trás do diretório, impedindo a execução de outras instâncias.
O utilitário semáforo usa flock
(como discutido acima, por exemplo, presto8) para implementar um semáforo de contagem . Ele permite qualquer número específico de processos simultâneos que você deseja. Nós o usamos para limitar o nível de simultaneidade de vários processos do operador de fila.
É como sem, mas muito mais leve. (Divulgação completa: escrevi depois de descobrir que o sem era muito pesado para nossas necessidades e não havia um utilitário simples de semáforo de contagem disponível.)
Um exemplo com o flock (1), mas sem subshell. O arquivo flock () ed / tmp / foo nunca é removido, mas isso não importa, pois ele é flock () e 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
Já respondeu um milhão de vezes, mas de outra maneira, sem a necessidade de dependências 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 ele grava o PID atual ($$) no arquivo de bloqueio e na inicialização do script, verifica se um processo está sendo executado com o PID mais recente.
Usar o bloqueio do processo é muito mais forte e também cuida das saídas desagradáveis. lock_file é mantido aberto enquanto o processo estiver em execução. Ele será fechado (pelo shell) assim que o processo existir (mesmo que seja morto). Eu achei isso muito 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
Eu uso oneliner @ no início do 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
É bom ver a presença de processo na memória (não importa qual seja o status do processo); mas faz o trabalho para mim.
O caminho do rebanho é o caminho a percorrer. Pense no que acontece quando o script morre repentinamente. No caso do rebanho, você apenas perde o rebanho, mas isso não é um problema. Além disso, observe que um truque maléfico é atacar o próprio script ... mas isso obviamente permite que você corra a todo vapor em problemas de permissão.
Rapido e sujo?
#!/bin/sh
if [ -f sometempfile ]
echo "Already running... will now terminate."
exit
else
touch sometempfile
fi
..do what you want here..
rm sometempfile