Como fazer com que a leitura e a gravação do mesmo arquivo no mesmo pipeline sempre “falhem”?

9

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 principal, leio e escrevo o mesmo arquivo tmpque às vezes falha.

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

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

Obrigado por suas idéias.

karlosss
fonte

Respostas:

2

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

Existe alguma maneira de forçar esse script a gerar sempre 0 linhas (para que o redirecionamento de E / S para tmp seja sempre preparado primeiro e para que os dados sejam sempre destruídos)? Para ser claro, quero dizer alterar as configurações do sistema

IDK, se já existe uma ferramenta para isso, mas tenho uma ideia de como uma pode ser implementada. (Mas observe que nem sempre essas linhas devem ser 0, apenas um testador útil que captura facilmente corridas simples como essa e outras mais complicadas. Consulte o comentário do @Gilles .) Isso não garante que um script seja seguro , mas pode ser uma ferramenta útil no teste, semelhante ao teste de um programa multithread em diferentes CPUs, incluindo CPUs não x86 com ordem fraca, como o ARM.

Você o executaria como racechecker bash foo.sh

Use o mesmo sistema de chamada rastreamento / interceptando instalações que strace -fe ltrace -fusar para anexar a cada processo filho. (No Linux, é a mesma ptracechamada de sistema usada pelo GDB e outros depuradores para definir pontos de interrupção, etapa única e modificar a memória / registros de outro processo.)

Instrumento os opene openatsistema de chamadas: quando qualquer processo em execução sob esta ferramenta faz uma uma open(2)chamada de sistema (ou openat) com O_RDONLY, sono para talvez 1/2 ou 1 segundo. Deixe que outras openchamadas do sistema (especialmente as incluindo O_TRUNC) sejam executadas sem demora.

Isso deve permitir que o escritor vença a corrida em quase todas as condições de corrida, a menos que a carga do sistema também seja alta ou uma condição de corrida complicada em que o truncamento não aconteceu até depois de outra leitura. Portanto, variações aleatórias de quais open()s (e talvez read()s ou gravações) estão atrasadas aumentariam o poder de detecção dessa ferramenta, mas é claro que sem testes por um período infinito de tempo com um simulador de atrasos que eventualmente cobrirá todas as situações possíveis que você pode encontrar em No mundo real, você não pode ter certeza de que seus scripts estão livres de corridas, a menos que os leia com atenção e prove que não.


Você provavelmente iria precisar dele para whitelist (não demora open) para arquivos em /usr/bine /usr/libassim por processo de inicialização não leva para sempre. (A vinculação dinâmica do tempo de execução deve ter open()vários arquivos (veja strace -eopen /bin/trueou em /bin/lsalgum momento), embora se o próprio shell pai estiver executando o truncamento, tudo bem. Mas ainda será bom que 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 tem permissão para truncar em primeiro lugar. isto é, o processo de rastreamento pode fazer uma access(2)chamada do sistema antes de realmente suspender o processo que queria open()um arquivo.


racecheckerele próprio teria que ser escrito em C, não em shell, mas talvez pudesse usar straceo código como ponto de partida e não levasse muito trabalho para implementar.

Talvez você possa obter a mesma funcionalidade com um sistema de arquivos FUSE . Provavelmente, há um exemplo do FUSE de um sistema de arquivos de passagem puro, para que você possa adicionar verificações à open()função, que fazem com que seja suspenso por aberturas somente leitura, mas deixe o truncamento acontecer imediatamente.

Peter Cordes
fonte
Sua idéia para um verificador de corrida realmente não funciona. Primeiro, há o problema de que os tempos limite não são confiáveis: um dia o outro cara levará mais tempo do que o esperado (é um problema clássico com scripts de compilação ou teste, que parece funcionar por um tempo e depois falhar de maneiras difíceis de depurar quando a carga de trabalho se expande e muitas coisas são executadas em paralelo). Mas além disso, a qual abertura você adicionará um atraso? Para detectar qualquer coisa interessante, você precisa fazer várias execuções com diferentes padrões de atraso e comparar seus resultados.
Gilles 'SO- stop be evil'
@ Gilles: Certo, qualquer atraso razoavelmente curto não garante que o truncado ganhe a corrida (em uma máquina muito carregada, como você indica). A idéia aqui é que você use isso para testar seu script algumas vezes, não que você use racecheckero tempo todo. E provavelmente você deseja que o tempo de suspensão para leitura seja configurável para o benefício de pessoas em máquinas muito carregadas que desejam aumentá-lo, como 10 segundos. Ou diminua, como 0,1 segundos, para scripts longos ou ineficientes que reabrem muito os arquivos .
Peter Cordes
@Gilles: Ótima idéia sobre os diferentes padrões de atraso, que podem permitir que você participe de mais corridas do que apenas coisas simples dentro do mesmo pipeline que "deveriam ser óbvias (depois que você souber como as conchas funcionam)" como o caso do OP. Mas "o que abre?" qualquer aberto somente leitura, com uma lista de permissões ou alguma outra maneira de não atrasar a inicialização do processo.
Peter Cordes
Eu acho que você está pensando em corridas mais complexas com trabalhos em segundo plano que não são truncados até que outro processo seja concluído? Sim, pode ser necessária variação aleatória para entender isso. Ou talvez veja a árvore do processo e adie mais a leitura "precoce" para tentar inverter a ordem usual. Você pode tornar a ferramenta cada vez mais complicada para simular cada vez mais possibilidades de reordenamento, mas em algum momento você ainda precisa projetar seus programas corretamente se estiver executando várias tarefas. O teste automatizado pode ser útil para scripts mais simples, onde os possíveis problemas são mais limitados.
Peter Cordes
É bem parecido com o teste de código multiencadeado, especialmente algoritmos sem bloqueio: o raciocínio lógico sobre o motivo da correção é muito importante, bem como o teste, porque você não pode contar com testes em nenhum conjunto específico de máquinas para produzir todos os reordenamentos que possam ser um problema se você não fechou todas as brechas. Mas, assim como testar uma arquitetura de ordem fraca, como ARM ou PowerPC, é uma boa idéia na prática, testar um script em um sistema que atrasa artificialmente as coisas pode expor algumas raças, por isso é melhor que nada. Você sempre pode introduzir bugs que não pegam!
Peter Cordes
18

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 esperar pelo outro. O ponto mais comum de sincronização é quando o lado direito bloqueia a espera de leitura dos dados em sua entrada padrão e fica desbloqueado quando o lado esquerdo grava mais dados. O inverso também pode acontecer, quando o lado direito é lento para ler dados e o lado esquerdo bloqueia em sua operação de gravação até que o lado direito leia 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 -ximprime cada comando à medida que o executa):

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; }'

Brinque com variações até se sentir confortável com o que observa.

Dado o comando composto

cat tmp | head -1 > tmp

o processo do lado esquerdo faz o seguinte (listei apenas as etapas relevantes à minha explicação):

  1. Execute o programa externo catcom o argumento tmp.
  2. Aberto tmppara leitura.
  3. Enquanto não chegou ao final do arquivo, leia um pedaço do arquivo e grave-o na saída padrão.

O processo do lado direito faz o seguinte:

  1. Redirecione a saída padrão para tmp, truncando o arquivo no processo.
  2. Execute o programa externo headcom 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 processe uma linha completa. Não há sincronização entre esquerda-2 e direita-1, para que elas possam ocorrer em qualquer ordem. Em que ordem elas acontecem não é previsível: depende da arquitetura da CPU, do shell, do kernel, de quais núcleos os processos estão agendados, do que interrompe a CPU que recebe nessa época, etc.

Como mudar o comportamento

Você não pode alterar o comportamento alterando uma configuração do sistema. O computador faz o que você pede. Você disse para truncar tmpe ler tmpem paralelo, o que faz as duas coisas em paralelo.

Ok, há uma “configuração do sistema” que você pode alterar: você pode substituir /bin/bashpor um programa diferente que não seja do bash. Espero que seja desnecessário dizer que essa não é uma boa ideia.

Se você deseja que o truncamento ocorra antes do lado esquerdo do tubo, é 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 idéia do por que você iria querer isso. Qual é o sentido de ler um arquivo que você sabe que está vazio?

Por outro lado, se você deseja que o redirecionamento de saída (incluindo o truncamento) ocorra após a catconclusão da leitura, será necessário armazenar em buffer os dados na memória, por exemplo

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

ou escreva para um arquivo diferente e mova-o para o lugar. Geralmente, essa é a maneira robusta de fazer as coisas em scripts e tem a vantagem de o arquivo ser escrito por inteiro antes de ser visível pelo 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 seu objetivo era pegar scripts mal escritos e descobrir automaticamente onde eles quebram, desculpe, a vida não é tão simples. A análise de tempo de execução não encontrará o problema com segurança, porque às vezes cattermina 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 encontrar um problema semelhante em um script mais complexo.

Gilles 'SO- parar de ser mau'
fonte
Esse era o meu objetivo, determinar se o script está bem escrito ou não. Se o script pode ter destruído dados dessa maneira, eu só queria que eles fossem destruídos todas as vezes. Não é bom ouvir que isso é quase impossível. Graças a você, agora sei qual é o problema e tentarei pensar em uma solução.
karlosss
@karlosss: Hmm, gostaria de saber se você poderia usar o mesmo material de rastreamento / interceptação de chamadas de sistema que strace(por exemplo, Linux ptrace) para fazer openchamadas de sistema com leitura em todos os processos (em todos os processos filhos) dormir por meio segundo, por isso, ao competir com um truncamento, o truncamento quase sempre vencerá.
Peter Cordes
@ PeterCordes Eu sou um novato nisso, se você conseguir uma maneira de conseguir isso e escrever como resposta, eu aceito.
karlosss
@ PeterCordes Você não pode garantir que o truncamento vencerá com um atraso. Funcionará na maioria das vezes, mas, ocasionalmente, em uma máquina muito carregada, seu script falhará de maneiras mais ou menos misteriosas.
Gilles 'SO- stop be evil'
@ Gilles: Vamos discutir isso sob a minha resposta.
Peter Cordes