¿Por qué `echo | xargs > >(cat)` se cuelga en mi Mac?

¿Por qué `echo | xargs > >(cat)` se cuelga en mi Mac?

Esto es reproducible en zshy bash.

Confundiéndome aún más, echo | ( xargs; : ) > >(cat)no se cuelga. Esto también es reproducible en zshy bash.

Si uso GNU xargssegún lo previsto, brew install findutilsno se bloquea: echo | gxargs > >(cat).

De hecho, no he encontrado ningún otro programa además del de mi sistema xargsque se comporte de esta manera. Pensé que podría haber algo xargsrelacionado con los descriptores de archivos, así que intenté reemplazarlos xargscon bash -c 'kill -9 $$'muchas bash -c 'exec 0<&- 1<&-'otras tomas en la oscuridad.

También busqué ayuda en ##mac, #macosx, ##linuxy #bashen Freenode, pero nadie parecía saber lo que estaba pasando.También pregunté en Stack Overflowpero no fue suficiente programación.


> 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

Respuesta1

Pude encontrar el código fuente de mi sistema xargsejecutando strings $(which xargs)y buscando palabras clave interesantes. PROJECT:shell_cmds-207.40.1Me llamó la atención y pronto encontré el código fuente de una versión ligeramente anterior shell_cmds-203enSitio de código abierto de Apple.

Compilé la versión de xargsese paquete con gcc -g *.c, ejecuté echo | ./a.out > >(cat)y adjunté mi depurador lldbal a.outproceso. Descubrí que estaba atascado en una llamada waitpiddesde xargs.c:610(fuente). Extracto:

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

Como xargses un programa complicado, quería crear un programa C más pequeño que reprodujera el comportamiento. Aquí lo tienes:

// tiny.c
#include <sys/wait.h>

int main() {
    int status;
    waitpid(-1, &status, 0);
    return 0;
}

Compilando eso gcc tiny.c -o tinyy ejecutándolo, echo | ./tiny > >(cat)se colgó como xargs. De hecho, ahora podría simplificar aún más, ./tiny > >(cat)colgaría y ( ./tiny; : ) > >(cat)no colgaría.

Aparte: este pequeño programa se puede compilar en Linux y luego puedes reproducir este comportamiento en Linux fácilmente.

Pasar -1a waitpidhará que esperecualquier proceso hijo. Entonces eso plantea la pregunta:¿Por qué tinytiene un proceso hijo ./tiny > >(cat)pero no ( ./tiny; : ) > >(cat)?

No me he sumergido en bashel código fuente de , pero tengo una idea bastante fundamentada de lo que está pasando.

Primero analicemos el primer comando: ./tiny > >(cat). Primero bashcrea una canalización con nombre y luego fork()-exec()la catcrea como un proceso secundario. Luego configura la suya propia stdoutpara que sea la tubería con el mismo nombre. Finalmente bashtermina su vida llamando exec()a transformarse en tiny. Ahora tinytiene el mismo PID y el sistema operativo todavía considera que el catproceso es su hijo.

Es importante destacar que sucede lo mismo con ( ./tiny ) > >(cat)pero simplemente exec()está en bash (el paréntesis inicia un subshell) y luego en tiny. Un hecho clave parece ser que cuando bashse inicia con un solo comando para ejecutar, no lo hace, fork()-exec()sino que simplemente exec()se ejecuta de inmediato.

Ahora analicemos el segundo comando: ( ./tiny; : ) > >(cat). Al principio obtenemos lo mismo: fork()-exec()nacer cat. Luego bash exec()ingresa a una nueva bashinstancia. Luego ve que tiene dos comandos para ejecutar, por lo que fork()-exec()existe tinyy, debido a que se bifurcó, este nuevo tinyproceso no tiene catcomo hijo, por lo que no se bloquea. Luego bashse ejecuta :( :es un integrado especial, por lo que no hay ningún ejecutivo aquí, pero el uso de uno no integrado aún provocaría tinyque se bifurcara, por lo que aún no se colgaría).

información relacionada