Como garantir que apenas uma instância de um script bash seja executada?


26

Uma solução que não requer ferramentas adicionais seria preferida.


Que tal um arquivo de bloqueio?
Marco

@Marco eu encontrei esta resposta SO usando isso, mas como declarou em um comentário , isso pode criar uma condição de corrida
Tobias KIENZLER

3
Este é o BashFAQ 45 .
jw013

@ jw013 obrigado! Então, talvez algo como ln -s my.pid .lockvai reivindicar o bloqueio (seguido echo $$ > my.pid) e em caso de falha pode verificar se o PID armazenado em .locké realmente uma instância ativa do script
Tobias KIENZLER

Respostas:


18

Quase como a resposta do nsg: use um diretório de bloqueio . A criação de diretórios é atômica sob linux e unix e * BSD e muitos outros sistemas operacionais.

if mkdir $LOCKDIR
then
    # Do important, exclusive stuff
    if rmdir $LOCKDIR
    then
        echo "Victory is mine"
    else
        echo "Could not remove lock dir" >&2
    fi
else
    # Handle error condition
    ...
fi

Você pode colocar o PID do bloqueio sh em um arquivo no diretório de bloqueio para fins de depuração, mas não caia na armadilha de pensar que pode verificar esse PID para ver se o processo de bloqueio ainda é executado. Muitas condições de corrida se encontram nesse caminho.


11
Eu consideraria usar o PID armazenado para verificar se a instância de bloqueio ainda está ativa. No entanto, aqui é uma reivindicação que mkdirnão é atômica sobre NFS (que não é o caso para mim, mas eu acho que deve-se mencionar que, se for verdade)
Tobias KIENZLER

Sim, use o PID armazenado para verificar se o processo de bloqueio ainda é executado, mas não tente fazer nada além de registrar uma mensagem. O trabalho de verificar o pid armazenado, criar um novo arquivo PID, etc, deixa uma grande janela para as corridas.
precisa saber é o seguinte

Ok, como Ihunath afirmou , o lockdir provavelmente estaria no /tmpqual geralmente não é compartilhado pelo NFS, portanto tudo bem.
Tobias Kienzler 19/09/12

Eu usaria rm -rfpara remover o diretório de bloqueio. rmdirfalhará se alguém (não necessariamente você) conseguir adicionar um arquivo ao diretório.
chepner

18

Para adicionar à resposta de Bruce Ediger , e inspirado nessa resposta , você também deve adicionar mais inteligência à limpeza para se proteger contra o encerramento de scripts:

#Remove the lock directory
function cleanup {
    if rmdir $LOCKDIR; then
        echo "Finished"
    else
        echo "Failed to remove lock directory '$LOCKDIR'"
        exit 1
    fi
}

if mkdir $LOCKDIR; then
    #Ensure that if we "grabbed a lock", we release it
    #Works for SIGTERM and SIGINT(Ctrl-C)
    trap "cleanup" EXIT

    echo "Acquired lock, running"

    # Processing starts here
else
    echo "Could not create lock directory '$LOCKDIR'"
    exit 1
fi

Alternativamente if ! mkdir "$LOCKDIR"; then handle failure to lock and exit; fi trap and do processing after if-statement,.
Kusalananda

6

Isso pode ser muito simplista, corrija-me se estiver errado. Não é simples o pssuficiente?

#!/bin/bash 

me="$(basename "$0")";
running=$(ps h -C "$me" | grep -wv $$ | wc -l);
[[ $running > 1 ]] && exit;

# do stuff below this comment

11
Agradável e / ou brilhante. :)
Spooky

11
Eu usei essa condição por uma semana e, em duas ocasiões, não impediu o início de um novo processo. Imaginei qual era o problema - o novo pid é uma subcadeia do antigo e fica oculto grep -v $$. exemplos reais: antigo - 14532, novo - 1453, antigo - 28858, novo - 858.
Naktibalda

Eu fixo-lo, mudando grep -v $$paragrep -v "^${$} "
Naktibalda

@Naktibalda boa captura, obrigado! Você também pode corrigi-lo com grep -wv "^$$"(veja editar).
terdon

Obrigado por essa atualização. Ocasionalmente, meu padrão falhava porque os pids mais curtos eram preenchidos com espaços.
Naktibalda 8/03

4

Eu usaria um arquivo de bloqueio, como mencionado por Marco

#!/bin/bash

# Exit if /tmp/lock.file exists
[ -f /tmp/lock.file ] && exit

