Почему `echo | xargs > >(cat)` зависает на моем Mac?

Почему `echo | xargs > >(cat)` зависает на моем Mac?

Это воспроизводится в zshи bash.

Еще больше меня сбивает с толку, echo | ( xargs; : ) > >(cat)не зависает. Это также воспроизводится в zshи bash.

Если я использую GNU, xargsкак указано в brew install findutilsруководстве, то зависаний не происходит: echo | gxargs > >(cat).

Действительно, я не нашел ни одной другой программы, кроме моей системы, xargsкоторая ведет себя таким образом. Я подумал, что, возможно, что-то xargsпроисходит с дескрипторами файлов, поэтому я попробовал заменить xargsна bash -c 'kill -9 $$'или bash -c 'exec 0<&- 1<&-'или много других снимков в темноте.

Я также искал помощи на ##mac, #macosx, ##linux, и #bashна Freenode, но никто там, похоже, не понимал, что происходит.Я также спросил на Stack Overflowно это было не совсем программирование.


> 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

решение1

Я смог найти исходный код для моей системы, xargsзапустив strings $(which xargs)и поискав интересные ключевые слова. PROJECT:shell_cmds-207.40.1застряли у меня в памяти, и вскоре я нашел исходный код для немного более старой версии shell_cmds-203наСайт Apple с открытым исходным кодом.

Я скомпилировал версию xargsв этом пакете с помощью gcc -g *.c, запустил echo | ./a.out > >(cat)и присоединил свой отладчик lldbк a.outпроцессу. Я обнаружил, что он застрял в вызове waitpidfrom xargs.c:610(источник). Отрывок:

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

Поскольку xargsэто сложная программа, я хотел сделать меньшую программу на C, которая воспроизводила бы поведение. Вот она:

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

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

Компиляция этого с gcc tiny.c -o tinyи запуск echo | ./tiny > >(cat)зависал так же, как xargs. Действительно, теперь я мог бы упростить еще больше, ./tiny > >(cat)зависал, в то время как ( ./tiny; : ) > >(cat)не зависал.

Примечание: эту маленькую программу можно скомпилировать в Linux, а затем вы сможете легко воспроизвести ее поведение на Linux.

Переход -1к waitpidзаставит его ждатьлюбой дочерний процесс. Так что возникает вопрос:почему tinyесть дочерний процесс, ./tiny > >(cat)но нет ( ./tiny; : ) > >(cat)?

Я не вникал в bashисходный код, но у меня есть довольно обоснованное предположение о том, что происходит.

Сначала давайте разберем первую команду: ./tiny > >(cat). Сначала bashсоздает именованный канал, а затем fork()-exec()переходит catв создание в качестве дочернего процесса. Затем он устанавливает свой собственный stdoutканал с тем же именованным каналом. Наконец, bashзавершает свою жизнь, вызывая exec()преобразование в tiny. Теперь tinyимеет тот же PID, и ОС по-прежнему считает этот catпроцесс своим дочерним.

Важно то, что то же самое происходит с ( ./tiny ) > >(cat), но он просто exec()s в bash (скобки начинают подоболочку), а затем в tiny. Ключевым фактом, по-видимому, является то, что когда bashзапускается только с одной командой для выполнения, он не делает этого, fork()-exec()а просто exec()немедленно s.

Теперь давайте разберем вторую команду: ( ./tiny; : ) > >(cat). В начале мы получаем то же самое: fork()-exec()ing catв существование. Затем bash exec()s в новый bashэкземпляр. Затем он видит, что у него есть две команды для выполнения, поэтому он fork()-exec()в tinyсуществование, и поскольку он разветвился, этот новый tinyпроцесс не имеет catдочернего, поэтому он не зависает. Затем bashвыполняет :( :— это специальная встроенная функция, поэтому здесь нет exec, но использование не встроенной функции все равно приведет tinyк разветвлению, поэтому зависания все равно не будет).

Связанный контент