Pesquisa de site

Evite esses problemas limitando a execução de scripts Bash uma vez por vez


Principais conclusões

  • Certifique-se de que apenas uma única instância do seu script esteja em execução usando pgrep, lsof ou rebanho para evitar problemas de simultaneidade.
  • Implemente facilmente verificações para encerrar automaticamente um script se outras instâncias em execução forem detectadas.
  • Aproveitando os comandos exec e env, o comando rebanho pode conseguir tudo isso com uma linha de código.

Alguns scripts do Linux têm essa sobrecarga de execução; a execução de várias instâncias ao mesmo tempo precisa ser evitada. Felizmente, existem várias maneiras de conseguir isso em seus próprios scripts Bash.

Às vezes, uma vez é suficiente

Alguns scripts não devem ser iniciados se uma instância anterior desse script ainda estiver em execução. Se o seu script consumir tempo excessivo de CPU e RAM, ou gerar muita largura de banda de rede ou sobrecarga de disco, limitar sua execução a uma instância por vez é bom senso.

Mas não são apenas os consumidores de recursos que precisam funcionar isoladamente. Se o seu script modificar arquivos, poderá haver um conflito entre duas (ou mais) instâncias do script enquanto elas disputam o acesso aos arquivos. As atualizações podem ser perdidas ou o arquivo pode estar corrompido.

Uma técnica para evitar esses cenários problemáticos é fazer com que o script verifique se não há outras versões dele em execução. Se detectar qualquer outra cópia em execução, o script será encerrado automaticamente.

Outra técnica é projetar o script de forma que ele se bloqueie quando for iniciado, impedindo a execução de outras cópias.

Veremos dois exemplos da primeira técnica e, em seguida, veremos uma maneira de fazer a segunda.

Usando pgrep para evitar simultaneidade

O comando pgrep pesquisa os processos em execução em um computador Linux e retorna o ID do processo que corresponde ao padrão de pesquisa.

Eu tenho um script chamado loop.sh. Ele contém um loop for que imprime a iteração do loop e depois dorme por um segundo. Faz isso dez vezes.

#!/bin/bash
for (( i=1; i<=10; i+=1 ))
do
 echo "Loop:" $i
 sleep 1
done
exit 0

Eu configurei duas instâncias dele em execução e usei o pgrep para procurá-lo pelo nome.

pgrep loop.sh

Ele localiza as duas instâncias e relata seus IDs de processo. Podemos adicionar a opção -c (count) para que pgrep retorne o número de instâncias.

pregp -c loop.sh

Podemos usar essa contagem de instâncias em nosso script. Se o valor retornado por pgrep for maior que um, deverá haver mais de uma instância em execução e nosso script será encerrado.

Criaremos um script que usa essa técnica. Chamaremos isso de pgrep-solo.sh.

A comparação if testa se o número retornado por pgrep é maior que um. Se for, o script é encerrado.

# count the instances of this script 
if [ $(pgrep -c pgrep-solo.sh) -gt 1 ]; then
 echo "Another instance of $0 is running. Stopping."
 exit 1
fi

Se o número retornado por pgrep for um, o script poderá continuar. Aqui está o script completo.

#!/bin/bash
echo "Starting."
# count the instances of this script 
if [ $(pgrep -c pgrep-solo.sh) -gt 1 ]; then
 echo "Another instance of $0 is running. Stopping."
 exit 1
fi
# we're cleared for take off
for (( i=1; i<=10; i+=1 ))
do
 echo "Loop:" $i
 sleep 1
done
exit 0

Copie para o seu editor favorito e salve-o como pgrep-solo.sh. Em seguida, torne-o executável com chmod.

chmod +x pgrep-loop.sh

Quando é executado, fica assim.

./pgrep-solo.sh

Mas se eu tentar iniciá-lo com outra cópia já em execução em outra janela do terminal, ele detectará isso e sairá.

./pgrep-solo.sh

Usando lsof para evitar simultaneidade

Podemos fazer algo muito semelhante com o comando lsof.

Se adicionarmos a opção -t (concisa), lsof listará os IDs do processo.

lsof -t loop.sh

Podemos canalizar a saída de lsof para wc. A opção -l (linhas) conta o número de linhas que, neste cenário, é igual ao número de IDs de processo.

lsof -t loop.sh | wc -l

Podemos usar isso como base do teste na comparação if em nosso script.

Salve esta versão como lsof-solo.sh.

#!/bin/bash
echo "Starting."
# count the instances of this script 
if [ $(lsof -t "$0" | wc -l) -gt 1 ]; then
 echo "Another instance of $0 is running. Stopping."
 exit 1
fi
# we're cleared for take off
for (( i=1; i<=10; i+=1 ))
do
 echo "Loop:" $i
 sleep 1
done
exit 0

Use chmod para torná-lo executável.

chmod +x lsof-solo.sh

Agora, com o script lsof-solo.sh em execução em outra janela do terminal, não podemos iniciar uma segunda cópia.

./lsof-solo.sh

O método pgrep requer apenas uma única chamada para um programa externo (pgrep), o método lsof requer duas (lsof e wc). Mas a vantagem que o método lsof tem sobre o método pgrep é que você pode usar a variável $0 na comparação if. Isso contém o nome do script.

Isso significa que você pode renomear o script e ele ainda funcionará. Você não precisa se lembrar de editar a linha de comparação if e inserir o novo nome do script. A variável $0 inclui ‘./’ no início do nome do script (como ./lsof-solo.sh), e pgrep não gosta disso.

Usando rebanho para evitar simultaneidade

Nossa terceira técnica usa o comando rebanho, que é projetado para definir bloqueios de arquivos e diretórios dentro de scripts. Enquanto estiver bloqueado, nenhum outro processo poderá acessar o recurso bloqueado.

Este método requer que uma única linha seja adicionada na parte superior do seu script.

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

Decodificaremos esses hieróglifos em breve. Por enquanto, vamos apenas verificar se funciona. Salve este como rebanho-solo.sh.

#!/bin/bash
[ "${GEEKLOCK}" != "$0" ] && exec env GEEKLOCK="$0" flock -en "$0" "$0" "$@" || :
echo "Starting."
# we're cleared for take off
for (( i=1; i<=10; i+=1 ))
do
 echo "Loop:" $i
 sleep 1
done
exit 0

Claro, precisamos torná-lo executável.

chmod +x flock-solo.sh

Iniciei o script em uma janela de terminal e tentei executá-lo novamente em uma janela de terminal diferente.

./flock-solo
./flock-solo
./flock-solo

Não consigo iniciar o script até que a instância na outra janela do terminal seja concluída.

Vamos desmarcar a linha que faz a mágica. No centro disso está o comando do rebanho.

flock -en "$0" "$0" "$@"

O comando rebanho é usado para bloquear um arquivo ou diretório e, em seguida, executar um comando. As opções que estamos usando são -e (exclusivo) e -n (não bloqueador).

A opção exclusiva significa que se conseguirmos bloquear o arquivo, ninguém mais poderá acessá-lo. A opção sem bloqueio significa que se não conseguirmos obter um bloqueio, pararemos imediatamente de tentar. Não tentamos novamente por um período de tempo, nós nos retiramos graciosamente imediatamente.

O primeiro $0 indica o arquivo que desejamos bloquear. Esta variável contém o nome do script atual.

O segundo $0 é o comando que queremos executar se conseguirmos obter um bloqueio. Novamente, estamos passando o nome deste script. Como o bloqueio bloqueia todos exceto nós, podemos iniciar o arquivo de script.

Podemos passar parâmetros para o comando que é lançado. Estamos usando $@ para passar quaisquer parâmetros de linha de comando que foram passados para este script, para a nova invocação do script que será lançado pelo rebanho.

Então temos esse script bloqueando o arquivo de script e iniciando outra instância dele mesmo. Isso é quase o que queremos, mas há um problema. Quando a segunda instância for concluída, o primeiro script retomará seu processamento. No entanto, temos outro truque na manga para resolver isso, como você verá.

Estamos usando uma variável de ambiente que chamamos de GEEKLOCK para indicar se um script em execução precisa aplicar o bloqueio ou não. Se o script tiver sido iniciado e não houver nenhum bloqueio em vigor, o bloqueio deverá ser aplicado. Se o script foi iniciado e há um bloqueio em vigor, ele não precisa fazer nada, ele pode apenas ser executado. Com um script em execução e o bloqueio ativado, nenhuma outra instância do script poderá ser iniciada.

[ "${GEEKLOCK}" != "$0" ] 

Este teste se traduz em 'retornar verdadeiro se a variável de ambiente GEEKLOCK não estiver definida como o nome do script.' O teste é encadeado ao resto do comando por && (e) e || (ou). A parte && será executada se o teste retornar verdadeiro e o || seção é executada se o teste retornar falso.

env GEEKLOCK="$0"

O comando env é usado para executar outros comandos em ambientes modificados. Estamos modificando nosso ambiente criando a variável de ambiente GEEKLOCK e definindo-a com o nome do script. O comando que env irá iniciar é o comando rebanho, e o comando rebanho inicia a segunda instância do script.

A segunda instância do script realiza sua verificação para ver se a variável de ambiente GEEKLOCK não existe, mas descobre que existe. O || A seção do comando é executada, que contém apenas dois pontos ‘:’, que na verdade é um comando que não faz nada. O caminho de execução percorre o restante do script.

Mas ainda temos o problema do primeiro script continuar seu próprio processamento quando o segundo script for finalizado. A solução para isso é o comando exec. Isso executa outros comandos, substituindo o processo de chamada pelo processo recém-iniciado.

exec env GEEKLOCK="$0" flock -en "$0" "$0" "$@" 

Então a sequência completa é:

  • O script é iniciado e não consegue encontrar a variável de ambiente. A cláusula && é executada.
  • exec inicia env e substitui o processo de script original pelo novo processo env.
  • O processo env cria a variável de ambiente e inicia o rebanho.
  • rebanho bloqueia o arquivo de script e inicia uma nova instância do script que detecta a variável de ambiente, executa o || cláusula e o script é capaz de ser executado até sua conclusão.
  • Como o script original foi substituído pelo processo env, ele não está mais presente e não pode continuar sua execução quando o segundo script terminar.
  • Como o arquivo de script é bloqueado durante a execução, outras instâncias não podem ser iniciadas até que o script iniciado pelo rebanho pare de ser executado e libere o bloqueio.

Isso pode soar como o enredo de Inception, mas funciona perfeitamente. Essa linha certamente é um golpe.

Para maior clareza, é o bloqueio no arquivo de script que impede a inicialização de outras instâncias, não a detecção da variável de ambiente. A variável de ambiente apenas informa a um script em execução para definir o bloqueio ou que o bloqueio já está em vigor.

Bloquear e carregar

É mais fácil do que você imagina garantir que apenas uma única instância de um script seja executada por vez. Todas essas três técnicas funcionam. Embora seja o mais complicado em operação, o rebanho one-liner é o mais fácil de inserir em qualquer script.