# Create lock file, sleep 1 sec and verify lock
echo $$ > /tmp/lock.file
sleep 1
[ "x$(cat /tmp/lock.file)" == "x"$$ ] || exit

# Do stuff
sleep 60

# Remove lock file
rm /tmp/lock.file

11
(Acho que você esqueceu de criar o arquivo de bloqueio) E as condições da corrida ?
Tobias Kienzler

ops :) Sim, as condições de corrida são um problema no meu exemplo, eu normalmente escrevo trabalhos cron de hora em hora ou diariamente e as condições de corrida são raras.
nsg 18/09/12

Eles também não devem ser relevantes no meu caso, mas é algo que se deve ter em mente. Talvez usar lsof $0não seja ruim, também?
Tobias Kienzler

Você pode diminuir a condição de corrida escrevendo seu $$no arquivo de bloqueio. Depois, sleeppor um curto intervalo e leia-o novamente. Se o PID ainda for seu, você adquiriu o bloqueio com sucesso. Não precisa absolutamente de ferramentas adicionais.
manatwork

11
Eu nunca usei lsof para esse fim, e isso deve funcionar. Observe que lsof é realmente lento no meu sistema (1 a 2 segundos) e provavelmente há muito tempo para as condições da corrida.
nsg 18/09/12

3

Se você deseja garantir que apenas uma instância do seu script esteja em execução, consulte:

Bloqueie seu script (contra execução paralela)

Caso contrário, você pode verificar psou invocar lsof <full-path-of-your-script>, pois eu não os chamaria de ferramentas adicionais.


Suplemento :

na verdade, eu pensei em fazê-lo assim:

