Como esse script garante que apenas uma instância esteja em execução?

22

Em 19 de agosto de 2013, Randal L. Schwartz publicou este shell script, que pretendia garantir, no Linux, "que apenas uma instância do [the] script esteja sendo executada, sem condições de corrida ou com a necessidade de limpar arquivos de bloqueio":

#!/bin/sh
# randal_l_schwartz_001.sh
(
    if ! flock -n -x 0
    then
        echo "$$ cannot get flock"
        exit 0
    fi
    echo "$$ start"
    sleep 10 # for testing.  put the real task here
    echo "$$ end"
) < $0

Parece funcionar como anunciado:

$ ./randal_l_schwartz_001.sh & ./randal_l_schwartz_001.sh
[1] 11863
11863 start
11864 cannot get flock
$ 11863 end

[1]+  Done                    ./randal_l_schwartz_001.sh
$

Aqui está o que eu entendo:

  • O script redireciona ( <) uma cópia de seu próprio conteúdo (isto é, de $0) para o STDIN (isto é, descritor de arquivo 0) de um subshell.
  • Dentro do subshell, o script tenta obter um lock exclusivo (não bloqueador flock -n -x) no descritor de arquivo 0.
    • Se essa tentativa falhar, o subshell será encerrado (e o script principal também, pois não há mais nada a fazer).
    • Se a tentativa for bem-sucedida, o subshell executará a tarefa desejada.

Aqui estão as minhas perguntas:

  • Por que o script precisa redirecionar, para um descritor de arquivo herdado pelo subshell, uma cópia de seu próprio conteúdo, em vez de, digamos, o conteúdo de outro arquivo? (Tentei redirecionar de um arquivo diferente e executar novamente como acima, e a ordem de execução mudou: a tarefa sem segundo plano ganhou o bloqueio antes da anterior. Portanto, talvez o uso do próprio conteúdo do arquivo evite condições de corrida; mas como?)
  • Por que o script precisa redirecionar, para um descritor de arquivo herdado pelo subshell, uma cópia do conteúdo de um arquivo, afinal?
  • Por que manter um bloqueio exclusivo no descritor de arquivo 0em um shell impede que uma cópia do mesmo script, executado em um shell diferente, obtenha um bloqueio exclusivo no descritor de arquivo 0? Não conchas têm suas próprias cópias, separadas dos descritores de arquivo padrão ( 0, 1e 2, ou seja, STDIN, STDOUT e STDERR)?
sampablokuper
fonte
Qual foi o seu processo de teste exato quando você tentou redirecionar sua experiência de um arquivo diferente?
Freiheit
1
Eu acho que você pode consultar este link. stackoverflow.com/questions/185451/…
Deb Paikar

Respostas:

22

Por que o script precisa redirecionar, para um descritor de arquivo herdado pelo subshell, uma cópia de seu próprio conteúdo, em vez de, digamos, o conteúdo de outro arquivo?

Você pode usar qualquer arquivo, desde que todas as cópias do script usem o mesmo. O uso $0apenas vincula o bloqueio ao próprio script: se você copiar o script e modificá-lo para outro uso, não precisará criar um novo nome para o arquivo de bloqueio. Isso é conveniente.

Se o script for chamado por meio de um link simbólico, o bloqueio estará no arquivo real, e não no link.

(Obviamente, se algum processo executa o script e atribui a ele um valor inventado como o argumento zeroth em vez do caminho real, isso é interrompido. Mas isso raramente é feito.)

(Tentei usar um arquivo diferente e executar novamente como acima, e a ordem de execução foi alterada)

Você tem certeza de que foi por causa do arquivo usado, e não apenas variação aleatória? Como em um pipeline, não há realmente nenhuma maneira de ter certeza de em que ordem os comandos serão executados cmd1 & cmd. Depende principalmente do agendador do SO. Eu recebo variação aleatória no meu sistema.

Por que o script precisa redirecionar, para um descritor de arquivo herdado pelo subshell, uma cópia do conteúdo de um arquivo, afinal?

Parece que é assim que o próprio shell mantém uma cópia da descrição do arquivo que contém o bloqueio, em vez de apenas o flockutilitário que o contém. Um bloqueio feito com flock(2)é liberado quando os descritores de arquivo que o possuem são fechados.

flockpossui dois modos, para bloquear um bloqueio com base no nome de um arquivo e executar um comando externo (nesse caso, flockcontém o descritor de arquivo aberto necessário) ou pegar um descritor de arquivo de fora, portanto, um processo externo é responsável por manter isto.

Observe que o conteúdo do arquivo não é relevante aqui e não há cópias feitas. O redirecionamento para o subshell não copia dados em si, apenas abre um identificador para o arquivo.

Por que manter um bloqueio exclusivo no descritor de arquivo 0 em um shell impede que uma cópia do mesmo script, executado em um shell diferente, obtenha um bloqueio exclusivo no descritor de arquivo 0? Os shells não têm cópias próprias e separadas dos descritores de arquivo padrão (0, 1 e 2, ou seja, STDIN, STDOUT e STDERR)?

Sim, mas o bloqueio está no arquivo , não no descritor do arquivo. Somente uma instância aberta do arquivo pode reter o bloqueio por vez.


Eu acho que você deve conseguir fazer o mesmo sem o subshell, usando execpara abrir um identificador para o arquivo de bloqueio:

$ cat lock.sh
#!/bin/sh

exec 9< "$0"

if ! flock -n -x 9; then
    echo "$$/$1 cannot get flock" 
    exit 0
fi

echo "$$/$1 got the lock"
sleep 2
echo "$$/$1 exit"

$ ./lock.sh bg & ./lock.sh fg ; wait; echo
[1] 11362
11363/fg got the lock
11362/bg cannot get flock
11363/fg exit
[1]+  Done                    ./lock.sh bg
ilkkachu
fonte
1
Usar em { }vez de ( )também funcionaria e evitaria o subshell.
R ..
Mais abaixo nos comentários no post do G +, alguém também sugeriu aproximadamente o mesmo método usando exec.
David Z
@R .., claro. Mas ainda é feio com os aparelhos extras em torno do script real.
ilkkachu 4/01
9

Um bloqueio de arquivo é anexado a um arquivo, por meio de uma descrição do arquivo . Em um nível alto, a sequência de operações em uma instância do script é:

  1. Abra o arquivo ao qual o bloqueio está anexado ("o arquivo de bloqueio").
  2. Faça um bloqueio no arquivo de bloqueio.
  3. Fazer coisas.
  4. Feche o arquivo de bloqueio. Isso libera o bloqueio anexado à descrição do arquivo criada pela abertura de um arquivo.

Manter o bloqueio impede que outra cópia do mesmo script seja executada, porque é isso que os bloqueios fazem. Desde que exista um bloqueio exclusivo em um arquivo em algum lugar do sistema, é impossível criar uma segunda instância do mesmo bloqueio, mesmo através de uma descrição de arquivo diferente.

Abrir um arquivo cria uma descrição do arquivo . Este é um objeto do kernel que não tem muita visibilidade direta nas interfaces de programação. Você acessa uma descrição do arquivo indiretamente por meio de descritores de arquivos, mas normalmente pensa nisso como acessando o arquivo (lendo ou gravando seu conteúdo ou metadados). Um bloqueio é um dos atributos que são uma propriedade para a descrição do arquivo, em vez de um arquivo ou um descritor.

No início, quando um arquivo é aberto, a descrição do arquivo possui um único descritor de arquivo, mas mais descritores podem ser criados criando outro descritor (a dupfamília de chamadas do sistema) ou bifurcando um subprocesso (após o qual o pai e o criança tem acesso à mesma descrição do arquivo). Um descritor de arquivo pode ser fechado explicitamente ou quando o processo em que ele está morre. Quando o último descritor de arquivo anexado a um arquivo é fechado, a descrição do arquivo é fechada.

