这可以在zsh
和中重现bash
。
更让我困惑的是,echo | ( xargs; : ) > >(cat)
它并没有挂起。这在zsh
和中也可以重现bash
。
如果我使用 GNUxargs
提供的,brew install findutils
它就不会挂起:echo | gxargs > >(cat)
。
确实,除了我的系统之外,我还没有发现任何其他程序有这样的行为。我想文件描述符xargs
可能出了问题,所以我尝试用或或其他许多方法进行替换。xargs
xargs
bash -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
进程。我发现它卡在了对waitpid
from 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
结束其生命。现在具有相同的 PID,并且操作系统仍然将该进程视为其子进程。exec()
tiny
tiny
cat
重要的是,同样的事情发生在 中,( ./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
分叉,所以仍然不会有任何挂起)。