Maneira rápida e suja de garantir que apenas uma instância de um shell script esteja em execução por vez

179

Qual é uma maneira rápida e suja de garantir que apenas uma instância de um shell script esteja sendo executada em um determinado momento?

raldi
fonte
Por favor, digite seu
mail do

Respostas:

109

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 -0que não entrega nenhum sinal, mas apenas verifica se existe um processo com o PID especificado. Além disso, a chamada para trapgarantirá que o arquivo de bloqueio seja removido mesmo quando seu processo for encerrado (exceto kill -9).

bmdhacks
fonte
73
Como já mencionado em um comentário sobre outra resposta, isso tem uma falha fatal - se o outro script iniciar entre a verificação e o eco, você estará brindando.
Paul Tomblin
1
O truque do link simbólico é legal, mas se o proprietário do arquivo de bloqueio for morto -9'd ou o sistema travar, ainda haverá uma condição de corrida para ler o link simbólico, observe que o proprietário se foi e exclua-o. Estou aderindo à minha solução.
9338 bmdhacks
9
A verificação atômica e a criação estão disponíveis no shell usando flock (1) ou lockfile (1). Veja outras respostas.
dmckee --- ex-moderador gatinho
3
Veja minha resposta para uma maneira portátil de fazer uma verificação atômica e criar sem precisar confiar em utilitários como flock ou lockfile.
Lhunath 08/04/09
2
Isso não é atômico e, portanto, é inútil. Você precisa de um mecanismo atômico para testar e definir.
K Richard Pixley
214

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.

Alex B
fonte
11
Qual é o 200? Diz "fd" no manul, mas não sei o que isso significa.
chovy
4
@chovy "file descriptor", um identificador inteiro que designa um arquivo aberto.
Alex B
6
Se alguém mais estiver se perguntando: a sintaxe ( command A ) command Bchama 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.
Dr. Jan-Philip Gehrcke
1
Eu acho que o código dentro do sub-shell deve ser mais parecido: if flock -x -w 10 200; then ...Do stuff...; else echo "Failed to lock file" 1>&2; fipara 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?).
9788 Jonathan-Leffler1
1
O arquivo para o qual o redirecionado é apenas um dobrador de places para o bloqueio, não há dados significativos nele. O exité da parte dentro do ( ). Quando o subprocesso termina, o bloqueio é liberado automaticamente, porque não há processo mantendo-o.
clacke
158

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. mkdircria 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) .

lhunath
fonte
1
Você pode combinar a if ! mkdirpeç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.
Tobias Kienzler
4
Certamente é verdade que mkdirnã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ê /tmpseja um compartilhamento NFS e provavelmente será fornecido por um fs que é implementado mkdiratomicamente.
Lhunath 19/09/12
5
Mas há uma maneira de verificar a existência de um arquivo regular e criá-lo atomicamente, se não existir: use lnpara 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.
Juan Cespedes
4
Não é 'uma maneira de verificar se um arquivo existe e criá-lo em uma única ação atômica' - é 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).
Toby Speight
5
"Todas as abordagens que testam a existência de" arquivos de bloqueio "são defeituosas. Por quê? Como não há como verificar se existe um arquivo e criá-lo em uma única ação atômica." - Para torná-lo atômico, isso deve ser feito em o nível do kernel - e é feito no nível do kernel com o flock (1) linux.die.net/man/1/flock, que parece ter existido desde a data de copyright do homem desde, pelo menos, 2006. Então fiz um voto negativo (- 1), nada pessoal, apenas tenha forte convicção de que o uso das ferramentas implementadas pelo kernel fornecidas pelos desenvolvedores do kernel está correto.
Craig Hicks
42

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.

Cowan
fonte
3
A flock()chamada do sistema não é POSIX e não funciona para arquivos em montagens NFS.
maxschlepzig
17
Executando a partir de um trabalho Cron que utilizo flock -x -n %lock file% -c "%command%"para garantir que apenas uma instância esteja em execução.
Ryall
Aww, em vez do rebanho sem imaginação (1), eles deveriam ter seguido algo como o rebanho (U). ... tem alguma familiaridade com isso. . .parece que eu já ouvi isso antes de uma ou duas vezes.
Kent Kruckeberg
É notável que a documentação do flock (2) especifique o uso apenas com arquivos, mas a documentação do flock (1) especifique o uso com o arquivo ou o diretório. A documentação do flock (1) não é explícita sobre como indicar a diferença durante a criação, mas presumo que isso seja feito adicionando um "/" final. De qualquer forma, se o flock (1) pode manipular diretórios, mas o flock (2) não, o flock (1) não é implementado apenas no flock (2).
Craig Hicks
27

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.

