进程替换输出乱序

进程替换输出乱序

echo one; echo two > >(cat); echo three; 

命令给出意外的输出。

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

预期输出:

one
two
three

实际输出:

prompt$ echo one; echo two > >(cat); echo three;
one
three
prompt$ two

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

##### 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

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

问题:为什么它会这样?我的错误在哪里?需要全面的答案(并解释其bash幕后工作方式)。

答案1

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

对于一个<(...)人来说,这通常很好,如下所示:

cmd1 <(cmd2)

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

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

这是固定的,zsh等待在cmd2那里(但如果你把它写为cmd1 > >(cmd2)并且cmd1不是内置的,{cmd1} > >(cmd2)则改为使用 as有记录的)。

ksh默认情况下不等待,但让您使用内置等待它wait(它也使 pid 可用$!,尽管如果您这样做则没有帮助cmd1 >(cmd2) >(cmd3)

rc(使用cmd1 >{cmd2}语法),与其他相同,ksh只是您可以使用 . 获取所有后台进程的 pid $apids

es(也与)类似 incmd1 >{cmd2}等待,并且还等待进程内重定向。cmd2zshcmd2<{cmd2}

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

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

{ { cmd1 >(cmd2); } 3>&1 >&4 4>&- | cat; } 4>&1

这使得和 的 fd 3cmd1cmd2向管道开放。cat将等待另一端的文件结尾,因此通常仅在 和 都死亡时cmd1退出cmd2。 shell 将等待该cat命令。您可以将其视为捕获所有后台进程终止的网络(您可以将其用于在后台启动的其他操作,例如 with &、 coprocs 甚至是后台本身的命令,前提是它们不会像守护进程通常那样关闭所有文件描述符)。

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

{ { cmd1 >(sudo cmd2; exit); } 3>&1 >&4 4>&- | cat; } 4>&1

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

请注意,cat不会读取任何内容(因为进程不会在其 fd 3 上写入)。它只是用于同步。它只会执行一次read()系统调用,最终不会返回任何内容。

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

{ unused=$( { cmd1 >(cmd2); } 3>&1 >&4 4>&-); } 4>&1

这次,是 shell 而不是从另一端在和 的catfd 3 上打开的管道中读取数据。我们使用变量赋值,因此 的退出状态在 中可用。cmd1cmd2cmd1$?

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

{ cmd1 /dev/fd/3 3>&1 >&4 4>&- | cmd2 4>&-; } 4>&1

但请注意,如前所述,并非所有sh实现都会等待完成cmd1cmd2(尽管这比相反更好)。此时,$?包含 的退出状态cmd2;虽然bashzshmakecmd1的退出状态分别在${PIPESTATUS[0]}和中可用$pipestatus[1](另请参阅pipefail一些 shell 中的选项,以便$?可以报告除最后一个之外的管道组件的失败)

请注意,yash其流程也存在类似问题重定向特征。cmd1 >(cmd2)会写cmd1 /dev/fd/3 3>(cmd2)在那里。但cmd2不等待,您也不能使用wait等待它,并且它的 pid 也不在$!变量中可用。您将使用与 相同的解决方法bash

答案2

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

prompt$ echo one; echo two > >(cat) | cat; echo three;
one
two
three
prompt$

简短而简单。

=========

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

当您拥有 时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

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

相关内容