Bloqueio correto em scripts de shell?

66

Às vezes, você precisa garantir que apenas uma instância de um shell script esteja sendo executada ao mesmo tempo.

Por exemplo, uma tarefa cron que é executada via crond que não fornece bloqueio por si só (por exemplo, a crond Solaris padrão).

Um padrão comum para implementar o bloqueio é um código como este:

#!/bin/sh
LOCK=/var/tmp/mylock
if [ -f $LOCK ]; then            # 'test' -> race begin
  echo Job is already running\!
  exit 6
fi
touch $LOCK                      # 'set'  -> race end
# do some work
rm $LOCK

Obviamente, esse código tem uma condição de corrida. Há uma janela de tempo em que a execução de duas instâncias pode avançar após a linha 3 antes que alguém possa tocar no $LOCKarquivo.

Para um trabalho cron, isso geralmente não é um problema, porque você tem um intervalo de minutos entre duas invocações.

Mas as coisas podem dar errado - por exemplo, quando o arquivo de bloqueio está em um servidor NFS - que trava. Nesse caso, vários trabalhos cron podem bloquear na linha 3 e fazer fila. Se o servidor NFS estiver ativo novamente, você terá um rebanho trovejante de trabalhos em execução paralelos.

Pesquisando na web, encontrei a ferramenta lockrun, que parece ser uma boa solução para esse problema. Com ele, você executa um script que precisa ser bloqueado assim:

$ lockrun --lockfile=/var/tmp/mylock myscript.sh

Você pode colocar isso em um invólucro ou usá-lo no seu crontab.

Ele usa lockf()(POSIX) se disponível e volta a flock()(BSD). E o lockf()suporte ao NFS deve ser relativamente amplo.

Existem alternativas para lockrun?

E os outros daemons cron? Existem fundos comuns que suportam o bloqueio de maneira sã? Uma rápida olhada na página de manual do Vixie Crond (padrão nos sistemas Debian / Ubuntu) não mostra nada sobre o bloqueio.

Seria uma boa ideia incluir uma ferramenta como lockrunno coreutils ?

Na minha opinião ele implementa um tema muito semelhante a timeout, nicee amigos.

maxschlepzig
fonte
4
Tangencialmente, e para o benefício de outras pessoas que possam considerar seu padrão inicial Bom o suficiente (tm), esse código de shell deve possivelmente interceptar o TERM para remover seu arquivo de bloqueio quando killeditado; e parece uma boa prática armazenar o próprio pid no arquivo de bloqueio, em vez de apenas tocá-lo.
Ulrich Schwarz
@ Shawn, na verdade, não menciona crond e NFS.
maxschlepzig
pergunta relacionada no SO: stackoverflow.com/questions/185451/…
maxschlepzig 4/11
11
@ Ulrich muito tardiamente, armazenar um PID em um arquivo de bloqueio NFS agrega muito pouco valor. Mesmo adicionando o nome do host ainda realmente não ajuda com a verificação de um processo vivo
roaima

Respostas:

45

Aqui está outra maneira de bloquear o script shell que pode impedir a condição de corrida descrita acima, onde dois trabalhos podem passar na linha 3. A noclobberopção funcionará no ksh e no bash. Não use, set noclobberporque você não deve criar scripts no csh / tcsh. ;)

lockfile=/var/tmp/mylock

if ( set -o noclobber; echo "$$" > "$lockfile") 2> /dev/null; then

        trap 'rm -f "$lockfile"; exit $?' INT TERM EXIT

        # do stuff here

        # clean up after yourself, and release your trap
        rm -f "$lockfile"
        trap - INT TERM EXIT
else
        echo "Lock Exists: $lockfile owned by $(cat $lockfile)"
fi

YMMV com bloqueio no NFS (você sabe, quando os servidores NFS não estão acessíveis), mas em geral é muito mais robusto do que costumava ser. (Há 10 anos)

Se você tiver tarefas cron que fazem a mesma coisa ao mesmo tempo, de vários servidores, mas você só precisa de 1 instância para executar, algo como isso pode funcionar para você.

Não tenho experiência com lockrun, mas ter um ambiente de bloqueio pré-definido antes da execução do script realmente pode ajudar. Ou talvez não. Você está apenas configurando o teste para o arquivo de bloqueio fora do seu script em um invólucro e, teoricamente, não poderia simplesmente atingir a mesma condição de corrida se dois trabalhos fossem chamados pelo lockrun exatamente ao mesmo tempo, assim como o 'inside- solução do script?

O bloqueio de arquivos é praticamente o comportamento do sistema, e qualquer script que não verifique a existência do arquivo de bloqueio antes da execução fará o que for necessário. Apenas colocando o teste do arquivo de bloqueio e o comportamento adequado, você estará resolvendo 99% dos problemas em potencial, se não 100%.

Se você enfrentar muitas condições de corrida no arquivo de bloqueio, pode ser um indicador de um problema maior, como não ter seus trabalhos no tempo certo ou talvez se o intervalo não for tão importante quanto a conclusão do trabalho, talvez seu trabalho seja mais adequado para ser daemonizado .


EDITAR ABAIXO - 2016-05-06 (se você estiver usando o KSH88)


Com base no comentário do @Clint Pachl abaixo, se você usar o ksh88, use em mkdirvez de noclobber. Isso atenua principalmente uma condição potencial de corrida, mas não a limita inteiramente (embora o risco seja minúsculo). Para mais informações, leia o link que Clint postou abaixo .

lockdir=/var/tmp/mylock
pidfile=/var/tmp/mylock/pid

if ( mkdir ${lockdir} ) 2> /dev/null; then
        echo $$ > $pidfile
        trap 'rm -rf "$lockdir"; exit $?' INT TERM EXIT
        # do stuff here

        # clean up after yourself, and release your trap
        rm -rf "$lockdir"
        trap - INT TERM EXIT
else
        echo "Lock Exists: $lockdir owned by $(cat $pidfile)"
fi

E, como uma vantagem adicional, se você precisar criar tmpfiles em seu script, poderá usar o lockdirdiretório para eles, sabendo que eles serão limpos quando o script sair.

Para uma festa mais moderna, o método noclobber na parte superior deve ser adequado.

Tim Kennedy
fonte
11
Não, com o lockrun, você não tem problemas - quando um servidor NFS trava, todas as chamadas de lockrun ficam paralisadas (pelo menos) na lockf()chamada do sistema - quando é feito o backup, todos os processos são retomados, mas apenas um processo vence o bloqueio. Sem condição de corrida. Não encontro muitos problemas com o cronjobs - o oposto é o caso -, mas esse é um problema quando o atinge, pois tem o potencial de criar muita dor.
maxschlepzig
11
Aceitei esta resposta porque o método é seguro e até agora o mais elegante. Sugiro uma pequena variante: set -o noclobber && echo "$$" > "$lockfile"obter um fallback seguro quando o shell não suporta a opção noclobber.
maxschlepzig
3
Boa resposta, mas você também deve 'matar -0' o valor no arquivo de bloqueio para garantir que o processo que criou o bloqueio ainda exista.
perfil completo de Nigel Horne
11
A noclobberopção pode estar sujeita a condições de corrida. Veja mywiki.wooledge.org/BashFAQ/045 para um pouco de reflexão.
Clint Pachl
2
Nota: usando noclobber(ou -C) em ksh88 não funciona porque ksh88 não usa O_EXCLpara noclobber. Se você estiver executando com um novo shell você pode ser OK ...
jrw32982
14

Eu prefiro usar links físicos.

lockfile=/var/lock/mylock
tmpfile=${lockfile}.$$
echo $$ > $tmpfile
if ln $tmpfile $lockfile 2>&-; then
    echo locked
else
    echo locked by $(<$lockfile)
    rm $tmpfile
    exit
