Por que o uso de `yes` em pipelines do bash * não * causa loops infinitos?

16

De acordo com a documentação, o bash aguarda até que todos os comandos em um pipeline concluam a execução antes de continuar

O shell aguarda a conclusão de todos os comandos no pipeline antes de retornar um valor.

Então, por que o comando yes | truetermina imediatamente? O yesloop não deve para sempre e faz com que o pipeline nunca retorne?


E uma subquestão: de acordo com a especificação POSIX , os pipelines do shell podem optar por retornar após o término do último comando ou aguardar até que todos os comandos sejam concluídos. As conchas comuns têm um comportamento diferente nesse sentido? Existe alguma concha onde yes | truese repita para sempre?

hugomg
fonte
yes | tee >(true) >/dev/nullfará o que você espera, btw, como teecontinua até que todos os escritores estejam mortos, portanto, truesair não irá atrapalhá-lo completamente.
Charles Duffy
11
trueé basicamente um {return 0;}programa, então eu não esperaria que fosse executado por muito tempo, muito menos para sempre.
Dmitry Grigoryev

Respostas:

33

Quando truesai, o lado de leitura do pipe é fechado, mas yescontinua tentando gravar no lado de gravação. Essa condição é chamada de "tubo quebrado" e faz com que o kernel envie um SIGPIPEsinal para yes. Como yesnão faz nada de especial nesse sinal, ele será morto. Se ignorasse o sinal, sua writechamada falharia com o código de erro EPIPE. Os programas que fazem isso precisam estar preparados para perceber EPIPEe parar de escrever, ou entrarão em um loop infinito.

Se você fizer strace yes | true1, poderá ver o kernel se preparando para as duas possibilidades:

write(1, "y\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\n"..., 4096) = -1 EPIPE (Broken pipe)
--- SIGPIPE {si_signo=SIGPIPE, si_code=SI_USER, si_pid=17556, si_uid=1000} ---
+++ killed by SIGPIPE +++

straceestá assistindo a eventos por meio da API do depurador, que primeiro fala sobre a chamada do sistema retornando com um erro e depois sobre o sinal. Da yesperspectiva de, porém, o sinal acontece primeiro. (Tecnicamente, o sinal é entregue depois que o kernel retorna o controle ao espaço do usuário, mas antes que mais instruções da máquina sejam executadas, para que a writefunção "wrapper" na biblioteca C não tenha a chance de definir errnoe retornar ao aplicativo.)


1 Infelizmente, straceé específico do Linux. A maioria dos Unixes modernos possui algum comando que faz algo semelhante, mas geralmente tem um nome diferente, provavelmente não decodifica os argumentos do syscall tão completamente e, às vezes, funciona apenas como root.

Casey
fonte
3
@ hugomg nesse caso, o tubo é totalmente irrelevante.
Muru
3
@ hugomg porque nada yesestá conectado ao pipe.
Muru
4
É verdade, isso é uma demonstração do comportamento documentado "aguarde até que todos os comandos terminem antes de encerrar o pipeline". Apenas impede a yesobtenção do SIGPIPE, pois o FD para o qual está gravando não está conectado a um pipe.
Tom Hunt
2
@ hugomg, está repetindo para sempre da mesma maneira que yes >/dev/nullestá repetindo para sempre. Ele não demonstra nada sobre pipelines que também não são verdadeiros para comandos simples (como o comportamento de espera pela terminação que Tom ressalta também se aplica a comandos simples).
Charles Duffy
2
@ zwol: Acho que estamos usando nossos termos com significados ligeiramente diferentes aqui, ou pensando sobre coisas de perspectivas ligeiramente diferentes ... mas em ambos os casos write()(a função libc) não retorna (transfere o controle para o PC que está seguindo) até depois o manipulador de sinais foi executado, mas, como o manipulador de sinais termina o programa, o controle nunca é transferido e, portanto, write()nunca retorna. Sim, isso é implementado no kernel com o xxx_write()retorno de algumas funções -EPIPE, mas estamos depurando um programa de espaço do usuário e não estamos interessados ​​nisso.
Dietrich Epp
5

Existem conchas onde sim | verdadeiro vai fazer um loop para sempre?

Improvável, já que o yescomando está usando o pipe e falhará quando o pipe estiver quebrado. sleeppor outro lado, não usa o cachimbo, então:

sleep 100000000 | true

será executado por 100000000 segundos, pelo menos.

muru
fonte
2
Tenha cuidado com todas as conchas modernas que não se bifurcam para o último comando interno (mais à direita) em um tubo e onde ele trueestá. Isso se aplica a versões recentes do Bourne Shell, ksh93, zsh. Se você pressionar ^Zquando um comando estiver em execução, isso suspenderá o sono e o shell nunca poderá se recuperar sem ajuda externa.
schily
3
O zsh 4.3.4 (i386-pc-solaris2.11) aqui, parece que isso foi modificado recentemente. Idéia interessante, vou precisar verificar se posso implementar uma correção semelhante para o Bourne Shell. Ainda há uma dúvida de como ele funciona e qual grupo de processos tty é usado como no Bourne Shell, o fato de que o comando mais à direita é um built-in é descoberto depois que o grupo de processos para suspensão já foi configurado para sempre.
schily
2
@CharlesDuffy, pelo que entendi, schily mantém uma versão do sh, para a qual ele suporta as melhorias dos shells modernos. Ele postou sobre isso aqui, em algum lugar.
Muru
3
O Bourne Shell nos arquivos da herança foi mantido até ~ 2007, mas nunca foi totalmente portátil, pois ainda contém chamadas para sbrk(). A versão portátil e mantida é nas ferramentas Schily agrupar e @Charles Duffy já descobriu um local para obter informações ;-)
Schily
2
@muru Muitos dos recursos que eu backportei para o Bourne Shell são do meu bsh(Berthold Shell do VBERTOS, uma versão aprimorada de memória virtual do UNOS - o primeiro clone do UNIX). O Bsh obteve muitos recursos do csh em 1984 e 1985, mas o mecanismo de alias do UNOS já era superior ao do csh em 1980. Outros novos recursos do Bourne Shell são do POSIX, para permitir que ele se aproxime da conformidade com o POSIX.
schily