for LINE in `lsof -c <your_script> -F p`; do 
    if [ $$ -gt ${LINE#?} ] ; then
        echo "'$0' is already running" 1>&2
        exit 1;
    fi
done

isso garante que apenas o processo com o menor pidcontinue em execução, mesmo se você forçar e executar várias instâncias <your_script>simultaneamente.


11
Obrigado pelo link, mas você poderia incluir as partes essenciais em sua resposta? É política comum no SE impedir a podridão do link ... Mas algo como isso [[(lsof $0 | wc -l) > 2]] && exitpode ser suficiente, ou isso também é propenso a condições de corrida?
Tobias Kienzler

Você está certo, a parte essencial da minha resposta estava faltando e apenas a publicação de links é muito ruim. Eu adicionei minha própria sugestão à resposta.
user1146332

3

Outra maneira de garantir que uma única instância do script bash seja executada:

#!/bin/bash

# Check if another instance of script is running
pidof -o %PPID -x $0 >/dev/null && echo "ERROR: Script $0 already running" && exit 1

...

pidof -o %PPID -x $0 obtém o PID do script existente se ele já estiver em execução ou sai com o código de erro 1 se nenhum outro script estiver em execução


3

Embora você tenha solicitado uma solução sem ferramentas adicionais, esta é a minha maneira favorita de usar flock:

#!/bin/sh

[ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en "$0" "$0" "$@" || :

echo "servus!"
sleep 10

Isso vem da seção de exemplos man flock, que explica ainda:

Esse é um código útil para scripts de shell. Coloque-o na parte superior do script de shell que você deseja bloquear e ele se bloqueará automaticamente na primeira execução. Se o env var $ FLOCKER não estiver definido como o shell script que está sendo executado, execute o flock e pegue um bloqueio exclusivo sem bloqueio (usando o próprio script como arquivo de bloqueio) antes de se executar novamente com os argumentos corretos. Ele também define o env var FLOCKER para o valor certo, para que não seja executado novamente.

Pontos a considerar:

  • Requer flock, o script de exemplo termina com um erro se não puder ser encontrado
  • Não precisa de arquivo de bloqueio extra
  • Pode não funcionar se o script estiver no NFS (consulte /server/66919/file-locks-on-an-nfs )

Consulte também /programming/185451/quick-and-dirty-way-to-ensure-only-one-instance-of-a-shell-script-is-running-at .


1

Esta é uma versão modificada da resposta de Anselmo . A idéia é criar um descritor de arquivo somente leitura usando o próprio script bash e use flockpara manipular o bloqueio.

SCRIPT=`realpath $0`     # get absolute path to the script itself
exec 6< "$SCRIPT"        # open bash script using file descriptor 6
flock -n 6 || { echo "ERROR: script is already running" && exit 1; }   # lock file descriptor 6 OR show error message if script is already running

echo "Run your single instance code here"

A principal diferença para todas as outras respostas é que esse código não modifica o sistema de arquivos, usa uma área muito baixa e não precisa de nenhuma limpeza, pois o descritor de arquivo é fechado assim que o script termina independentemente do estado de saída. Portanto, não importa se o script falhar ou for bem-sucedido.


Você deve sempre citar todas as referências de variáveis ​​do shell, a menos que tenha um bom motivo para não fazê- lo e tenha certeza de que sabe o que está fazendo. Então você deveria estar fazendo exec 6< "$SCRIPT".
Scott

@ Scott Alterei o código de acordo com suas sugestões. Muito Obrigado.
31419 John Doe

1

Estou usando o cksum para verificar se meu script está realmente executando uma instância única, mesmo que eu mude o nome do arquivo e o caminho do arquivo .

Não estou usando o arquivo de interceptação e bloqueio, porque se meu servidor estiver inoperante repentinamente, preciso remover manualmente o arquivo de bloqueio após a inicialização do servidor.

Nota: #! / Bin / bash na primeira linha é necessário para grep ps

#!/bin/bash

checkinstance(){
   nprog=0
   mysum=$(cksum $0|awk '{print $1}')
   for i in `ps -ef |grep /bin/bash|awk '{print $2}'`;do 
        proc=$(ls -lha /proc/$i/exe 2> /dev/null|grep bash) 
        if [[ $? -eq 0 ]];then 
           cmd=$(strings /proc/$i/cmdline|grep -v bash)
                if [[ $? -eq 0 ]];then 
                   fsum=$(cksum /proc/$i/cwd/$cmd|awk '{print $1}')
                   if [[ $mysum -eq $fsum ]];then
                        nprog=$(($nprog+1))
                   fi
                fi
        fi
   done

   if [[ $nprog -gt 1 ]];then
        echo $0 is already running.
        exit
   fi
}

checkinstance 

#--- run your script bellow 

echo pass
while true;do sleep 1000;done

Ou você pode codificar cksum no seu script para não se preocupar novamente se quiser alterar o nome do arquivo, o caminho ou o conteúdo do script .

#!/bin/bash

mysum=1174212411

checkinstance(){
   nprog=0
   for i in `ps -ef |grep /bin/bash|awk '{print $2}'`;do 
        proc=$(ls -lha /proc/$i/exe 2> /dev/null|grep bash) 
        if [[ $? -eq 0 ]];then 
           cmd=$(strings /proc/$i/cmdline|grep -v bash)
                if [[ $? -eq 0 ]];then 
                   fsum=$(grep mysum /proc/$i/cwd/$cmd|head -1|awk -F= '{print $2}')
                   if [[ $mysum -eq $fsum ]];then
                        nprog=$(($nprog+1))
                   fi
                fi
        fi
   done

   if [[ $nprog -gt 1 ]];then
        echo $0 is already running.
        exit
   fi
}

checkinstance

#--- run your script bellow

echo pass
while true;do sleep 1000;done

11
Explique exatamente como codificar a soma de verificação é uma boa ideia.
Scott

não codificando a soma de verificação, ela apenas cria a chave de identidade do seu script; quando outra instância estiver em execução, ela verificará outro processo de script de shell e classificará o arquivo primeiro, se a sua chave de identidade estiver nesse arquivo, significa que a instância já está em execução.
arputra

ESTÁ BEM; por favor edite sua resposta para explicar isso. E, no futuro, não publique vários blocos de código de 30 linhas que parecem (quase) idênticos sem dizer e explicar como são diferentes. E não diga coisas como “você pode codificar [sic] cksum dentro de seu script” e não continue usando nomes de variáveis mysume fsum, quando não estiver mais falando sobre uma soma de verificação.
Scott

Parece interessante, obrigado! E bem-vindo ao unix.stackexchange :)
Tobias Kienzler


0

Meu código para você

#!/bin/bash

script_file="$(/bin/readlink -f $0)"
lock_file=${script_file////_}

function executing {
  echo "'${script_file}' already executing"
  exit 1
}

(
  flock -n 9 || executing

  sleep 10

) 9> /var/lock/${lock_file}

Com base em man flock, melhorando apenas:

  • o nome do arquivo de bloqueio, com base no nome completo do script
  • a mensagem executing

Onde eu coloquei aqui o sleep 10, você pode colocar todo o script principal.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.