Desativar o buffer no tubo

395

Eu tenho um script que chama dois comandos:

long_running_command | print_progress

As long_running_commandimpressões de um progresso, mas eu estou infeliz com ele. Estou usando print_progresspara torná-lo mais agradável (ou seja, imprimo o progresso em uma única linha).

O problema: conectar um pipe ao stdout também ativa um buffer de 4K, para o bom programa de impressão não recebe nada ... nada ... nada ... muito ... :)

Como posso desativar o buffer 4K do long_running_command(não, não tenho a fonte)?

Aaron Digulla
fonte
1
Portanto, quando você executa o comando long_running_com sem canalizar, pode ver as atualizações de progresso corretamente, mas quando o canal é armazenado em buffer?
1
Sim, é exatamente isso que acontece.
Aaron Digulla 16/06/09
20
A incapacidade de uma maneira simples de controlar o buffer é um problema há décadas. Por exemplo, consulte: marc.info/?l=glibc-bug&m=98313957306297&w=4, que basicamente diz "Não posso ser irritado fazendo isso e aqui está uma armadilha para justificar minha posição"
1
Na verdade, não é o canal que causa um atraso enquanto aguarda dados suficientes. Os tubos têm uma capacidade, mas assim que houver dados gravados no tubo, ele estará imediatamente pronto para leitura na outra extremidade.
Sam Watkins

Respostas:

254

Você pode usar o unbuffercomando (que faz parte do expectpacote), por exemplo

unbuffer long_running_command | print_progress

unbufferconecta-se long_running_commandatravés de um pseudoterminal (pty), que faz com que o sistema o trate como um processo interativo, portanto, não usa o buffer de 4 kiB no pipeline, que é a causa provável do atraso.

Para pipelines mais longos, pode ser necessário descomprimir cada comando (exceto o final), por exemplo

unbuffer x | unbuffer -p y | z
Stephen Kitt
fonte
3
De fato, o uso de um pty para conectar-se a processos interativos é verdadeiro em relação às expectativas em geral.
15
Ao canalizar chamadas ao buffer, você deve usar o argumento -p para que o buffer leia do stdin.
26
Nota: Nos sistemas debian, isso é chamado expect_unbuffere está no expect-devpacote, não no expectpacote
bdonlan
4
@dondonlan: Pelo menos no Ubuntu (baseado em debian), expect-devfornece ambos unbuffere expect_unbuffer(o primeiro é um link simbólico para o último). Os links estão disponíveis desde expect 5.44.1.14-1(2009).
jfs
1
Nota: Nos sistemas Ubuntu 14.04.x, também está no pacote expect-dev.
Alexandre Mazel
462

Outra maneira de esfolar esse gato é usar o stdbufprograma, que faz parte do GNU Coreutils (o FreeBSD também possui um).

stdbuf -i0 -o0 -e0 command

Isso desativa o buffer completamente para entrada, saída e erro. Para algumas aplicações, o buffer de linha pode ser mais adequado por razões de desempenho:

stdbuf -oL -eL command

Observe que ele funciona apenas para stdiobuffer ( printf(), fputs()...) para aplicativos vinculados dinamicamente e somente se esse aplicativo não ajustar o buffer de seus fluxos padrão sozinho, embora isso deva cobrir a maioria dos aplicativos.

a3nm
fonte
6
"unbuffer" precisa ser instalado no Ubuntu, que está dentro do pacote: espera-dev que é 2MB ...
Lepe
2
Isso funciona muito bem na instalação raspbian padrão no log sem buffer. Encontrei sudo stdbuff … commandobras, embora stdbuff … sudo commandnão.
Natevw
20
@qdii stdbufnão funciona teeporque teesubstitui os padrões definidos por stdbuf. Veja a página de manual de stdbuf.
ceving 30/06/14
5
@lepe Estranhamente, o unbuffer tem dependências no x11 e no tcl / tk, o que significa que ele realmente precisa de> 80 MB se você estiver instalando em um servidor sem eles.
precisa saber é o seguinte
10
O @qdii stdbufusa o LD_PRELOADmecanismo para inserir sua própria biblioteca carregada dinamicamente libstdbuf.so. Isso significa que ele não funcionará com esses tipos de executáveis: com recursos de setuid ou de arquivo definidos, vinculados estaticamente, sem usar a libc padrão. Nesses casos, é melhor usar as soluções com unbuffer/ script/ socat. Veja também stdbuf com setuid / capacidades .
Pabouk
75

Outra maneira de ativar o modo de saída com buffer de linha long_running_commandé usar o scriptcomando que executa o seu long_running_commandem um pseudo terminal (pty).