Gunstick
fonte
1
Execute isso algumas vezes simultaneamente (como "./a.sh & ./a.sh & ./a.sh & ./a.sh & ./a.sh & ./a.sh & ./a.sh & ") e o script vazará algumas vezes.
Nippysaurus
7
@ Nippysaurus: Este método de bloqueio não vaza. O que você viu foi o script inicial terminando antes de todas as cópias serem lançadas, para que outro pudesse (corretamente) obter o bloqueio. Para evitar esse falso positivo, adicione um sleep 10antes rmdire tente cascatear novamente - nada "vazará".
Sir Athos
Outras fontes afirmam que o mkdir não é atômico em alguns sistemas de arquivos como o NFS. E já vi ocasiões em que no NFS o mkdir recursivo simultâneo leva a erros às vezes nos trabalhos de matriz de jenkins. Então, eu tenho certeza que é esse o caso. Mas mkdir é muito bom para IMO de casos de uso menos exigentes.
Akostadinov
Você pode usar a opção nashboard de Bash'es com arquivos regulares.
Palec
26

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:

  • ligação simbólica
  • renomear

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.


fonte
Quais páginas do NFS Illustrated suportam a afirmação de que o mkdir não é atômico sobre o NFS?
maxschlepzig
Agradece por esta técnica. Uma implementação do shell mutex está disponível na minha nova lib shell: github.com/Offirmo/offirmo-shell-lib , consulte "mutex". Ele usa, lockfilese disponível, ou fallback para esse symlinkmétodo, se não.
Offirmo
Agradável. Infelizmente, esse método não fornece uma maneira de excluir automaticamente bloqueios obsoletos.
Richard Hansen
Para o desbloqueio em dois estágios ( mv, rm), deve rm -fser usado, e não rmno 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 mve rm), finalmente P1 tenta rme falha.
Matt Wallis
1
@ MattWallis Esse último problema pode ser facilmente mitigado com a inclusão $$no ${f}.deletemenome do arquivo.
Stefan Majewsky
23

Outra opção é usar a noclobberopçã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 pdkshadiciona a O_TRUNCflag, mas obviamente é redundante:
você está criando um arquivo vazio ou não está fazendo nada.


Como você faz isso rmdepende 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
Mikel
fonte
Abordagem muito nova ... essa parece ser uma maneira de obter atomicidade usando um arquivo de bloqueio em vez de um diretório de bloqueio.
Matt Caldwell
Boa abordagem. :-) Na armadilha EXIT, deve restringir qual processo pode limpar o arquivo de bloqueio. Por exemplo: trap 'if [[$ (cat "$ lockfile") == "$$"]]; então rm "$ lockfile"; fi 'EXIT
Kevin Seifert
1
Os arquivos de bloqueio não são atômicos sobre o NFS. é por isso que as pessoas passaram a usar diretórios de bloqueio.
K Richard Pixley
20

Você pode usar GNU Parallelisso, 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.

Mark Setchell
fonte
Pena que as pessoas relutam em votar respostas inúteis: isso leva a que novas respostas relevantes sejam enterradas em uma pilha de lixo.
Dmitry Grigoryev
4
Nós só precisamos de muitos votos. Essa é uma resposta tão organizada e pouco conhecida. (Embora fosse um pedante, o OP queria ser rápido e sujo, enquanto isso é rápido e limpo!) Mais sobre sema questão relacionada unix.stackexchange.com/a/322200/199525 .
Parcialmente nublado
16

Para scripts de shell, costumo usar o mkdirover flock, pois torna os bloqueios mais portáteis.

De qualquer maneira, usar set -enã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_exitsempre 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.

Mark Stinson
fonte
2
Seu script é muito detalhado, acho que poderia ter sido muito mais curto, mas no geral, sim, você precisa configurar armadilhas para fazer isso corretamente. Além disso, eu adicionaria SIGHUP.
Mjuba
Isso funciona bem, exceto que parece verificar $ LOCK_DIR enquanto remove $ __ lockdir. Talvez eu devesse sugerir que, ao remover o bloqueio, você faria rm -r $ LOCK_DIR?
bevada
Obrigado pela sugestão. O código acima foi levantado e colocado em um código de psuedo, portanto, será necessário um ajuste com base no uso das pessoas. No entanto, eu fui deliberadamente com o rmdir no meu caso, pois o rmdir remove com segurança apenas os diretórios - se estiverem vazios. Se as pessoas estão colocando recursos neles, como arquivos PID, etc., eles devem alterar a limpeza de seus bloqueios para torná-los mais agressivos rm -r $LOCK_DIRou até forçá-los conforme necessário (como fiz também em casos especiais, como manter arquivos de rascunho relativos). Felicidades.
Mark Stinson
Você já testou exit 1002?
Gilles Quenot
13

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. :)

Majal
fonte
Como faço para simular isso para testá-lo? Existe uma maneira de iniciar um script duas vezes em uma linha e talvez receber um aviso, se ele já estiver em execução?
rubo77
2
Isso não está funcionando! Por que verificar -gt 2? O grep nem sempre se encontra no resultado do ps!
rubo77
pgrepnão está no POSIX. Se você deseja que isso funcione de maneira portável, é necessário o POSIX pse processar sua saída.
Palec 31/07
No OSX -cnão existe, você terá que usar | wc -l. Sobre a comparação de números: -gt 1está marcado desde que a primeira instância se vê.
Benjamin Peter
6

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
bk138
fonte
5

