- Pipelines que encadeiam várias comandos com uma saída que chega lentamente, como
tail -f /some/log/file | grep thing1 | grep thing2, podem parecer vazios não porque realmente travaram, mas porque um comando intermediário acumula a saída em um buffer grepe muitos programas verificam se stdout é um terminal comisatty; se for um terminal, usam buffering por linha, e se for um pipe ou arquivo, usam buffering em blocos de aproximadamente 8 KBtail,cateteesão exemplos que não fazem buffering de saída, mas opções para reduzir buffering variam por comando, comogrep --line-buffered,sed -u,tcpdump -l,jq -uetr -u- Se você interromper um pipeline com
Ctrl-C, a saída que estava no buffer de programas comotcpdumppode desaparecer; se encerrar comkill -TERM $PID, o buffer pode ser liberado e a saída pode aparecer - Soluções práticas incluem trocar por comandos que terminam rapidamente,
grep --line-buffered, um únicoawkou umgrepmais complexo,stdbufeunbuffer, mas é preciso verificar as condições de funcionamento e os efeitos colaterais de cada uma
Por que pipes parecem travar
- Quando linhas são adicionadas lentamente a um arquivo de log, o pipeline abaixo pode não mostrar saída mesmo que haja resultados correspondentes
tail -f /some/log/file | grep thing1 | grep thing2
- A causa não é o pipe em si, mas o
grep thing1intermediário, que não escreve os resultados imediatamente e os armazena em um buffer - Se um programa escrevesse imediatamente a cada vez, aumentaria o número de chamadas de sistema; por desempenho, ele junta uma certa quantidade de dados antes de escrever em um pipe ou arquivo
- Nesse exemplo, o
grep thing1pode esperar até acumular cerca de 8 KB de saída e, em um log lento, essa condição pode, na prática, nunca acontecer
A forma de saída muda entre terminal e pipe
tail -f file | grep thingfunciona bem, mas, ao adicionar um segundogrepdepois, a saída pode parecer travargrepe muitos programas verificam se stdout é um terminal usando a funçãoisatty- Se stdout for um terminal, usam buffering por linha e imprimem imediatamente linha a linha
- Se stdout for um pipe ou arquivo, usam buffering em blocos e só imprimem quando uma certa quantidade de dados é acumulada
- Portanto, quando
grepescreve diretamente no terminal, a linha aparece na hora; quando escreve em um pipe ligado ao próximo comando, ela pode não aparecer - O tamanho do buffer varia de programa para programa
- Em
grep, a libc cuida do buffering, e o tamanho da libc é definido pela variávelBUFSIZ - A definição na glibc fica em stdio.h
- Em
- Não usar um buffer de saída de 8 KB ao escrever em um terminal não é uma lei da física; um programa poderia implementar isso se quisesse, mas seria um comportamento muito estranho
Comportamentos de buffering variam por comando
- O que torna o buffering de saída complicado é que o usuário precisa lembrar quais comandos fazem buffering ao escrever em um pipe
- Exemplos de comandos que não fazem buffering de saída:
tailcattee
- Comandos comuns que fazem buffering de saída ao escrever em um pipe e formas de mitigação:
grep:--line-bufferedsed:-uawk: funçãofflush()tcpdump:-ljq:-utr:-ucut: não é possível desativar o buffering
- Para comandos como
sort, que só conseguem trabalhar depois de receber toda a entrada, a questão do buffering não é realmente importante - Houve uma tentativa de testar tanto o Mac OS quanto as versões GNU, mas, como há muitas variações, pode haver alguns erros
A saída padrão de linguagens de programação também faz buffering
- A saída do
printpadrão em algumas linguagens de programação também faz buffering ao escrever em um pipe - Formas de desativar por linguagem:
- C:
setvbuf - Python:
python -u,PYTHONUNBUFFERED=1,sys.stdout.reconfigure(line_buffering=False),print(x, flush=True) - Ruby:
STDOUT.sync = true - Perl:
$| = 1
- C:
- Esse comportamento padrão parece ser um design para tornar a função de saída padrão mais rápida em processamento em lote
- A existência de buffering pode variar conforme a forma de saída
- Em C++,
cout << "hello\n"faz buffering ao escrever em um pipe cout << "hello" << endllibera a saída
- Em C++,
Diferenças com Ctrl-C e redirecionamento para arquivo
- Se você conectar a saída de
tcpdumpagrepcomo abaixo e esquecer o-l, a saída pode ficar no buffersudo tcpdump -ni any port 53 | grep example.com
- Idealmente, ao pressionar
Ctrl-C, seria esperado quetcpdumpliberasse o buffer e quegreppesquisasse e mostrasse a saída que faltava - Na prática, quando os programas terminam, a saída que estava no buffer do
tcpdumpé perdida - Ao verificar com
strace,greprecebeSIGINTantes detcpdump, então, mesmo quetcpdumptente liberar o buffer,greppode já estar morto - Como contorno, se você encontrar o PID do
tcpdumpe executarkill -TERM $PID, otcpdumppode liberar o buffer e a saída pode aparecer - Redirecionamento para arquivo também faz buffering
sudo tcpdump -ni any port 53 > output.txt
- Porém, diferentemente do problema em que
Ctrl-Cdescarta completamente o conteúdo do buffer, no redirecionamento para arquivo, pela experiência, muitas vezes o conteúdo do buffer é escrito no arquivo antes de o programa terminar - Não está claro se esse comportamento pode ser sempre considerado confiável
Cinco formas de evitar buffering
-
Trocar por um programa que termina rapidamente
- É possível evitar a própria situação de escrever lentamente em um pipe e trocar por um comando que termina rápido
- Exemplo:
cat /some/log/file | grep thing1 | grep thing2 | tail
- Não é o mesmo comportamento do comando original com
tail -f, mas evita problemas complexos de buffering
-
Usar a opção de buffering por linha do
grepgreptem uma flag para evitar buffering- Exemplo:
tail -f /some/log/file | grep --line-buffered thing1 | grep thing2
-
Combinar com
awkou umgrepmais complexo- Situações com vários
greppodem ser convertidas para um únicoawktail -f /some/log/file | awk '/thing1/ && /thing2/'
- Ou podem ser escritas com um
grepusando uma expressão regular mais complexatail -f /some/log/file | grep -E 'thing1.*thing2'
- Como
awktambém faz buffering, para esse método funcionar oawkprecisa ser o último comando do pipeline
- Situações com vários
-
Usar
stdbufstdbufusaLD_PRELOADpara desativar o buffering da libc- Exemplo de desativação do buffering de saída:
tail -f /some/log/file | stdbuf -o0 grep thing1 | grep thing2
- Como outras soluções baseadas em
LD_PRELOAD, sua confiabilidade é limitada- Não funciona com binários estáticos
- Pode não funcionar se o programa não usa o buffering da libc
- Nem sempre funciona no Mac OS
- Há uma explicação relacionada em How stdbuf works, de Harry Marr
-
Usar
unbufferunbuffer programforça a saída do programa a parecer um TTY, fazendo com que ele use menos buffering e ative coisas como saída colorida, como em um TTY normal- Exemplo:
tail -f /some/log/file | unbuffer grep thing1 | grep thing2
- Ao contrário de
stdbuf, sempre funciona, mas pode ter efeitos colaterais indesejados- Por exemplo,
grep thing1pode inserir cores nos resultados correspondentes
- Por exemplo,
unbuffervem no pacoteexpect
Situações em que o problema aparece com mais frequência e a ideia de uma variável de ambiente
- Esse problema aparece principalmente em programas que enviam dados lentamente para um pipe
- Exemplos:
tcpdumptail -f- Monitoramento de logs no estilo
kubectl logs - Saída de cálculos lentos
- Seria bom ter uma variável de ambiente padrão para desativar buffering, como
PYTHONUNBUFFEREDem Python - A ideia veio de um post de blog de 2018 e de um post de acompanhamento, de Mark Dominus
- Um exemplo de nome possível seria
NO_BUFFER, parecido comNO_COLOR - O design é complicado
- O NETBSD tem variáveis de ambiente como
STDBUF,STDBUF1etc., que oferecem bastante controle - A maioria dos desenvolvedores talvez não queira implementar várias variáveis de ambiente para um caso de borda relativamente pequeno
- O NETBSD tem variáveis de ambiente como
- Também é uma curiosidade se existem programas que liberam automaticamente o buffer de saída em intervalos, como 1 segundo, mas nenhum vem à mente, e isso pode ter desvantagens
Escopo não abordado
- A diferença entre buffering por linha e saída totalmente sem buffer foi excluída
- A diferença entre buffering de stderr e buffering de stdout foi excluída
- Este conteúdo trata apenas de buffering que ocorre dentro dos programas
- O driver de TTY do sistema operacional às vezes também faz um pouco de buffering
- Outros motivos para liberar a saída além da situação de escrever em um pipe foram excluídos
1 comentários
Comentários do Hacker News
Uma abordagem com buffer quase sempre deveria ser do tipo “limiar ou timeout”: fazer flush ao atingir um limiar de bytes ou, se houver pelo menos 1 byte, depois de certo tempo
É uma abordagem comum em interfaces de hardware para resolver problemas semelhantes
Nesse caso, a biblioteca que faz buffering no espaço do usuário deve configurar um temporizador adequado quando coloca dados no buffer pela primeira vez. O valor do timeout pode ser recebido como argumento, definido em algo curto para uma pessoa, como 1–100 ms, tornado proporcional a
{largura de banda / limiar}, ou escolhido de modo que o overhead das chamadas de sistema não ultrapasse 0,1% do tempo totalEssa abordagem se aplica não só à escrita, mas também à leitura. Se forem feitas leituras em lote ou leituras combinadas, é necessário algo parecido, mas o canal de dados precisa ter uma forma eficiente de consultar ou receber notificações sobre “dados pendentes”, então isso depende mais do desenho do canal. Em hardware, abordagens como coalescência de interrupções são comuns
Erros de E/S poderiam ocorrer não só no momento da escrita, mas a qualquer momento, e várias chamadas de sistema poderiam ser interrompidas pelo temporizador, não apenas nos pontos em que o programa definiu seus próprios temporizadores ou onde um sinal chegou
Também pode haver confusão se a aplicação e a libc configurarem temporizadores ao mesmo tempo. As APIs modernas de temporizador do kernel parecem melhores do que eu lembrava, então talvez isso seja menos relevante, mas, se a aplicação bloquear sinais temporariamente em uma seção crítica, isso também afeta temporizadores de E/S
Por causa do momento e da forma de tratamento dos sinais, seria preciso ter ainda mais cuidado ao acessar estruturas de E/S
Com io_uring e temporizadores em espaço de usuário, isso escala muito melhor, mas, para dar suporte a muitas escritas pequenas e rápidas, ainda são necessários alguns truques. Por exemplo, acima de cerca de 1 milhão por segundo, o custo de gerenciar temporizadores começa a aparecer, e, para chegar a 100 milhões de escritas por segundo, foram necessárias técnicas bem peculiares
A causa do problema está na mistura entre algo que deveria ser interativo e um contrato que não pressupõe interação. Um exemplo é enviar a saída de acompanhamento do
tailpor um pipeNão acho que exista um problema real a resolver. Usando uma analogia de hardware, é como uma cisterna que acumula água da chuva e só a transfere quando está cheia. Não sei que exemplo você tem em mente, mas, até onde sei, flush baseado em tempo não é comum em hardware
A correção proposta torna o contrato muito mais complexo
O problema não está tanto na semântica em si, mas em não conhecer a semântica
Mesmo lidando com sistemas NIX há mais de 20 anos, eu sei que isso acontece, mas toda vez só me lembro depois de passar um bom tempo intrigado tentando entender por que a saída não aparece
Sobre a parte “os posts recentes estão ficando bem longos; será que alguém realmente quer ler um texto de 3000 palavras sobre buffering?”, pessoalmente, eu quero
Acho que às vezes textos prolixos são enchimento para otimização de busca
Também seria possível ter seções TLDR e NTLDR, isto é, “li apesar de ser longo”
Eu gostaria que todos os buffers fossem descarregados sempre que a CPU do sistema inteiro ficasse ociosa
Buffering, em geral, é uma técnica para economizar CPU. Se a CPU fosse infinita, todos os buffers teriam 1 byte. Buffers reúnem dados para processá-los em lote por eficiência
Mas, quando a CPU fica ociosa, não deveria sobrar “trabalho para depois”. Assim que o escalonador do kernel ficasse ocioso, ele deveria enviar a todos os processos um sinal para fazer flush dos buffers
Seria como fazer todo esse trabalho e depois ainda obrigar chamadas de sistema para fazer flush dos buffers. Talvez dê para adicionar um mecanismo que permita ao kernel conhecer buffers em espaço de usuário e buscá-los diretamente quando estiver ocioso
Fico pensando se isso é, em certa medida, algo como io_uring https://man7.org/linux/man-pages/man3/io_uring_register_buff...
Este texto confunde duas coisas diferentes: sem buffer e buffering por linha
Sem buffer piora o desempenho desnecessariamente e pode gerar saída incorreta quando várias fontes escrevem no mesmo pipe. Linhas suficientemente longas vão se misturar de qualquer forma, mas, na prática, a maioria das linhas de saída tem menos de 4096 bytes, mesmo incluindo caracteres de formatação/controle e caracteres de planos suplementares
Buffering por linha é o padrão em terminais e, em pipes, normalmente também é o comportamento desejado. Basta executar cada comando sob
stdbuf -oL -eL. Os raros programas que desejam atualizar dentro da própria linha já precisam fazer flush manual, então também se comportam corretamente aquiO que o
stdbufrealmente faz pode ser visto assim:env -i \command -v stdbuf` -oL -eL `command -v env``Já escrevi sobre esse problema antes: https://world-playground-deceit.net/blog/2024/09/bourne_shel...
Sobre comandos que não fazem buffering, isso depende da implementação; ou, no caso do
cat, talvez esteja errado. Veja https://pubs.opengroup.org/onlinepubs/9799919799/utilities/c... e-u. É uma grande dor o POSIX não ter incluído uma forma oficial de controlar isso.Algo que não foi mencionado é o buffering de entrada, que produz resultados estranhos como este:
$ seq 5 | { v1=$(head -1); v2=$(head -1); printf '%s=%s\n' v1 "$v1" v2 "$v2"; }v1=1v2=Nesse caso, a solução é usar
stdbuf -i0 head -1.socketpairpossa impor esse tipo de restrição ao processo que está escrevendo. Exceto usando hacks pesados comoptrace().Talvez seja possível ajustar o tamanho do buffer do pipe, mas não conheço nenhuma convenção segundo a qual a E/S padrão do C tenha que obedecer a isso.
De todo modo, neste caso o
stdbufnão parece ajudar:$ ./a | stdbuf -i0 -- cat#include#includeint main(void) {for (;;) {printf("n");usleep(100000);}}Há bons motivos para existirem buffers. Imprimir saída na tela é relativamente muito lento em comparação com escrever em um buffer.
Emitir caracteres um por um é extremamente ineficiente.
É um problema antigo e aparece com frequência ao lidar com UART. Há várias soluções possíveis: uma abordagem baseada em linhas, que marca o fim da saída com um caractere especial como nova linha; uma baseada em tamanho, que espera até atingir um comprimento como 8 KB; e uma baseada em tempo, que emite a saída a cada X milissegundos.
Cada abordagem tem prós e contras, e o que é ideal depende da aplicação. Acho incorreta a parte do texto que diz que certos programas não usam buffering. Esses programas simplesmente não usam uma abordagem obviamente baseada em tamanho.
A abordagem baseada em linhas é assim, mas exige um acordo sobre qual caractere usar. Normalmente é a nova linha.
/dev/nullpor si só já pode prejudicar bastante o desempenho.Uso Unix há mais de 35 anos, mas nunca entendi completamente como isso funciona.
Foi bom ver uma explicação geral do comportamento de buffering em vários sistemas e componentes; com certeza aprendi algo.
Sobre a parte “ao pressionar Ctrl-C em um pipe, o conteúdo do buffer desaparece”, acho que a maioria dos programas faria flush do buffer em SIGINT.
Só que, para isso funcionar assim no shell, seria preciso entregar o SIGINT apenas ao primeiro programa do pipeline, e provavelmente não é isso que acontece na prática.
siginte os demais recebemsigpipe.