O que impede o stdout / stderr de intercalar?

13

Digamos que eu execute alguns processos:

#!/usr/bin/env bash

foo &
bar &
baz &

wait;

Eu executo o script acima da seguinte maneira:

foobarbaz | cat

Até onde eu sei, quando qualquer um dos processos escreve para stdout / stderr, sua saída nunca se entrelaça - cada linha do stdio parece ser atômica. Como isso funciona? Qual utilitário controla como cada linha é atômica?

Alexander Mills
fonte
3
Quantos dados seus comandos geram? Tente fazê-los produzir alguns kilobytes.
Kusalananda
Você quer dizer onde um dos comandos gera alguns kb antes de uma nova linha?
Alexander Mills
Não, algo como isto: unix.stackexchange.com/a/452762/70524
muru

Respostas:

22

Eles se entrelaçam! Você tentou apenas rajadas curtas de saída, que permanecem sem divisão, mas na prática é difícil garantir que uma saída específica permaneça sem divisão.

Buffer de saída

Depende de como os programas armazenam sua saída em buffer . A biblioteca stdio que a maioria dos programas usa ao escrever usa buffers para tornar a saída mais eficiente. Em vez de emitir dados assim que o programa chama uma função de biblioteca para gravar em um arquivo, a função armazena esses dados em um buffer e, na verdade, somente os dados são emitidos após o preenchimento do buffer. Isso significa que a saída é feita em lotes. Mais precisamente, existem três modos de saída:

  • Sem buffer: os dados são gravados imediatamente, sem usar um buffer. Isso pode ser lento se o programa gravar sua saída em pedaços pequenos, por exemplo, caractere por caractere. Este é o modo padrão para erro padrão.
  • Totalmente em buffer: os dados são gravados apenas quando o buffer está cheio. Este é o modo padrão ao gravar em um canal ou em um arquivo regular, exceto com stderr.
  • Buffer de linha: os dados são gravados após cada nova linha ou quando o buffer está cheio. Este é o modo padrão ao gravar em um terminal, exceto com stderr.

Os programas podem reprogramar cada arquivo para se comportar de maneira diferente e podem liberar explicitamente o buffer. O buffer é liberado automaticamente quando um programa fecha o arquivo ou sai normalmente.

Se todos os programas que estão gravando no mesmo canal usam o modo de buffer de linha ou usam o modo sem buffer e gravam cada linha com uma única chamada para uma função de saída e se as linhas são curtas o suficiente para gravar em um único pedaço, a saída será uma intercalação de linhas inteiras. Mas se um dos programas usar o modo totalmente em buffer ou se as linhas forem muito longas, você verá linhas mistas.

Aqui está um exemplo em que intercalo a saída de dois programas. Eu usei o GNU coreutils no Linux; versões diferentes desses utilitários podem se comportar de maneira diferente.

  • yes aaaaescreve aaaapara sempre no que é essencialmente equivalente ao modo de buffer de linha. Na yesverdade, o utilitário grava várias linhas por vez, mas cada vez que emite saída, a saída é um número inteiro de linhas.
  • echo bbbb; done | grep bgrava bbbbpara sempre no modo com buffer total. Ele usa um tamanho de buffer de 8192 e cada linha tem 5 bytes de comprimento. Como 5 não divide 8192, os limites entre gravações não estão em um limite de linha em geral.

Vamos lançá-los juntos.

$ { yes aaaa & while true; do echo bbbb; done | grep b & } | head -n 999999 | grep -e ab -e ba
bbaaaa
bbbbaaaa
baaaa
bbbaaaa
bbaaaa
bbbaaaa
ab
bbbbaaa

Como você pode ver, sim, algumas vezes interrompia o grep e vice-versa. Apenas cerca de 0,001% das linhas foram interrompidas, mas aconteceu. A saída é aleatória para que o número de interrupções varie, mas eu vi pelo menos algumas interrupções todas as vezes. Haveria uma fração mais alta de linhas interrompidas se as linhas fossem mais longas, pois a probabilidade de uma interrupção aumenta à medida que o número de linhas por buffer diminui.

Existem várias maneiras de ajustar o buffer de saída . Os principais são:

  • Desative o buffer em programas que usam a biblioteca stdio sem alterar suas configurações padrão com o programa stdbuf -o0encontrado no GNU coreutils e em alguns outros sistemas como o FreeBSD. Como alternativa, você pode alternar para o buffer de linha com stdbuf -oL.
  • Alterne para o buffer de linha direcionando a saída do programa através de um terminal criado apenas para esta finalidade com unbuffer. Alguns programas podem se comportar de maneira diferente de outras maneiras, por exemplo, grepusa cores por padrão se sua saída for um terminal.
  • Configure o programa, por exemplo, passando --line-bufferedpara o GNU grep.

Vamos ver o trecho acima novamente, desta vez com buffer de linha nos dois lados.

{ stdbuf -oL yes aaaa & while true; do echo bbbb; done | grep --line-buffered b & } | head -n 999999 | grep -e ab -e ba
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb

Portanto, desta vez, o yes nunca interrompeu o grep, mas o grep às vezes interrompeu o yes. Eu irei ao porquê mais tarde.

Intercalação de tubos

Desde que cada programa produza uma linha de cada vez, e as linhas sejam curtas o suficiente, as linhas de saída serão bem separadas. Mas há um limite para quanto tempo as linhas podem demorar para que isso funcione. O próprio tubo possui um buffer de transferência. Quando um programa gera um canal, os dados são copiados do programa gravador para o buffer de transferência do canal e, posteriormente, do buffer de transferência do canal para o programa leitor. (Pelo menos conceitualmente - o kernel às vezes pode otimizar isso para uma única cópia.)