script -q /dev/null long_running_command | print_progress      # FreeBSD, Mac OS X
script -c "long_running_command" /dev/null | print_progress    # Linux
Chade
fonte
15
Um bom truque, uma vez que scripté um comando tão antigo, deve estar disponível em todas as plataformas do tipo Unix.
Aaron Digulla
5
você também precisa -qno Linux:script -q -c 'long_running_command' /dev/null | print_progress
jfs
1
Parece que o script lê stdin, o que impossibilita a execução long_running_commandem segundo plano, pelo menos quando iniciado no terminal interativo. Para contornar o problema, consegui redirecionar o stdin de /dev/null, pois o meu long_running_commandnão usa stdin.
haridsv
1
Até funciona no Android.
Not2qubit
3
Uma desvantagem significativa: ctrl-z não funciona mais (ou seja, não posso suspender o script). Isso pode ser corrigido por, por exemplo: echo | script sudo -c / usr / local / bin / ec2-snapshot-all / dev / null | ts, se você não se importa de não poder interagir com o programa.
rlpowell
66

Para grep, sede awkvocê pode forçar a saída a ser buffer de linha. Você pode usar:

grep --line-buffered

Força a saída a ser buffer de linha. Por padrão, a saída é buffer de linha quando a saída padrão é um terminal e o buffer do bloco é contrário.

sed -u

Tornar a linha de saída em buffer.

Consulte esta página para obter mais informações: http://www.perkin.org.uk/posts/how-to-fix-stdio-buffering.html

yaneku
fonte
51

Se houver um problema com a libc modificando seu buffer / liberação quando a saída não for para um terminal, tente socat . Você pode criar um fluxo bidirecional entre quase qualquer tipo de mecanismo de E / S. Um deles é um programa bifurcado falando com uma pseudo-tty.

 socat EXEC:long_running_command,pty,ctty STDIO 

O que faz é

  • crie um pseudo-tty
  • fork long_running_command com o lado escravo do pty como stdin / stdout
  • estabelecer um fluxo bidirecional entre o lado mestre do pty e o segundo endereço (aqui é STDIO)

Se isso lhe der a mesma saída que long_running_command, então você pode continuar com um tubo.

Edit: Uau Não vi a resposta do buffer! Bem, o socat é uma ótima ferramenta de qualquer maneira, então eu posso deixar essa resposta

shodanex
fonte
1
... e eu não sabia sobre socat - parece meio netcat, talvez mais ainda. ;) Obrigado e +1.
3
Eu usaria socat -u exec:long_running_command,pty,end-close -aqui
Stéphane Chazelas
20

Você pode usar

long_running_command 1>&2 |& print_progress

O problema é que a libc faz buffer de linha quando o stdout é exibido na tela e buffer total quando o stdout é exibido em um arquivo. Mas sem buffer para stderr.

Eu não acho que seja o problema com o buffer de pipe, é tudo sobre a política de buffer da libc.

Wang HongQin
fonte
Você está certo; minha pergunta ainda é: como posso influenciar a política de buffer da libc sem recompilar?
Aaron Digulla
@ StéphaneChazelas fd1 será redirecionado para stderr
Wang HongQin
@ StéphaneChazelas eu não entendo o seu argumento. plz fazer um teste, ele funciona
Wang HongQin
3
OK, o que está acontecendo é que, com os dois zsh(de onde |&vem adaptado do csh) e bash, quando você faz cmd1 >&2 |& cmd2, os fd 1 e 2 são conectados ao stdout externo. Por isso, ele evita o armazenamento em buffer quando esse stdout externo é um terminal, mas apenas porque a saída não passa pelo tubo ( print_progressnão imprime nada). Portanto, é o mesmo que long_running_command & print_progress(exceto que print_progress stdin é um canal que não possui gravador). Você pode verificar com em ls -l /proc/self/fd >&2 |& catcomparação com ls -l /proc/self/fd |& cat.
Stéphane Chazelas
3
Isso porque |&é uma abreviação 2>&1 |, literalmente. Assim cmd1 |& cmd2é cmd1 1>&2 2>&1 | cmd2. Portanto, os fd 1 e 2 acabam conectados ao stderr original e nada fica gravado no canal. ( s/outer stdout/outer stderr/gno meu comentário anterior).
Stéphane Chazelas
11

Costumava ser o caso, e provavelmente ainda é o caso, de que quando a saída padrão é gravada em um terminal, a linha é armazenada em buffer por padrão - quando uma nova linha é gravada, a linha é gravada no terminal. Quando a saída padrão é enviada para um tubo, ela é totalmente armazenada em buffer - portanto, os dados são enviados apenas para o próximo processo no pipeline quando o buffer de E / S padrão é preenchido.

Essa é a fonte do problema. Não tenho certeza se há muito que você pode fazer para corrigi-lo sem modificar a gravação do programa no canal. Você pode usar a setvbuf()função com o _IOLBFsinalizador para colocar incondicionalmente stdoutno modo de buffer de linha. Mas não vejo uma maneira fácil de aplicar isso em um programa. Ou o programa pode fazer fflush()nos pontos apropriados (após cada linha de saída), mas o mesmo comentário se aplica.