fi
trap "rm ${tmpfile} ${lockfile}" 0 1 2 3 15
# do what you need to

Os links físicos são atômicos sobre o NFS e, na maioria das vezes, o mkdir também . Usando mkdir(2)ou link(2)são praticamente os mesmos, em um nível prático; Prefiro usar links físicos, porque mais implementações do NFS permitiam links físicos atômicos do que atômicos mkdir. Nas versões modernas do NFS, você também não precisa se preocupar em usá-lo.

Arcege
fonte
12

Eu entendo que mkdiré atômico, então talvez:

lockdir=/var/tmp/myapp
if mkdir $lockdir; then
  # this is a new instance, store the pid
  echo $$ > $lockdir/PID
else
  echo Job is already running, pid $(<$lockdir/PID) >&2
  exit 6
fi

# then set traps to cleanup upon script termination 
# ref http://www.shelldorado.com/goodcoding/tempfiles.html
trap 'rm -r "$lockdir" >/dev/null 2>&1' 0
trap "exit 2" 1 2 3 13 15
Glenn Jackman
fonte
Ok, mas não consegui encontrar informações mkdir()sobre se o NFS (> = 3) é padronizado para ser atômico.
maxschlepzig
2
@maxschlepzig O RFC 1813 não chama explicitamente mkdirpara ser atômico (ele faz para rename). Na prática, sabe-se que algumas implementações não são. Relacionado: um tópico interessante, incluindo uma contribuição do autor do GNU arch .
Gilles 'SO- stop be evil'
8

Uma maneira fácil é usar lockfilevindo normalmente com o procmailpacote.

LOCKFILE="/tmp/mylockfile.lock"
# try once to get the lock else exit
lockfile -r 0 "$LOCKFILE" || exit 0

# here the actual job

rm -f "$LOCKFILE"
jofel
fonte
5

semque é parte das parallelferramentas GNU , pode ser o que você está procurando:

sem [--fg] [--id <id>] [--semaphoretimeout <secs>] [-j <num>] [--wait] command

Como em:

sem --id my_semaphore --fg "echo 1 ; date ; sleep 3" &
sem --id my_semaphore --fg "echo 2 ; date ; sleep 3" &
sem --id my_semaphore --fg "echo 3 ; date ; sleep 3" &

saída:

1
Thu 10 Nov 00:26:21 UTC 2016
2
Thu 10 Nov 00:26:24 UTC 2016
3
Thu 10 Nov 00:26:28 UTC 2016

Observe que o pedido não é garantido. Além disso, a saída não é exibida até terminar (irritante!). Mas, mesmo assim, é a maneira mais concisa que conheço para me proteger contra a execução simultânea, sem se preocupar com arquivos de bloqueio, novas tentativas e limpeza.

Parcialmente nublado
fonte
O bloqueio oferecido pelo semidentificador está sendo abatido no meio da execução?
roaima
2

Eu uso dtach.

$ dtach -n /tmp/socket long_running_task ; echo $?
0
$ dtach -n /tmp/socket long_running_task ; echo $?
dtach: /tmp/socket: Address already in use
1
AndresVia
fonte
1

Eu uso a ferramenta de linha de comando "flock" para gerenciar bloqueios nos meus scripts do bash, conforme descrito aqui e aqui . Eu usei esse método simples na página de manual do flock, para executar alguns comandos em um subshell ...

   (
     flock -n 9
     # ... commands executed under lock ...
   ) 9>/var/lock/mylockfile

Nesse exemplo, ele falha com o código de saída 1 se não puder adquirir o arquivo de bloqueio. Mas o flock também pode ser usado de maneiras que não exigem que os comandos sejam executados em um sub-shell :-)

dru8274
fonte
3
A flock()chamada do sistema não funciona no NFS .
maxschlepzig
11
O BSD possui uma ferramenta semelhante, "lockf".
dubiousjim
2
@dubiousjim, o BSD lockf também chama flock()e, portanto, é problemático no NFS. Enquanto isso, o flock () no Linux agora volta para fcntl()quando o arquivo está localizado em uma montagem NFS; portanto, em um ambiente NFS somente para Linux, flock()agora funciona sobre NFS.
Maxschlepzig #
1

Não use um arquivo.

Se o seu script for executado assim, por exemplo:

bash my_script

Você pode detectar se está sendo executado usando:

running_proc=$(ps -C bash -o pid=,cmd= | grep my_script);
if [[ "$running_proc" != "$$ bash my_script" ]]; do 
  echo Already locked
  exit 6
fi
frogstarr78
fonte
Hum, o código de verificação ps é executado por dentro my_script? No caso de outra instância estar em execução - não running_proccontém duas linhas correspondentes? Eu gosto da idéia, mas é claro - você vai ter resultados falsos quando outro usuário estiver executando um script com o mesmo nome ...
maxschlepzig
3
Também inclui uma condição de corrida: se duas instâncias executam a primeira linha em paralelo, nenhuma recebe o 'bloqueio' e ambas saem com o status 6. Isso seria uma espécie de fome mútua de uma rodada . Btw, não sei por que você usa em $!vez de $$no seu exemplo.
maxschlepzig
@maxschlepzig realmente sinto muito pelo $ incorreto! vs. $$
frogstarr78
@maxschlepzig para lidar com vários usuários executando o script, adicione euser = ao argumento -o.
Frogstarr78 5/05
@maxschlepzig para impedir várias linhas, você também pode alterar os argumentos para grep ou "filtros" adicionais (por exemplo grep -v $$). Basicamente, eu estava tentando fornecer uma abordagem diferente para o problema.
Frogstarr78 5/05
1

Para uso real, você deve usar a resposta mais votada .

No entanto, quero discutir algumas abordagens quebradas e semi-viáveis ​​usando pse as muitas advertências que elas têm, já que continuo vendo as pessoas usá-las.

Esta resposta é realmente a resposta para "Por que não usar pse grepmanipular o bloqueio no shell?"

Abordagem quebrada # 1

Primeiro, uma abordagem dada em outra resposta que tem alguns votos positivos, apesar de não funcionar (e nunca poderia) funcionar e claramente nunca ter sido testada:

running_proc=$(ps -C bash -o pid=,cmd= | grep my_script);
if [[ "$running_proc" != "$$ bash my_script" ]]; do 
  echo Already locked
  exit 6
fi

Vamos corrigir os erros de sintaxe e os psargumentos quebrados e obter:

running_proc=$(ps -C bash -o pid,cmd | grep "$0");
echo "$running_proc"
if [[ "$running_proc" != "$$ bash $0" ]]; then
  echo Already locked
  exit 6
fi

Esse script sempre sai do 6, toda vez, não importa como você o execute.

Se você executá-lo ./myscript, a pssaída será apenas 12345 -bash, o que não corresponde à string necessária 12345 bash ./myscript, e isso falhará.

Se você executá-lo bash myscript, as coisas ficam mais interessantes. O processo do bash bifurca-se para executar o pipeline e o shell filho executa o pse grep. O shell original e o shell filho aparecerão na pssaída, algo como isto:

25793 bash myscript
25795 bash myscript

Essa não é a saída esperada $$ bash $0; portanto, seu script será encerrado.

Abordagem quebrada # 2

Agora, com toda a justiça para o usuário que escreveu a abordagem quebrada nº 1, fiz algo parecido quando tentei isso pela primeira vez:

if otherpids="$(pgrep -f "$0" | grep -vFx "$$")" ; then
  echo >&2 "There are other copies of the script running; exiting."
  ps >&2 -fq "${otherpids//$'\n'/ }" # -q takes about a tenth the time as -p
  exit 1
fi

Isso quase funciona. Mas o fato de bifurcar-se para passar o tubo acaba com isso. Portanto, este sempre sairá também.