Se houver mais dados para copiar do que cabe no buffer de transferência do pipe, o kernel copia um buffer de cada vez. Se vários programas estão gravando no mesmo canal, e o primeiro programa escolhido pelo kernel deseja gravar mais de um buffer, não há garantia de que o kernel escolherá o mesmo programa novamente na segunda vez. Por exemplo, se P é o tamanho do buffer, foodeseja escrever 2 * P bytes e bardeseja escrever 3 bytes, uma intercalação possível é P bytes de foo, depois 3 bytes de bare P bytes de foo.

Voltando ao exemplo yes + grep acima, no meu sistema, yes aaaaacontece o número de linhas que cabem em um buffer de 8192 bytes de uma só vez. Como existem 5 bytes para escrever (4 caracteres imprimíveis e a nova linha), isso significa que ele grava 8190 bytes sempre. O tamanho do buffer do canal é 4096 bytes. Portanto, é possível obter 4096 bytes de yes, alguma saída do grep e o restante da gravação de yes (8190 - 4096 = 4094 bytes). 4096 bytes deixam espaço para 819 linhas com aaaae um solitário a. Daí uma linha com este solitário, aseguida por uma gravação de grep, fornecendo uma linha com abbbb.

Se você quiser ver os detalhes do que está acontecendo, getconf PIPE_BUF .informará o tamanho do buffer do canal no seu sistema e poderá ver uma lista completa das chamadas do sistema feitas por cada programa com

strace -s9999 -f -o line_buffered.strace sh -c '{ stdbuf -oL yes aaaa & while true; do echo bbbb; done | grep --line-buffered b & }' | head -n 999999 | grep -e ab -e ba

Como garantir a intercalação de linhas limpas

Se os comprimentos da linha forem menores que o tamanho do buffer do tubo, o buffer da linha garante que não haverá nenhuma linha mista na saída.

Se os comprimentos das linhas puderem ser maiores, não há como evitar a mistura arbitrária quando vários programas estiverem gravando no mesmo canal. Para garantir a separação, você precisa fazer com que cada programa grave em um canal diferente e use um programa para combinar as linhas. Por exemplo, o GNU Parallel faz isso por padrão.

Gilles 'SO- parar de ser mau'
fonte
interessante, então, o que pode ser uma boa maneira de garantir que todas as linhas sejam gravadas catatomicamente, de modo que o processo cat receba linhas inteiras de foo / bar / baz, mas não meia linha de uma e meia linha de outra etc. Existe algo que eu possa fazer com o script bash?
Alexander Mills
1
Parece que isso se aplica ao meu caso também, onde eu tinha centenas de arquivos e awkproduzi duas (ou mais) linhas de saída para o mesmo ID, find -type f -name 'myfiles*' -print0 | xargs -0 awk '{ seen[$1]= seen[$1] $2} END { for(x in seen) print x, seen[x] }' mas com find -type f -name 'myfiles*' -print0 | xargs -0 cat| awk '{ seen[$1]= seen[$1] $2} END { for(x in seen) print x, seen[x] }'ele produziu corretamente apenas uma linha para todos os IDs.
αғsнιη
Para impedir qualquer intercalação, eu posso fazer isso em um ambiente de programação como o Node.js, mas com o bash / shell, não sei como fazê-lo.
Alexander Mills
1
@JoL É devido ao enchimento do buffer do tubo. Eu sabia que teria que escrever a segunda parte da história ... Feito.
Gilles 'SO- stop be evil'
1
@OlegzandrDenman TLDR acrescentou: eles se intercalam. O motivo é complicado.
Gilles 'SO- stop be evil'
1

http://mywiki.wooledge.org/BashPitfalls#Non-atomic_writes_with_xargs_-P analisou isso:

O GNU xargs suporta a execução de vários trabalhos em paralelo. -P n em que n é o número de tarefas a serem executadas em paralelo.

seq 100 | xargs -n1 -P10 echo "$a" | grep 5
seq 100 | xargs -n1 -P10 echo "$a" > myoutput.txt

Isso funcionará bem em muitas situações, mas possui uma falha enganosa: se $ a contiver mais de ~ 1000 caracteres, o eco poderá não ser atômico (pode ser dividido em várias chamadas de gravação ()) e existe o risco de duas linhas será misturado.

$ perl -e 'print "a"x2000, "\n"' > foo
$ strace -e write bash -c 'read -r foo < foo; echo "$foo"' >/dev/null
write(1, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 1008) = 1008
write(1, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 993) = 993
+++ exited with 0 +++

Obviamente, o mesmo problema surge se houver várias chamadas para eco ou printf:

slowprint() {
  printf 'Start-%s ' "$1"
  sleep "$1"
  printf '%s-End\n' "$1"
}
export -f slowprint
seq 10 | xargs -n1 -I {} -P4 bash -c "slowprint {}"
# Compare to no parallelization
seq 10 | xargs -n1 -I {} bash -c "slowprint {}"
# Be sure to see the warnings in the next Pitfall!

As saídas dos trabalhos paralelos são misturadas, porque cada trabalho consiste em duas (ou mais) chamadas de gravação () separadas.

Se você precisar que as saídas não sejam misturadas, é recomendável usar uma ferramenta que garanta que a saída será serializada (como o GNU Parallel).

Ole Tange
fonte
Essa seção está errada. xargs echonão chama o eco bash embutido, mas o echoutilitário de $PATH. De qualquer forma, não posso reproduzir esse comportamento do bash echo com o bash 4.4. No Linux, as gravações em um canal (não / dev / null) maior que 4K não são garantidas como atômicas.
Stéphane Chazelas 23/07/19