进程替换输出乱序

Min*_*Max 16 bash process-substitution

echo one; echo two > >(cat); echo three; 
Run Code Online (Sandbox Code Playgroud)

命令给出了意外的输出。

我读到:如何在 bash 中实现进程替换?以及互联网上有关进程替换的许多其他文章,但不明白为什么它会这样。

预期输出:

one
two
three
Run Code Online (Sandbox Code Playgroud)

实际输出:

prompt$ echo one; echo two > >(cat); echo three;
one
three
prompt$ two
Run Code Online (Sandbox Code Playgroud)

此外,从我的角度来看,这两个命令应该是等效的,但它们不是:

##### first command - the pipe is used.
prompt$ seq 1 5 | cat
1
2
3
4
5
##### second command - the process substitution and redirection are used.
prompt$ seq 1 5 > >(cat)
prompt$ 1
2
3
4
5
Run Code Online (Sandbox Code Playgroud)

为什么我认为,它们应该是一样的?因为,两者都通过匿名管道将seq输出连接到cat输入 -维基百科,进程替换

问题:为什么它会这样?我的错误在哪里?需要全面的答案(并解释如何bash在引擎盖下进行)。

Sté*_*las 24

是的,bash就像在ksh(功能来自哪里)一样,不等待进程替换中的进程(在运行脚本中的下一个命令之前)。

对于<(...)一个,这通常很好,例如:

cmd1 <(cmd2)
Run Code Online (Sandbox Code Playgroud)

外壳将等待cmd1并且cmd1通常会cmd2通过读取直到被替换的管道上的文件结束而等待,并且该文件结束通常发生在cmd2死亡时。这与几个 shell(不是bash)不费心等待cmd2in 的原因相同cmd2 | cmd1

cmd1 >(cmd2)然而,对于,通常情况并非如此,因为它cmd2通常在cmd1那里等待,因此通常会在之后退出。

这是固定的zsh,在cmd2那里等待(但如果你把它写成cmd1 > >(cmd2)并且cmd1不是内置的,则不是,{cmd1} > >(cmd2)而是按照文档使用)。