Veja como a sequência de operações acima afeta a descrição do arquivo.

  1. O redirecionamento <$0abre o arquivo de script no subshell, criando uma descrição do arquivo. Neste ponto, há um descritor de arquivo único anexado à descrição: descritor número 0 no subshell.
  2. O subshell chama flocke aguarda a saída. Enquanto o flock está em execução, há dois descritores anexados à descrição: número 0 no subshell e número 0 no processo do flock. Quando o flock assume o bloqueio, isso define uma propriedade da descrição do arquivo. Se outra descrição do arquivo já tiver um bloqueio no arquivo, o rebanho não poderá aceitá-lo, pois é um bloqueio exclusivo.
  3. O subshell faz coisas. Como ainda possui um descritor de arquivo aberto na descrição com o bloqueio, essa descrição permanece existente e mantém seu bloqueio, pois ninguém jamais remove o bloqueio.
  4. O subshell morre no parêntese de fechamento. Isso fecha o último descritor de arquivo na descrição do arquivo que possui o bloqueio, portanto o bloqueio desaparece nesse momento.

A razão pela qual o script usa um redirecionamento $0é que o redirecionamento é a única maneira de abrir um arquivo no shell, e manter um redirecionamento ativo é a única maneira de manter um descritor de arquivo aberto. O subshell nunca lê de sua entrada padrão, apenas precisa mantê-lo aberto. Em um idioma que fornece acesso direto à chamada aberta e fechada, você pode usar

fd = open($0)
flock(fd, LOCK_EX)
do stuff
close(fd)

Você pode realmente obter a mesma sequência de operações no shell se fizer o redirecionamento com o execbuiltin.

exec <$0
flock -n -x 0
# do stuff
exec <&-

O script poderia usar um descritor de arquivo diferente se quisesse continuar acessando a entrada padrão original.

exec 3<$0
flock -n -x 0
# do stuff
exec 3<&-

ou com um subshell:

(
  flock -n -x 3
  # do stuff
) 3<$0

O bloqueio não precisa estar no arquivo de script. Pode estar em qualquer arquivo que possa ser aberto para leitura (portanto, ele deve existir, deve ser um tipo de arquivo que possa ser lido, como um arquivo regular ou um pipe nomeado, mas não um diretório, e o processo de script deve ter a permissão para lê-lo). O arquivo de script tem a vantagem de garantir sua presença e legibilidade (exceto no caso de borda em que foi excluído externamente entre o momento em que o script foi chamado e o momento em que o script chega ao <$0redirecionamento).

Desde que seja flockbem-sucedido, e o script esteja em um sistema de arquivos em que os bloqueios não sejam com erros (alguns sistemas de arquivos de rede como o NFS podem ser com erros), não vejo como o uso de um arquivo de bloqueio diferente pode permitir uma condição de corrida. Suspeito de um erro de manipulação da sua parte.

Gilles 'SO- parar de ser mau'
fonte
Há uma condição de corrida: você não pode controlar qual instância do script obtém o bloqueio. Felizmente, para quase todos os fins, isso não importa.
Mark
4
@ Mark Há uma corrida para o bloqueio, mas não é uma condição de corrida. Uma condição de corrida é quando o tempo pode permitir que algo ruim aconteça, como dois processos na mesma seção crítica ao mesmo tempo. Não sabendo que processo entrará na seção crítica é esperado não determinismo, não é uma condição de corrida.
Gilles 'SO- stop be evil'
1
Apenas para sua informação, o link em "descrição do arquivo" aponta para a página de índice de especificações do Open Group em vez de para uma descrição específica do conceito, que é o que eu acho que você pretende fazer. Ou você também pode vincular sua resposta mais antiga aqui, unix.stackexchange.com/a/195164/85039
Sergiy Kolodyazhnyy
5

O arquivo usado para bloquear não é importante, o script usa $0porque é um arquivo conhecido por existir.

A ordem na qual os bloqueios são obtidos será mais ou menos aleatória, dependendo da rapidez com que sua máquina é capaz de iniciar as duas tarefas.

Você pode usar qualquer descritor de arquivo, não necessariamente 0. O bloqueio é mantido no arquivo aberto para o descritor de arquivo, não o próprio descritor.

( flock -x 9 || exit 1
  echo 'Locking for 5 secs'; sleep 5; echo 'Done' ) 9>/tmp/lock &
Kusalananda
fonte