Abordagem não confiável # 3

pids_this_script="$(pgrep -f "$0")"
if not_this_process="$(echo "$pids_this_script" | grep -vFx "$$")"; then
  echo >&2 "There are other copies of this script running; exiting."
  ps -fq "${not_this_process//$'\n'/ }"
  exit 1
fi

Essa versão evita o problema de bifurcação de pipeline na abordagem nº 2, obtendo primeiro todos os PIDs que possuem o script atual em seus argumentos de linha de comando e, em seguida, filtrando essa lista de pidlists separadamente para omitir o PID do script atual.

Isso pode funcionar ... desde que nenhum outro processo tenha uma linha de comando correspondente ao $0, e fornecendo o script sempre chamado da mesma maneira (por exemplo, se for chamado com um caminho relativo e depois um caminho absoluto, a última instância não notará o anterior )

Abordagem não confiável # 4

E se ignorarmos a verificação da linha de comando completa, pois isso pode não indicar um script realmente em execução e, em lsofvez disso, verificarmos todos os processos que têm esse script aberto?

Bem, sim, essa abordagem não é realmente tão ruim:

if otherpids="$(lsof -t "$0" | grep -vFx "$$")"; then
  echo >&2 "Error: There are other processes that have this script open - most likely other copies of the script running.  Exiting to avoid conflicts."
  ps >&2 -fq "${otherpids//$'\n'/ }"
  exit 1
fi

Obviamente, se uma cópia do script estiver em execução, a nova instância será iniciada muito bem e você terá duas cópias em execução.

Ou se o script em execução for modificado (por exemplo, com Vim ou com a git checkout), a versão "nova" do script será iniciada sem problemas, pois o Vim e git checkoutresulta em um novo arquivo (um novo inode) no lugar do antigo.

No entanto, se o script nunca for modificado e nunca copiado, essa versão será muito boa. Não há condição de corrida porque o arquivo de script já precisa ser aberto antes que a verificação possa ser alcançada.

Ainda pode haver falsos positivos se outro processo tiver o arquivo de script aberto, mas observe que, mesmo que esteja aberto para edição no Vim, o vim na verdade não mantém o arquivo de script aberto, portanto não resultará em falsos positivos.

Mas lembre-se, não use essa abordagem se o script puder ser editado ou copiado, pois você obterá falsos negativos, ou seja, várias instâncias sendo executadas ao mesmo tempo - portanto, o fato de a edição com o Vim não fornecer falso-positivos não deve importar para você. I mencionar isso, porém, porque abordagem # 3 não dar falsos positivos (ou seja, se recusa a iniciar) se você tiver o script aberto com Vim.

Então o que fazer então?

A resposta mais votada para esta pergunta fornece uma boa abordagem sólida.

Talvez você possa escrever uma melhor ... mas se você não entender todos os problemas e advertências com todas as abordagens acima, provavelmente não escreverá um método de bloqueio que evite todas elas.

Curinga
fonte
0

Aqui está algo que às vezes adiciono em um servidor para lidar facilmente com as condições de corrida de qualquer trabalho na máquina. É semelhante à postagem de Tim Kennedy, mas dessa maneira você obtém manipulação de corrida adicionando apenas uma linha a cada script do bash que precisa.

Coloque o conteúdo abaixo em, por exemplo, / opt / racechecker / racechecker:

ZPROGRAMNAME=$(readlink -f $0)
EZPROGRAMNAME=`echo $ZPROGRAMNAME | sed 's/\//_/g'`
EZMAIL="/usr/bin/mail"
EZCAT="/bin/cat"

if  [ -n "$EZPROGRAMNAME" ] ;then
        EZPIDFILE=/tmp/$EZPROGRAMNAME.pid
        if [ -e "$EZPIDFILE" ] ;then
                EZPID=$($EZCAT $EZPIDFILE)
                echo "" | $EZMAIL -s "$ZPROGRAMNAME already running with pid $EZPID"  alarms@someemail.com >>/dev/null
                exit -1
        fi
        echo $$ >> $EZPIDFILE
        function finish {
          rm  $EZPIDFILE
        }
        trap finish EXIT
