Digamos que eu execute alguns processos:
#!/usr/bin/env bash
foo &
bar &
baz &
wait;
Eu corro o script acima assim:
foobarbaz | cat
até onde posso dizer, quando qualquer um dos processos grava em stdout/stderr, sua saída nunca é intercalada - cada linha de stdio parece ser atômica. Como isso funciona? Qual utilitário controla como cada linha é atômica?
Eles intercalam! Você apenas tentou rajadas de saída curtas, que permanecem sem divisão, mas na prática é difícil garantir que qualquer saída em particular permaneça sem divisão.
Buffer de saída
Depende de como os programas armazenam sua saída. 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 só produz os dados quando o buffer estiver cheio. 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 pipe usam o modo de buffer de linha ou usam o modo sem buffer e escrevem cada linha com uma única chamada para uma função de saída, e se as linhas são curtas o suficiente para escrever em um único bloco, entã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 onde eu intercalo a saída de dois programas. Eu usei 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 ao mesmo tempo, mas cada vez que emite uma saída, a saída é um número inteiro de linhas.echo bbbb; done | grep b
gravabbbb
para sempre no modo totalmente em buffer. 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, às vezes, interrompeu 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 maior de linhas interrompidas se as linhas fossem mais longas, pois a probabilidade de 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 alguns outros sistemas como o FreeBSD. 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 GNU grep.Vamos ver o trecho acima novamente, desta vez com buffer de linha em ambos os lados.
Portanto, desta vez o sim nunca interrompeu o grep, mas o grep às vezes interrompeu o sim. Voltarei ao porquê mais tarde.
Intercalação de tubos
Contanto que cada programa produza uma linha de cada vez e as linhas sejam curtas o suficiente, as linhas de saída serão perfeitamente separadas. Mas há um limite de quanto tempo as linhas podem ser para que isso funcione. O próprio pipe tem um buffer de transferência. Quando um programa é enviado para um pipe, os dados são copiados do programa gravador para o buffer de transferência do pipe e, posteriormente, do buffer de transferência do pipe 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 bufferful de cada vez. Se vários programas estiverem gravando no mesmo pipe e o primeiro programa que o kernel escolher quiser gravar mais de um bufferful, 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
quer escrever 2* P bytes ebar
quer escrever 3 bytes, então uma possível intercalação é P bytes defoo
, então 3 bytes debar
, e P bytes defoo
.Voltando ao exemplo yes+grep acima, no meu sistema,
yes aaaa
acontece de escrever quantas linhas cabem em um buffer de 8192 bytes de uma só vez. Como há 5 bytes para escrever (4 caracteres imprimíveis e a nova linha), isso significa que ele grava 8190 bytes todas as vezes. O tamanho do buffer de pipe é 4096 bytes. Portanto, é possível obter 4096 bytes de yes, depois alguma saída de grep e, em seguida, o restante da gravação de yes (8190 - 4096 = 4094 bytes). 4096 bytes deixa espaço para 819 linhas comaaaa
um único arquivoa
. Daí uma linha com este lonea
seguido por uma escrita de grep, dando uma linha comabbbb
.Se você quiser ver os detalhes do que está acontecendo,
getconf PIPE_BUF .
ele informará o tamanho do buffer do pipe em seu sistema e você poderá ver uma lista completa de chamadas de sistema feitas por cada programa comComo garantir um entrelaçamento de linha limpo
Se os comprimentos de linha forem menores que o tamanho do buffer de tubulação, o buffer de linha garante que não haverá nenhuma linha mista na saída.
Se os comprimentos de linha puderem ser maiores, não há como evitar a mistura arbitrária quando vários programas estão gravando no mesmo pipe. Para garantir a separação, você precisa fazer com que cada programa grave em um pipe diferente e usar um programa para combinar as linhas. Por exemplo , o GNU Parallel faz isso por padrão.
http://mywiki.wooledge.org/BashPitfalls#Non-atomic_writes_with_xargs_-P deu uma olhada nisso: