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?
Respostas:
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:
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 aaaa
escreveaaaa
para sempre no que é essencialmente equivalente ao modo de buffer de linha. Nayes
verdade, 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 b
gravabbbb
para 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.
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:
stdbuf -o0
encontrado no GNU coreutils e em alguns outros sistemas como o FreeBSD. Como alternativa, você pode alternar para o buffer de linha comstdbuf -oL
.unbuffer
. Alguns programas podem se comportar de maneira diferente de outras maneiras, por exemplo,grep
usa cores por padrão se sua saída for um terminal.--line-buffered
para o GNU grep.Vamos ver o trecho acima novamente, desta vez com buffer de linha nos dois lados.
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,
foo
deseja escrever 2 * P bytes ebar
deseja escrever 3 bytes, uma intercalação possível é P bytes defoo
, depois 3 bytes debar
e P bytes defoo
.Voltando ao exemplo yes + grep acima, no meu sistema,
yes aaaa
acontece 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 comaaaa
e um solitárioa
. Daí uma linha com este solitário,a
seguida por uma gravação de grep, fornecendo uma linha comabbbb
.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 comComo 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.
fonte
cat
atomicamente, 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?awk
produzi 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 comfind -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.http://mywiki.wooledge.org/BashPitfalls#Non-atomic_writes_with_xargs_-P analisou isso:
fonte
xargs echo
não chama o eco bash embutido, mas oecho
utilitá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.