Suponho que, se você substituísse o pipe por um pseudo-terminal, a biblioteca de E / S padrão pensaria que a saída era um terminal (porque é um tipo de terminal) e alinharia o buffer automaticamente. Essa é uma maneira complexa de lidar com as coisas, no entanto.

Jonathan Leffler
fonte
7

Sei que essa é uma pergunta antiga e já tinha muitas respostas, mas se você deseja evitar o problema do buffer, tente algo como:

stdbuf -oL tail -f /var/log/messages | tee -a /home/your_user_here/logs.txt

Isso produzirá os logs em tempo real e também os salvará no logs.txtarquivo, e o buffer não afetará mais o tail -fcomando.

Marin Nedea
fonte
4
Isto parece com a segunda resposta: - /
Aaron Digulla
2
O stdbuf está incluído no gnu coreutils (verifiquei na versão mais recente 8.25). verificou que isso funciona em um linux incorporado.
Zhaorufei
Da documentação de stdbuf, NOTE: If COMMAND adjusts the buffering of its standard streams ('tee' does for example) then that will override corresponding changes by 'stdbuf'.
shrewmouse 17/09
6

Não acho que o problema esteja com o cano. Parece que seu processo de execução demorada não está liberando seu próprio buffer com frequência suficiente. Alterar o tamanho do buffer do canal seria um truque para contorná-lo, mas não acho possível sem reconstruir o kernel - algo que você não gostaria de fazer como um hack, pois provavelmente a aversley afeta muitos outros processos.


fonte
18
A causa principal é que a libc muda para o buffer 4k se o stdout não for um tty.
Aaron Digulla 16/06/09
5
Isso é muito interessante ! porque o pipe não causa nenhum buffer. Eles fornecem buffer, mas se você lê em um canal, obtém todos os dados disponíveis, não precisa esperar por um buffer no canal. Portanto, o culpado seria o armazenamento temporário no aplicativo.
3

De acordo com este post aqui , você pode tentar reduzir o ulimit do pipe para um único bloco de 512 bytes. Certamente não desativará o buffer, mas bem, 512 bytes é muito menor que 4K: 3

RAKK
fonte
3

Da mesma forma que a resposta de Chad , você pode escrever um pequeno script como este:

# save as ~/bin/scriptee, or so
script -q /dev/null sh -c 'exec cat > /dev/null'

Em seguida, use este scripteecomando como um substituto para tee.

my-long-running-command | scriptee

Infelizmente, não consigo obter uma versão como essa para funcionar perfeitamente no Linux, então parece limitada aos unixes no estilo BSD.

No Linux, isso está próximo, mas você não recebe seu prompt de volta quando ele termina (até você pressionar enter, etc) ...

script -q -c 'cat > /proc/self/fd/1' /dev/null
jwd
fonte
Por que isso funciona? O "script" desativa o buffer?
Aaron Digulla 06/12/19
@ Aaron Digulla: scriptemula um terminal, então sim, acredito que ele desativa o buffer. Ele também faz eco de cada caractere enviado a ele - e é por isso que ele caté enviado /dev/nullno exemplo. No que diz respeito ao programa em execução script, ele está conversando com uma sessão interativa. Acredito que seja semelhante a expectesse respeito, mas scriptprovavelmente faz parte do seu sistema básico.
jwd
O motivo pelo qual uso teeé enviar uma cópia do fluxo para um arquivo. Para onde o arquivo é especificado scriptee?
Bruno Bronosky 31/07
@BrunoBronosky: Você está certo, é um nome ruim para este programa. Não está realmente fazendo uma operação de "tee". É apenas desativar o buffer da saída, de acordo com a pergunta original. Talvez devesse ser chamado "scriptcat" (embora não esteja fazendo concatenação também ...). Independentemente, você pode substituir o catcomando tee myfile.txte obter o efeito desejado.
jwd 31/07
2

Encontrei esta solução inteligente: (echo -e "cmd 1\ncmd 2" && cat) | ./shell_executable

Isso faz o truque. catlerá a entrada adicional (até EOF) e a passará para o canal depois de echocolocar seus argumentos no fluxo de entrada de shell_executable.

jaggedsoft
fonte
2
Na verdade, catnão vê a saída do echo; basta executar dois comandos em um subshell e a saída de ambos é enviada para o canal. O segundo comando no subshell ('cat') lê do stdin pai / externo, é por isso que funciona.
Aaron Digulla
0

De acordo com isso, o tamanho do buffer do pipe parece estar definido no kernel e exigiria que você recompile seu kernel para alterar.


fonte
7
Eu acredito que é um buffer diferente.
Samuel Edwin Ward