なぜ私の Mac では `echo | xargs > >(cat)` がハングするのでしょうか?

なぜ私の Mac では `echo | xargs > >(cat)` がハングするのでしょうか?

これはzshおよびで再現可能ですbash

さらに混乱するのは、echo | ( xargs; : ) > >(cat)ハングアップしないことです。これは、およびでも再現可能zshですbash

xargs提供されているGNU を使用すると、brew install findutilsハングしませんecho | gxargs > >(cat)

実際、私のシステム以外でこのように動作するプログラムは見つかりませんでした。ファイル記述子にxargs何か問題があるのではないかと考え、またはに置き換えてみたり、さまざまな手探りの作業を試しました。xargsxargsbash -c 'kill -9 $$'bash -c 'exec 0<&- 1<&-'

##mac私は、、、、 Freenodeでも助けを求めました#macosxが、誰も何が起こっているのか分かっていないようでした。##linux#bashStack 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.1shell_cmds-203Appleのオープンソースサイト

xargs私はそのパッケージの のバージョンを でコンパイルしgcc -g *.c、 を実行しecho | ./a.out > >(cat)、デバッガをプロセスlldbにアタッチしました。すると、からのa.outの呼び出しでスタックしていることがわかりました(waitpidxargs.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 を持ち、OS は依然としてそのプロセスを子プロセスと見なします。exec()tinytinycat

重要なのは、 でも同じことが起きます( ./tiny ) > >(cat)が、単にexec()bash (括弧でサブシェルを開始) に入り、次に に入るというtinyことです。重要な事実は、bashが実行するコマンドを 1 つだけ指定して起動された場合、 は ではなく、fork()-exec()ただちに になるということですexec()

では、2 番目のコマンド を分析してみましょう: ( ./tiny; : ) > >(cat)。最初は同じ結果になります:fork()-exec()cat作成されます。次に が作成され、bash exec()新しいbashインスタンスが作成されます。次に、実行するコマンドが 2 つあることがわかり、 が作成fork()-exec()tinyれます。これはフォークされているため、この新しいプロセスは子プロセスをtiny持たないため、ハングしません。次に が実行されます(は特殊な組み込みコマンドであるため、ここでは exec はありませんが、非組み込みコマンドを使用すると がフォークされるため、ハングは発生しません)。catbash::tiny

関連情報