Como fazer com que ler e escrever o mesmo arquivo no mesmo pipeline sempre “falhe”?

8

Digamos que eu tenha o seguinte script:

#!/bin/bash
for i in $(seq 1000)
do
    cp /etc/passwd tmp
    cat tmp | head -1 | head -1 | head -1 > tmp  #this is the key line
    cat tmp
done

Na linha chave, eu leio e escrevo o mesmo arquivo tmp , que às vezes falha.

(Eu li isso é por causa das condições de corrida porque os processos no pipeline são executados em paralelo, o que eu não entendo por que - cada head precisa pegar os dados do anterior, não é? NÃO é minha pergunta principal, mas você pode responder também.)

Quando executo o script, ele gera cerca de 200 linhas. Existe alguma maneira que eu possa forçar este script para saída sempre 0 linhas (assim o redirecionamento de E / S para tmp é sempre preparado primeiro e assim os dados são sempre destruídos)? Para ser claro, quero dizer alterar as configurações do sistema, não este script.

Obrigado pelas suas ideias.

    
por karlosss 09.12.2017 / 12:20

2 respostas

2

A resposta de Gilles explica a condição da corrida. Eu só vou responder a esta parte:

Is there any way I can force this script to output always 0 lines (so the I/O redirection to tmp is always prepared first and so the data is always destroyed)? To be clear, I mean changing the system settings

IDK, se já existe uma ferramenta para isso, mas tenho uma ideia de como alguém pode ser implementado. (Mas note que isso não seria sempre 0 linhas, apenas um testador útil que pega facilmente raças simples como esta, e algumas raças mais complicadas. Veja @ Comentário de Gilles .) Não garante que um script seja seguro , mas pode ser uma ferramenta útil em testes, semelhante ao teste de um programa multi-thread em diferentes CPUs, incluindo CPUs não-x86 fracamente ordenadas como ARM.

Você executaria isso como racechecker bash foo.sh

Use os mesmos recursos de rastreamento / interceptação de chamadas do sistema que strace -f e ltrace -f usam para anexar a cada processo filho. (No Linux, essa é a mesma ptrace chamada do sistema usada pelo GDB e outros depuradores para definir pontos de parada, etapa única, e modificar memória / registros de outro processo.)

Instrua as chamadas de sistema open e openat : quando qualquer processo executado sob essa ferramenta fizer uma uma chamada de sistema open(2) (ou openat ) com O_RDONLY , dormir por talvez 1/2 ou 1 segundo. Deixe que outras chamadas do sistema open (especialmente as que incluem O_TRUNC ) sejam executadas sem atraso.

Isso deve permitir que o escritor ganhe a corrida em quase todas as condições de corrida, a menos que a carga do sistema também seja alta, ou seja, uma condição de corrida complicada na qual o truncamento não aconteceu até depois de alguma outra leitura. Então a variação aleatória de qual open() s (e talvez read() s ou gravações) está atrasada aumentaria o poder de detecção dessa ferramenta, mas é claro, sem testes por um período infinito de tempo com um simulador de atraso que acabará por cobrir todas as situações possíveis que você pode encontrar no mundo real, você não pode ter certeza de que seus scripts estão livres de corridas, a menos que você os leia atentamente e prove que eles não são.

Provavelmente, você precisará da lista de permissões (não demora open ) para arquivos em /usr/bin e /usr/lib , para que a inicialização do processo não demore para sempre. (A vinculação dinâmica em tempo de execução precisa de open() multiple files (veja em strace -eopen /bin/true ou /bin/ls sometime), embora, se o próprio shell pai estiver fazendo o truncamento, isso será ok. Mas ainda será bom para essa ferramenta não torne os scripts excessivamente lentos).

Ou, talvez, coloque na lista de permissões todos os arquivos que o processo de chamada não tenha permissão para truncar em primeiro lugar. ou seja, o processo de rastreamento pode fazer uma chamada de sistema access(2) antes de realmente suspender o processo que queria open() um arquivo.

racechecker teria que ser escrito em C, não no shell, mas poderia usar o código de strace como ponto de partida e pode não precisar de muito trabalho para implementar.

Você talvez tenha a mesma funcionalidade com um sistema de arquivos FUSE . Provavelmente há um exemplo de FUSE de um sistema de arquivos puro de passagem, portanto, você poderia adicionar verificações à função open() naquilo que faz com que ele durma para somente leitura, mas permita que o truncamento aconteça imediatamente.

    
por 09.12.2017 / 15:45
17