ksh默认情况下不等待,但允许您使用wait内置函数等待它(它还使 pid 可用$!,但如果您这样做也无济于事cmd1 >(cmd2) >(cmd3)

rc(使用cmd1 >{cmd2}语法),ksh除了您可以使用$apids.

es(也cmd1 >{cmd2})等待cmd2zsh,并且还等待cmd2<{cmd2}过程重定向。

bash确实使cmd2(或更确切地说是子shell的pid,因为它确实cmd2在该子shell的子进程中运行,即使它是那里的最后一个命令)在 中可用$!,但不会让您等待它。

如果您确实必须使用bash,则可以通过使用等待两个命令的命令来解决该问题:

{ { cmd1 >(cmd2); } 3>&1 >&4 4>&- | cat; } 4>&1
Run Code Online (Sandbox Code Playgroud)

这使得两者cmd1cmd2有自己的FD 3开来管。cat将在另一端等待文件结束,因此通常仅在cmd1cmd2都死了时才会退出。shell 将等待该cat命令。您可以将其视为捕获所有后台进程终止的网络(您可以将它用于其他在后台启动的事情,例如 with &、 coprocs 甚至是后台的命令,前提是它们不会像守护进程那样关闭所有文件描述符)。

请注意,由于上面提到的浪费的子shell进程,即使cmd2关闭了它的 fd 3它也能工作(命令通常不这样做,但有些人喜欢sudossh这样做)。的未来版本bash可能最终会像在其他 shell 中一样在那里进行优化。那么你需要这样的东西:

{ { cmd1 >(sudo cmd2; exit); } 3>&1 >&4 4>&- | cat; } 4>&1
Run Code Online (Sandbox Code Playgroud)

确保还有一个额外的 shell 进程打开 fd 3 等待该sudo命令。

请注意,cat不会读取任何内容(因为进程不在其 fd 3 上写入)。它只是为了同步。它将只执行一个read()系统调用,该调用最后什么也没有返回。

您实际上可以cat通过使用命令替换来执行管道同步来避免运行:

{ unused=$( { cmd1 >(cmd2); } 3>&1 >&4 4>&-); } 4>&1
Run Code Online (Sandbox Code Playgroud)

这一次,它的外壳,而不是cat说从它的另一端是FD 3的开管道读取cmd1cmd2。我们正在使用变量赋值,因此 的退出状态cmd1$?.

或者您可以手动进行进程替换,然后您甚至可以使用您的系统,sh因为这将成为标准的 shell 语法:

{ cmd1 /dev/fd/3 3>&1 >&4 4>&- | cmd2 4>&-; } 4>&1
Run Code Online (Sandbox Code Playgroud)

但请注意,如前所述,并非所有sh实现都会cmd1cmd2完成后等待(尽管这比反过来要好)。那个时候,$?包含退出状态cmd2;虽然bashzsh化妆cmd1的可用退出状态${PIPESTATUS[0]},并$pipestatus[1]分别(也看到了pipefail在几个炮弹选项,这样$?可以报告管道元件比上次其他故障)

请注意,yash它的进程重定向功能也有类似的问题。cmd1 >(cmd2)会写cmd1 /dev/fd/3 3>(cmd2)在那里。但是cmd2没有等待,您也不能使用wait等待它,并且它的 pid 在$!变量中也不可用。您将使用与bash.


Nic*_*one 7

您可以将第二个命令通过管道传输到另一个命令中cat,该命令将等待其输入管道关闭。前任:

prompt$ echo one; echo two > >(cat) | cat; echo three;
one
two
three
prompt$
Run Code Online (Sandbox Code Playgroud)

简短而简单。

=========

看似简单,幕后却发生了很多事情。如果您对其工作原理不感兴趣,您可以忽略答案的其余部分。

当您拥有 时echo two > >(cat); echo three>(cat)会被交互式 shell 分叉,并独立于 运行echo two。因此,echo two完成,然后echo three执行,但在>(cat)完成之前。当从意想不到的时间(几毫秒后)bash获取数据时,它会给您类似提示的情况,您必须按换行符才能返回终端(就像另一个用户向您发送信息一样)。>(cat)mesg

但是,给定echo two > >(cat) | cat; echo three,会生成两个子 shell(根据|符号的文档)。

一个名为 A 的子 shell 是 for echo two > >(cat),一个名为 B 的子 shell 是 for cat。A 自动连接到 B(A 的标准输出是 B 的标准输入)。然后,echo two开始>(cat)执行。>(cat)的 stdout 设置为 A 的 stdout,等于 B 的 stdin。完成后echo two,A 退出,关闭其标准输出。然而,>(cat)仍然保留对 B 的 stdin 的引用。第二个cat的 stdin 保存着 B 的 stdin,并且cat在看到 EOF 之前不会退出。仅当没有人再以写入模式打开文件时才会给出 EOF,因此>(cat)的 stdout 会阻塞第二个cat。B 仍在等待那一刻cat。由于echo two退出,>(cat)最终得到一个 EOF,因此>(cat)刷新其缓冲区并退出。没有人再持有 B's/secondcat的 stdin,因此第二个cat读取 EOF (B 根本不读取其 stdin,它不在乎)。这个 EOF 导致第二个cat刷新其缓冲区,关闭其标准输出,然后退出,然后 B 退出,因为cat已退出并且 B 正在等待cat

需要注意的是,bash 还会生成>(cat)!的子 shell。正因为如此,你会看到

echo two > >(sleep 5) | cat; echo three

echo three即使sleep 5不保存 B 的标准输入,在执行之前仍会等待 5 秒。这是因为为 生成的隐藏子 shell C>(sleep 5)正在等待sleep,并且 C 正在保存 B 的标准输入。你可以看看如何

echo two > >(exec sleep 5) | cat; echo three

但是不会等待,因为sleep不保存 B 的标准输入,并且没有幽灵子 shell C 保存 B 的标准输入(exec 将强制 sleep 来替换 C,而不是分叉并使 C 等待sleep)。不管这个警告,

echo two > >(exec cat) | cat; echo three

仍将按顺序正确执行功能,如前所述。