Por que `eco | xargs > >(cat)` trava no meu Mac?

Por que `eco | xargs > >(cat)` trava no meu Mac?

Isso é reproduzível em zshe bash.

Me confundindo ainda mais, echo | ( xargs; : ) > >(cat)não trava. Isso também é reproduzível em zshe bash.

Se eu usar o GNU xargsconforme fornecido, brew install findutilsele não trava: echo | gxargs > >(cat).

Na verdade, não encontrei nenhum outro programa além do meu sistema xargsque se comporte dessa maneira. Achei que poderia haver algo xargsacontecendo com os descritores de arquivo, então tentei substituí-los xargspor bash -c 'kill -9 $$'ou bash -c 'exec 0<&- 1<&-'por muitas outras fotos no escuro.

Também procurei ajuda no ##mac, #macosx, ##linuxe #bashno 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 xargsexecutando strings $(which xargs)e procurando palavras-chave interessantes. PROJECT:shell_cmds-207.40.1chamou minha atenção e logo encontrei o código-fonte de uma versão um pouco mais antiga shell_cmds-203emSite de código aberto da Apple.

Compilei a versão xargsdesse pacote com gcc -g *.c, executei echo | ./a.out > >(cat)e anexei meu depurador lldbao a.outprocesso. Descobri que ele estava preso em uma chamada waitpidde xargs.c:610(fonte). Excerto:

while ((pid = waitpid(-1, &status, !waitall && curprocs < maxprocs ?
        WNOHANG : 0)) > 0) {

Por xargsser 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 tinye 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 -1para waitpidfará com que ele esperequalquer processo filho. Então isso levanta a questão:por que tinytem um processo filho, ./tiny > >(cat)mas não ( ./tiny; : ) > >(cat)?

Não mergulhei no bashcódigo-fonte de , mas tenho um palpite bastante bem fundamentado sobre o que está acontecendo.

Primeiro vamos dissecar o primeiro comando: ./tiny > >(cat). Primeiro bashcria um pipe nomeado e depois fork()-exec()entra catna criação como um processo filho. Em seguida, ele define o seu próprio stdoutcanal com o mesmo nome. Finalmente bashtermina sua vida chamando exec()a transformar-se tiny. Agora tinytem o mesmo PID e o SO ainda considera o catprocesso 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 cata existir. Em seguida, bash exec()entramos em uma nova bashinstância. Então ele vê que tem dois comandos para executar, então ele fork()-exec()existe tinye, como foi bifurcado, esse novo tinyprocesso não tem catcomo filho, então não trava. Em seguida, bashexecuta :( :é um built-in especial, então não há exec aqui, mas usar um não-built-in ainda causaria tinyum fork, então ainda não haveria nenhum travamento).

informação relacionada