Bloqueio correto em scripts de shell?

62

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

Por exemplo, uma tarefa cron que é executada via crond que não fornece bloqueando por conta própria (por exemplo, o padrão Solaris crond).

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

Claro, esse código tem uma condição de corrida. Existe uma janela de tempo onde o a execução de duas instâncias pode avançar após a linha 3 antes que uma seja capaz de toque no arquivo $LOCK .

Para uma tarefa 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 trava está em um servidor NFS - que trava. Nesse caso, vários cron jobs podem bloquear na linha 3 e fazer fila. E se o servidor NFS está ativo novamente, então você tem rebanho trovejante paralelo executando trabalhos.

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

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

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

Ele usa lockf() (POSIX) se disponível e volta para flock() (BSD). E o lockf() de suporte sobre o NFS deve ser relativamente difundido.

Existem alternativas para lockrun ?

E os outros daemons do cron? Existem crenas comuns que suportam o bloqueio em um maneira sã? Um rápido olhar para a página man do Vixie Crond (default on Sistemas Debian / Ubuntu) não mostra nada sobre bloqueio.

Seria uma boa idéia incluir uma ferramenta como lockrun em coreutils ?

Na minha opinião, ele implementa um tema muito semelhante ao timeout , nice e amigos.

    
por maxschlepzig 04.10.2011 / 20:59

13 respostas

43

Aqui está outra maneira de bloquear o shell script que pode evitar a condição de corrida descrita acima, onde duas tarefas podem passar a linha 3. A opção noclobber funcionará em ksh e bash. Não use set noclobber porque você não deveria estar fazendo script 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 agendadas que façam a mesma coisa ao mesmo tempo, a partir de vários servidores, mas precisar de apenas uma instância para ser executada, algo assim poderá funcionar para você.

Eu não tenho experiência com o lockrun, mas ter um ambiente de bloqueio pré-definido antes de o script realmente ser executado pode ajudar. Ou talvez não. Você está apenas definindo o teste para o lockfile fora de seu script em um wrapper e, teoricamente, você não poderia simplesmente atingir a mesma condição de corrida se dois jobs fossem chamados por lockrun exatamente ao mesmo tempo, assim como com o 'inside- a solução do script?

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

Se você se deparar com as condições de corrida do arquivo lockfile, pode ser um indicador de um problema maior, como não ter seus trabalhos cronometrados corretamente ou talvez se o intervalo não for tão importante quanto o término do trabalho, talvez seu trabalho seja mais adequado para ser daemonized.

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

Baseie no comentário de @Clint Pachl abaixo, se você usar ksh88, use mkdir em vez de noclobber . Isso atenua principalmente uma possível condição de corrida, mas não a limita inteiramente (embora o risco seja minúsculo). Para mais informações, leia o link que o 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, se você precisar criar tmpfiles no seu script, você pode usar o diretório lockdir para eles, sabendo que eles serão limpos quando o script sair.

Para um bash mais moderno, o método noclobber no topo deve ser adequado.

    
por 04.10.2011 / 21:50
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

Links físicos são atômicos sobre NFS e, na maior parte, O mkdir também está . Usar mkdir(2) ou link(2) é praticamente o mesmo, em um nível prático; Eu prefiro apenas usar hard links porque mais implementações do NFS permitiam hard links atômicos do que atomic mkdir . Com os lançamentos modernos do NFS, você não precisa se preocupar com o uso de ambos.

    
por 05.10.2011 / 03:57
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
    
por 04.10.2011 / 21:37
8

Uma maneira fácil é usar lockfile vindo geralmente com o pacote procmail .

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"
    
por 08.04.2014 / 00:04
3

sem , que vem como parte das ferramentas do GNU parallel , 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é que termine (irritante!). Mas mesmo assim, é a maneira mais concisa que eu sei para me proteger contra a execução simultânea, sem me preocupar com arquivos de travamento, novas tentativas e limpeza.

    
por 10.11.2016 / 01:31
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
    
por 31.05.2012 / 21:16
1

Eu uso a ferramenta de linha de comando "flock" para gerenciar bloqueios em meus scripts bash, conforme descrito aqui e < href="http://www.hackinglinuxexposed.com/articles/20030616.html"> aqui . Eu usei este método simples da página de manobra 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 comandos para serem executados em um sub-shell :-)

    
por 05.10.2011 / 03:16
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
    
por 05.10.2011 / 07:52
0

