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.

Tobias Kienzler
fonte
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.

Bruce Ediger
fonte
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
Igor Zevaka
fonte
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
terdon
fonte
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
nsg
fonte
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.

user1146332
fonte
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

Sethu
fonte
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 .

schieferstapel
fonte
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.

John Doe
fonte
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
arputra
fonte
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

Você pode usar isso: https://github.com/sayanarijit/pidlock

sudo pip install -U pidlock

pidlock -n sleepy_script -c 'sleep 10'
Arijit Basu
fonte
> Uma solução que não requer ferramentas adicionais seria preferida.
dhag 12/01
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.

Anselmo Blanco Dominguez
fonte