fi

Aqui está como usá-lo. Observe a linha após o shebang:

     #/bin/bash
     . /opt/racechecker/racechecker
     echo "script are running"
     sleep 120

A maneira como funciona é que ele descobre o nome principal do arquivo bashscript e cria um pidfile em "/ tmp". Ele também adiciona um ouvinte ao sinal de chegada. O ouvinte removerá o pidfile quando o script principal estiver finalizando corretamente.

Em vez disso, se existir um arquivo pid quando uma instância é iniciada, a instrução if contendo o código dentro da segunda instrução if será executada. Nesse caso, decidi lançar um e-mail de alarme quando isso acontecer.

E se o script falhar

Um exercício adicional seria lidar com falhas. Idealmente, o pidfile deve ser removido, mesmo se o script principal travar por qualquer motivo, isso não é feito na minha versão acima. Isso significa que, se o script travar, o pidfile precisará ser removido manualmente para restaurar a funcionalidade.

Em caso de falha do sistema

É uma boa idéia armazenar o arquivo pidfile / lockfile em, por exemplo / tmp. Dessa forma, seus scripts continuarão a ser executados após uma falha no sistema, pois os arquivos pid sempre serão excluídos na inicialização.

ziggestardust
fonte
Diferentemente da ansatz de Tim Kennedy, seu script contém uma condição de corrida. Isso ocorre porque a verificação da presença do PIDFILE e de sua criação condicional não é feita em uma operação atômica.
maxschlepzig
+1 nisso! Vou levar isso em consideração e modificar meu script.
Ziggestardust
-2

Verifique meu script ...

Você pode adorar ....

[rambabu@Server01 ~]$ sh Prevent_cron-OR-Script_against_parallel_run.sh
Parallel RUN Enabled
Now running
Task completed in Parallel RUN...
[rambabu@Server01 ~]$ cat Prevent_cron-OR-Script_against_parallel_run.sh
#!/bin/bash
#Created by RambabuKella
#Date : 12-12-2013

#LOCK file name
Parallel_RUN="yes"
#Parallel_RUN="no"
PS_GREP=0
LOCK=/var/tmp/mylock_`whoami`_"$0"
#Checking for the process
PS_GREP=`ps -ef |grep "sh $0" |grep -v grep|wc -l`
if [ "$Parallel_RUN" == "no" ] ;then
echo "Parallel RUN Disabled"

 if [ -f $LOCK ] || [ $PS_GREP -gt 2   ] ;then
        echo -e "\nJob is already running OR LOCK file exists. "
        echo -e "\nDetail are : "
        ps -ef |grep  "$0" |grep -v grep
        cat "$LOCK"
  exit 6
 fi
echo -e "LOCK file \" $LOCK \" created on : `date +%F-%H-%M` ." &> $LOCK
# do some work
echo "Now running"
echo "Task completed on with single RUN ..."
#done

rm -v $LOCK 2>/dev/null
exit 0
else

echo "Parallel RUN Enabled"

# do some work
echo "Now running"
echo "Task completed in Parallel RUN..."
#done

exit 0
fi
echo "some thing wrong"
exit 2
[rambabu@Server01 ~]$
user54178
fonte
-3

Eu ofereço a seguinte solução, em um script chamado 'flocktest'

#!/bin/bash
export LOGFILE=`basename $0`.logfile
logit () {
echo "$1" >>$LOGFILE
}
PROGPATH=$0
(
flock -x -n 257
(($?)) && logit "'$PROGPATH' is already running!" && exit 0
logit "'$PROGPATH', proc($$): sleeping 30 seconds"
sleep 30
)257<$PROGPATH
Newton T Hammet Jr
fonte