Usando a ferramenta FLOM (Free LOck Manager) , os comandos de serialização se tornam tão fáceis quanto a execução

flom -- command_to_serialize

O FLOM permite que você implemente casos de uso mais sofisticados (bloqueio distribuído, leitores / gravadores, recursos numéricos, etc ...) como explicado aqui: link

    
por 24.04.2014 / 22:37
0

Aqui, às vezes, adiciono algo em um servidor para lidar facilmente com as condições de corrida de qualquer trabalho da máquina. É semelhante ao post de Tim Kennedy, mas desta forma você obtém o controle da corrida adicionando apenas uma linha a cada script bash que precisa dela.

Coloque o conteúdo abaixo, 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"  [email protected] >>/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 isso funciona é que ele descobre o nome do arquivo bashscript principal e cria um pidfile em "/ tmp". Também adiciona um ouvinte ao sinal de término. O ouvinte removerá o pidfile quando o script principal estiver terminando corretamente.

Em vez disso, se um pidfile existir quando uma instância for iniciada, a instrução if contendo o código dentro da segunda instrução if será executada. Neste caso, decidi lançar uma mensagem de alarme quando isso acontece.

E se o script travar

Um exercício adicional seria lidar com falhas. O ideal é que o pidfile seja removido mesmo que o script principal falhe por algum motivo, isso não é feito na minha versão acima. Isso significa que, se o script falhar, o pidfile teria que ser removido manualmente para restaurar a funcionalidade.

Em caso de falha do sistema

É uma boa idéia armazenar o pidfile / lockfile no exemplo / tmp. Desta forma, os seus scripts continuarão a executar após uma falha no sistema, uma vez que os pidfiles serão sempre apagados no arranque.

    
por 13.06.2015 / 12:03
0

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

No entanto, quero discutir algumas várias abordagens quebradas e semi-trabalháveis usando ps e as muitas ressalvas que elas têm, já que continuo vendo as pessoas usá-las.

Esta resposta é realmente a resposta para "Por que não usar ps e grep para lidar com o bloqueio no shell?"

Abordagem interrompida # 1

Primeiro, uma abordagem dada em outra resposta que tem alguns votos positivos, apesar do fato de que isso não acontece ( e nunca poderia) funcionar e claramente nunca foi testado:

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 argumentos ps 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

Este script irá sempre sair 6, sempre, não importa como você o execute.

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

Se você executar com bash myscript , as coisas ficarão mais interessantes. O processo bash forks para executar o pipeline e o shell child executa o ps e grep . O shell original e o shell filho serão exibidos na saída ps , algo assim:

25793 bash myscript
25795 bash myscript

Essa não é a saída esperada $$ bash $0 , então seu script sairá.

Abordagem interrompida # 2

Agora, com toda a justiça para o usuário que escreveu a abordagem quebrada nº 1, fiz algo semelhante quando tentei 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 forking para executar o tubo joga fora isso. Então 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

Esta versão evita o problema de bifurcação de pipeline na abordagem # 2 obtendo primeiro todos os PIDs que possuem o script atual em seus argumentos de linha de comando, e então filtrando essa lista de pid, separadamente, para omitir o PID de o script atual.

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

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 verificar lsof para localizar todos os processos que têm esse script aberto?

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

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

É claro que, se uma cópia do script estiver em execução, a nova instância iniciará bem e você terá duas cópias em execução.

>

Ou se o script em execução for modificado (por exemplo, com o Vim ou com git checkout ), a versão "nova" do script será iniciada sem problemas, pois tanto o Vim quanto o git checkout resultarão 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 é muito boa. Não há condição de corrida porque o arquivo de script já precisa estar aberto antes que a verificação seja 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 não mantém o arquivo de script aberto, portanto, não resultará em falsos positivos. / p>

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 de uma vez - então o fato de editar com o Vim não Não dê falsos positivos, não importa para você. Eu menciono isso, no entanto, porque a abordagem # 3 faz fornecer falsos positivos (isto é, recusa-se a iniciar) se você tiver o script aberto com o Vim.

Então, o que fazer, então?

A resposta mais votada a essa pergunta oferece uma boa abordagem sólida.

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

    
por 04.12.2018 / 06:30
-1

Verifique meu script ...

Você pode AMAR isso ....

[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 ~]$
    
por 09.12.2013 / 23:08
-2

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
    
por 19.09.2013 / 18:26