Isso é reproduzível em zsh
e bash
.
Me confundindo ainda mais, echo | ( xargs; : ) > >(cat)
não trava. Isso também é reproduzível em zsh
e bash
.
Se eu usar o GNU xargs
conforme fornecido, brew install findutils
ele não trava: echo | gxargs > >(cat)
.
Na verdade, não encontrei nenhum outro programa além do meu sistema xargs
que se comporte dessa maneira. Achei que poderia haver algo xargs
acontecendo com os descritores de arquivo, então tentei substituí-los xargs
por bash -c 'kill -9 $$'
ou bash -c 'exec 0<&- 1<&-'
por muitas outras fotos no escuro.
Também procurei ajuda no ##mac
, #macosx
, ##linux
e #bash
no Freenode, mas ninguém parecia saber o que estava acontecendo.Também perguntei no Stack Overflowmas não era programação suficiente.
> sw_vers | head -n 2
ProductName: Mac OS X
ProductVersion: 10.15.2
> zsh --version
zsh 5.7.1 (x86_64-apple-darwin19.0)
> bash --version | head -n 1
GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin19)
> strings $(which xargs) | grep 'xargs.c'
$FreeBSD: src/usr.bin/xargs/xargs.c,v 1.57 2005/02/27 02:01:31 gad Exp $
> gxargs --version | head -n 1
xargs (GNU findutils) 4.7.0
Responder1
Consegui encontrar o código-fonte do meu sistema xargs
executando strings $(which xargs)
e procurando palavras-chave interessantes. PROJECT:shell_cmds-207.40.1
chamou minha atenção e logo encontrei o código-fonte de uma versão um pouco mais antiga shell_cmds-203
emSite de código aberto da Apple.
Compilei a versão xargs
desse pacote com gcc -g *.c
, executei echo | ./a.out > >(cat)
e anexei meu depurador lldb
ao a.out
processo. Descobri que ele estava preso em uma chamada waitpid
de xargs.c:610
(fonte). Excerto:
while ((pid = waitpid(-1, &status, !waitall && curprocs < maxprocs ?
WNOHANG : 0)) > 0) {
Por xargs
ser um programa complicado, eu queria fazer um programa C menor que reproduzisse o comportamento. Aqui está:
// tiny.c
#include <sys/wait.h>
int main() {
int status;
waitpid(-1, &status, 0);
return 0;
}
Compilar isso gcc tiny.c -o tiny
e executar travado echo | ./tiny > >(cat)
exatamente como xargs
. Na verdade, agora eu poderia simplificar ainda mais, ./tiny > >(cat)
travaria, enquanto ( ./tiny; : ) > >(cat)
não travaria.
Além: este pequeno programa pode ser compilado no Linux e então você pode reproduzir esse comportamento no Linux facilmente.
Passar -1
para waitpid
fará com que ele esperequalquer processo filho. Então isso levanta a questão:por que tiny
tem um processo filho, ./tiny > >(cat)
mas não ( ./tiny; : ) > >(cat)
?
Não mergulhei no bash
código-fonte de , mas tenho um palpite bastante bem fundamentado sobre o que está acontecendo.
Primeiro vamos dissecar o primeiro comando: ./tiny > >(cat)
. Primeiro bash
cria um pipe nomeado e depois fork()-exec()
entra cat
na criação como um processo filho. Em seguida, ele define o seu próprio stdout
canal com o mesmo nome. Finalmente bash
termina sua vida chamando exec()
a transformar-se tiny
. Agora tiny
tem o mesmo PID e o SO ainda considera o cat
processo como seu filho.
É importante ressaltar que a mesma coisa acontece com ( ./tiny ) > >(cat)
mas é apenas exec()
no bash (parênteses inicia um subshell) e depois no tiny
. Um fato importante parece ser que quando bash
é iniciado com apenas um comando para executar, ele não é fork()-exec()
executado exec()
imediatamente.
Agora vamos dissecar o segundo comando: ( ./tiny; : ) > >(cat)
. Obtemos a mesma coisa no início: fork()-exec()
passar cat
a existir. Em seguida, bash
exec()
entramos em uma nova bash
instância. Então ele vê que tem dois comandos para executar, então ele fork()-exec()
existe tiny
e, como foi bifurcado, esse novo tiny
processo não tem cat
como filho, então não trava. Em seguida, bash
executa :
( :
é um built-in especial, então não há exec aqui, mas usar um não-built-in ainda causaria tiny
um fork, então ainda não haveria nenhum travamento).