Criar um arquivo de bloqueio em um local conhecido e verificar a existência no início do script? Colocar o PID no arquivo pode ser útil se alguém tentar rastrear uma instância incorreta que esteja impedindo a execução do script.

Roubar
fonte
5

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

Znik
fonte
1
Você pode usar ln -s, porque isso pode criar um link simbólico apenas quando não houver arquivo ou link simbólico, o mesmo que mkdir. muitos processos do sistema usavam links simbólicos no passado, por exemplo, init ou inetd. O synlink mantém a identificação do processo, mas realmente não aponta para nada. durante os anos esse comportamento foi alterado. processos usa bandos e semáforos.
Znik 14/08
5

As respostas existentes publicadas dependem do utilitário CLI flockou 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_upem qualquer ponto de saída do seu script.

Eu usei o acima em ambos os ambientes Linux e FreeBSD.

David M. Syzdek
fonte
4

Ao direcionar uma máquina Debian, acho o lockfile-progspacote uma boa solução. procmailtambém vem com uma lockfileferramenta. No entanto, às vezes eu estou preso com nenhum deles.

Aqui está minha solução que usa mkdiratô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_requirequando 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_trye 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
}
Jason Weathered
fonte
Obrigado, tentei no cygwin eu mesmo e passou em testes simples.
Ndemou
4

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 
presto8
fonte
3
Apenas pensei em apontar que -x (bloqueio de gravação) já está definido por padrão.
Keldon Alleyne '
e -nirá exit 1imediatamente se não puder obter o bloqueio
Anentropic
Obrigado @KeldonAlleyne, atualizei o código para remover "-x", pois é o padrão.
presto8
3

Alguns unixes possuem lockfilemuito 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.

dmckee --- gatinho ex-moderador
fonte
como podemos obter o lockfileutilitário?
Offirmo
lockfileé distribuído com procmail. Também há uma alternativa dotlockfileque acompanha o liblockfilepacote. Ambos afirmam trabalhar de maneira confiável no NFS.
Mr. Deathless
3

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 noclobberopçã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

NickSoft
fonte
3

Eu queria acabar com arquivos de bloqueio, lockdirs, programas especiais de bloqueio e, mesmo pidofque 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). ifDeclaraçã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
linux_newbie
fonte
1
Isso é sensível à saída 'ps', na minha máquina (Ubuntu 14.04, / bin / ps da procps-ng versão 3.3.9) o comando 'ps axf' imprime caracteres de árvore ascii que atrapalham os números de campo. Isso funcionou para mim: /bin/ps -a --format pid,cmd | awk -v pid=$$ '/'$(basename $0)'/ { if ($1!=pid) print $1; }'
qneill 23/02
2

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
rublo
fonte
Eu te dei +1 para uma solução diferente. Embora não funcione no AIX (> ps -eo pid, ppid, lstart $$ | tail -1 ps: lista inválida com -o.) Não no HP-UX (> ps -eo pid, ppid, lstart $$ | tail -1 ps: opção ilegal - o). Obrigado.
Tagar
2

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.

user3132194
fonte
1

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 pspara 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.

Drew Stephens
fonte
1

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.

thecowster
fonte
1

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.)

Tim Bunce
fonte
1

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
sivann
fonte
1

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.

Filidor Wiese
fonte
1
Sem a chamada de interceptação (ou pelo menos uma limpeza próxima ao final para o caso normal), você tem o bug falso positivo onde o arquivo de bloqueio é deixado após a última execução e o PID foi reutilizado por outro processo posteriormente. (E, na pior das hipóteses, ele foi dotado de um longo processo correndo como apache ....)
Philippe Chaintreuil
1
Concordo, minha abordagem é falha, precisa de uma armadilha. Eu atualizei minha solução. Eu ainda prefiro não ter dependências externas.
Filidor Wiese
1

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 
Sudhir Kumar
fonte
1

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.

Z KC
fonte
0

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.

EU RESPONDO AO CRAP
fonte
0

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
Aupajo
fonte
7
Isso pode ou não ser um problema, dependendo de como é usado, mas há uma condição de corrida entre testar o bloqueio e criá-lo, para que dois scripts possam ser iniciados ao mesmo tempo. Se um terminar primeiro, o outro continuará sendo executado sem um arquivo de bloqueio.
TimB
3
O C News, que me ensinou muito sobre scripts de shell portáteis, costumava criar um arquivo. $$ de bloqueio e, em seguida, tentar vinculá-lo a "bloqueio" - se o link foi bem-sucedido, você tinha o bloqueio, caso contrário, você removeu o bloqueio. $$ e saiu.
Paul Tomblin 9/10/08
Essa é realmente uma boa maneira de fazê-lo, exceto que você ainda sofre com a necessidade de remover o arquivo de bloqueio manualmente se algo der errado e o arquivo de bloqueio não for excluído.
Matthew Scharley 9/10/08
2
Rápido e sujo, isso é o que ele pediu :)
Aupajo