为什么 `echo | xargs > >(cat)` 在我的 Mac 上挂起?

为什么 `echo | xargs > >(cat)` 在我的 Mac 上挂起?

这可以在zsh和中重现bash

更让我困惑的是,echo | ( xargs; : ) > >(cat)它并没有挂起。这在zsh和中也可以重现bash

如果我使用 GNUxargs提供的,brew install findutils它就不会挂起:echo | gxargs > >(cat)

确实,除了我的系统之外,我还没有发现任何其他程序有这样的行为。我想文件描述符xargs可能出了问题,所以我尝试用或或其他许多方法进行替换。xargsxargsbash -c 'kill -9 $$'bash -c 'exec 0<&- 1<&-'

##mac我也在、、和Freenode 上寻求帮助#macosx,但似乎没有人知道发生了什么。##linux#bash我也在 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 上重现这种行为。

传递-1waitpid将导致它等待任何子进程。这就引出了一个问题:为什么tiny有子进程./tiny > >(cat)但没有( ./tiny; : ) > >(cat)

我还没有深入bash研究源代码,但我对正在发生的事情有一个相当有根据的猜测。

首先让我们分析一下第一个命令:./tiny > >(cat)。首先bash创建一个命名管道,然后fork()-exec()将其cat作为子进程创建。然后它将自己的设置stdout为相同的命名管道。最后通过调用转换为来bash结束其生命。现在具有相同的 PID,并且操作系统仍然将该进程视为其子进程。exec()tinytinycat

重要的是,同样的事情发生在 中,( ./tiny ) > >(cat)但它只是exec()进入 bash(括号启动一个子 shell),然后进入tiny。一个关键事实似乎是,当bash启动时只有一个命令要执行,它不会fork()-exec()而是exec()立即进入。

现在让我们剖析第二个命令:( ./tiny; : ) > >(cat)。我们在开始时得到了同样的东西:fork()-exec()进入cat存在。然后bash exec()进入一个新bash实例。然后它看到它有两个命令要执行,所以它fork()-exec()进入tiny存在,并且因为它分叉了,所以这个新tiny进程没有cat子进程,所以它不会挂起。然后bash执行::是一个特殊的内置函数,所以这里没有 exec,但使用非内置函数仍然会导致tiny分叉,所以仍然不会有任何挂起)。

相关内容