这
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
)不费心等待cmd2
in 的原因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}
等待,并且还等待进程内重定向。cmd2
zsh
cmd2
<{cmd2}
bash
确实使 pid cmd2
(或更准确地说是子 shell 的 pid,因为它确实cmd2
在该子 shell 的子进程中运行,即使它是那里的最后一个命令)在 中可用$!
,但不允许您等待它。
如果您确实必须使用bash
,则可以通过使用将等待这两个命令的命令来解决该问题:
{ { cmd1 >(cmd2); } 3>&1 >&4 4>&- | cat; } 4>&1
这使得和 的 fd 3cmd1
都cmd2
向管道开放。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 而不是从另一端在和 的cat
fd 3 上打开的管道中读取数据。我们使用变量赋值,因此 的退出状态在 中可用。cmd1
cmd2
cmd1
$?
或者您可以手动进行进程替换,然后您甚至可以使用系统的,sh
因为这将成为标准 shell 语法:
{ cmd1 /dev/fd/3 3>&1 >&4 4>&- | cmd2 4>&-; } 4>&1
但请注意,如前所述,并非所有sh
实现都会等待完成cmd1
后cmd2
(尽管这比相反更好)。此时,$?
包含 的退出状态cmd2
;虽然bash
和zsh
makecmd1
的退出状态分别在${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
仍将按顺序正确执行功能,如前所述。