Por que existe uma condição de corrida

Os dois lados de um tubo são executados em paralelo, não um após o outro. Existe uma maneira muito simples de demonstrar isso: execute

time sleep 1 | sleep 1

Isso leva um segundo, não dois.

O shell inicia dois processos filhos e aguarda a conclusão de ambos. Esses dois processos são executados em paralelo: a única razão pela qual um deles seria sincronizado com o outro é quando ele precisa para esperar pelo outro. O ponto de sincronização mais comum é quando o lado direito bloqueia a espera de dados para ler em sua entrada padrão e se torna desbloqueado quando o lado esquerdo grava mais dados. O inverso também pode acontecer, quando o lado direito é lento para ler dados e os blocos do lado esquerdo em sua operação de gravação até que o lado direito lê mais dados (há um buffer no próprio tubo, gerenciado pelo kernel, mas tem um tamanho máximo pequeno).

Para observar um ponto de sincronização, observe os seguintes comandos ( sh -x imprime cada comando à medida que é executado):

time sh -x -c '{ sleep 1; echo a; } | { cat; }'
time sh -x -c '{ echo a; sleep 1; } | { cat; }'
time sh -x -c '{ echo a; sleep 1; } | { sleep 1; cat; }'
time sh -x -c '{ sleep 2; echo a; } | { cat; sleep 1; }'

Jogue com variações até ficar satisfeito com o que você observa.

Dado o comando composto

cat tmp | head -1 > tmp

o processo à esquerda faz o seguinte (listei apenas as etapas relevantes para minha explicação):

  1. Execute o programa externo cat com o argumento tmp .
  2. Abra tmp para leitura.
  3. Embora não tenha chegado ao final do arquivo, leia um trecho do arquivo e grave-o na saída padrão.

O processo à direita faz o seguinte:

  1. Redirecione a saída padrão para tmp , truncando o arquivo no processo.
  2. Execute o programa externo head com o argumento -1 .
  3. Leia uma linha da entrada padrão e grave-a na saída padrão.

O único ponto de sincronização é que o right-3 espera que o left-3 tenha processado uma linha completa. Não há sincronização entre o left-2 e o right-1, para que eles possam acontecer em qualquer ordem. Em que ordem eles acontecem não é previsível: depende da arquitetura da CPU, do shell, do kernel, em que núcleos os processos são agendados, em que interrupções a CPU recebe por volta dessa hora, etc.

Como mudar o comportamento

Você não pode alterar o comportamento alterando uma configuração do sistema. O computador faz o que você diz para fazer. Você disse para truncar tmp e ler em tmp em paralelo, então faz as duas coisas em paralelo.

Ok, há uma "configuração do sistema" que você pode alterar: você pode substituir /bin/bash por um programa diferente que não seja bash. Espero que seja óbvio que isto não é uma boa ideia.

Se você quiser que o truncamento aconteça antes do lado esquerdo do pipe, será necessário colocá-lo fora do pipeline, por exemplo:

{ cat tmp | head -1; } >tmp

ou

( exec >tmp; cat tmp | head -1 )

Eu não tenho ideia de por que você gostaria disso. Qual é o sentido em ler de um arquivo que você sabe estar vazio?

Por outro lado, se você quiser que o redirecionamento de saída (incluindo o truncamento) aconteça depois que cat tiver terminado a leitura, será necessário armazenar completamente os dados na memória, por exemplo,

line=$(cat tmp | head -1)
printf %s "$line" >tmp

ou escreva em um arquivo diferente e, em seguida, mova-o para o lugar. Esta é geralmente a maneira robusta de fazer coisas em scripts e tem a vantagem de que o arquivo é escrito integralmente antes de ser visível através do nome original.

cat tmp | head -1 >new && mv new tmp

A coleção moreutils inclui um programa que faz exatamente isso, chamado sponge .

cat tmp | head -1 | sponge tmp

Como detectar o problema automaticamente

Se o seu objetivo era pegar scripts mal escritos e descobrir automaticamente onde eles quebram, então desculpe, a vida não é tão simples assim. A análise em tempo de execução não encontrará o problema de forma confiável, porque às vezes cat termina a leitura antes que o truncamento aconteça. A análise estática pode, em princípio, fazê-lo; o exemplo simplificado da sua pergunta é capturado pelo Shellcheck , mas pode não detectar um problema semelhante em um script mais complexo.

    
por 09.12.2017 / 13:25