请考虑终端会话(Debian Buster、Bash 5.0)中的以下日志:
root@cerberus ~/scripts # rm -f result
root@cerberus ~/scripts # { { echo test; } | cat > result; }
root@cerberus ~/scripts # cat result
test
root@cerberus ~/scripts #
这里没什么特别的,这是预期的行为,我理解它。
但我不明白以下情况下的行为:
root@cerberus ~/scripts # rm -f result
root@cerberus ~/scripts # { { echo test >&3; } | cat > result; } 3>&1
test
root@cerberus ~/scripts # cat result
root@cerberus ~/scripts #
准确地说,我相信我理解为什么执行第二行时会输出“test”,但我不明白为什么结果文件中没有任何内容。我对发生的情况的理解如下:
首先,fd 3 被设置为 的副本
stdout
。我确信这种情况发生在管道执行之前,因为否则管道中的任何命令都无法访问 fd 3,这将导致“错误描述符”错误消息。管道不是一个简单的命令,因此需要生成一个子 shell 来执行它。子 shell 继承父 shell 的执行环境,包括文件描述符和重定向。[1]
管道中的每个命令也在其自己的子 shell 中执行[2],再次继承执行环境和文件描述符。
echo
的输出被重定向到 fd 3,而 fd 3 又从之前被复制stdout
,总之,这导致echo
的输出出现stdout
(输出转到 fd 3,再转到 fd 1,即 stdout)。但我不明白为什么
echo
的输出没有进入结果文件。来自bash手册(强调我的):
管道中每个命令的输出通过管道连接到下一个命令的输入。也就是说,每个命令都会读取前一个命令的输出。此连接在命令指定的任何重定向之前执行。
我的理解是,echo
的输出应该连接到cat
的输入前>&3
分别设置或应用重定向。但如果这是真的,则在执行命令后结果文件将存在(并包含“test”)。所以我的理解显然是错误的。
有人可以解释一下我缺少什么吗?
更新,基于下面 AB 和 Gilles 的出色回答,并附有进一步的解释
我的担忧源于我在上面第 3 条中所写的内容。但事实并非如此。另请参阅吉尔斯的回答。
AB 是第一个提供答案的人(见下文)。然而,我需要一些时间来理解它。因此,我将解释一些段落,以便更容易理解。
该行的最后一部分:
3>&1
首先完成:指向终端输出的 fd 1 被复制到 fd 3。这意味着 fd 1 和 fd 3 现在都指向终端输出。它们是相同的并且可以互换使用。在分叉之前,通常使用系统调用在下一个可用的 fd 上创建管道
pipe(2)
:假设 fd 4 和 fd 5。然后,准备过程分叉到 future echo 和 future cat,执行以下步骤:
a) 准备过程echo 的工作原理如下:
fd 5 复制到 fd 1 (覆盖 fd 1 指向的位置:终端输出)。这意味着 fd 1 现在与 fd 5 相同,并且它们可以互换使用。具体来说,fd 1不再指向终端输出,而是指向管道的写入端。
在此阶段(但见下文), 的输出echo
将转到管道的写入端,因为echo
写入到 fd 1,它指向该写入端。
因为我们不需要为同一件事使用两个文件描述符,并且因为echo
无论如何都会写入 fd 1,所以 fd 5 现在会关闭。
然后echo
被执行,但是在设置了后面提到的附加重定向之后(参见 3.)。
b) 同样,复制fd 4到fd 0的准备过程cat
,意味着fd 0不再指向终端输入,而是指向管道的接收端。在此阶段,输入cat
将来自管道的接收端,因为cat
从 fd 0 读取,并且 fd 0 连接到该接收端。因为我们不需要两个文件描述符来表示同一件事,并且因为cat
无论如何都从 fd 0 读取,所以 fd 4 现在被关闭。然后cat
就被执行了。
当这一切发生时,fd 3 到处都会被继承。>&3
与项目符号 1 相反:它将 fd 3 复制到 fd 1。 fd 3 已创建,以便它指向终端输出,并由执行管道的子 shell 和执行各个管道命令的其他子 shell 继承。
在步骤 2a) 中,fd 1 已指向管道的写入侧。但现在,重定向>&3
再次覆盖 fd 1 并使其等于 fd 3,而 fd 3 又(仍然)指向终端输出。这意味着 fd 1 不再指向管道的写入端,而是指向终端输出。这就是执行管道时终端上出现“test”的原因(请记住,echo
无论 fd 1 指向何处,始终写入 fd 1)。
另外,当 fd 1 被重定向“覆盖”时,其旧版本将被关闭(因为底层系统调用dup2(2)
会执行此操作)。由于其旧版本指向管道的写入端,因此该写入端现在已关闭。
因此,接收端cat
将不会收到任何数据。他们立即收到 EOF 通知。这就是为什么cat
没有收到任何内容以及结果文件保持为空或被截断的原因。
[旁注:我应该在重定向后关闭 fd 3 (也就是说,我们应该写成>&3 3>&-
)>&3
,因为echo
- 如上所述 - 写入 fd 1 并且根本不知道有关 fd 3 的任何信息。然而,我的示例中缺少该部分,我想保留它,以免分散对实际问题的注意力)。 ]
答案1
1. 首先,fd 3 被设置为 stdout 的副本。
如上所述,这是正确的,但有点奇怪,您似乎误解了这意味着什么。这并不意味着在该重定向有效的情况下,写入 fd 3 相当于写入 stdout。这意味着 fd 3 连接到在设置重定向时连接到的任何 stdout。如果您在终端中运行此代码,3>&1
请将文件描述符 3 连接到终端。所以…
3. (…)
echo
的输出被重定向到 fd 3,而 fd 3 又是之前从 stdout 复制的,总之,这导致 echo 的输出出现在 stdout 上(输出转到 fd 3,fd 3 转到 fd 1,即 stdout )。
FD 3 是终端。事实上,在某个时刻它碰巧也是其他进程的 fd 1,这是一个无关的历史细节。
答案2
那是因为OP的第4条它是这样工作的,FD沿着进程的各种创建/执行继承。我没有写所有发生 fork/exec 的地方。我当然正在简化其中的一些(使用内置命令......)。为 Linux 提供了文档链接,但相同的行为应该发生在任何 POSIX 或类 POSIX 系统上。
- 该行的最后一部分:
3>&1
首先完成:FD1 指向终端被复制为FD3(通常使用dup2(2)
系统调用)。 - 在分叉之前,会创建一个管道,通常使用
pipe(2)
系统调用,下次可用时FDs:比方说 4 和 5。准备过程然后分为 futureecho
和 futurecat
。 proto-echo dups 5 到 1 (“覆盖”它指向的位置:终端),关闭 5 和 execsecho
, proto-cat dups2() 4 到 0,关闭 4 和 execscat
。FD3 到处继承。 >&3
与第 1 点相反:它重复FD3(指向终端)FD1. 因此管道的写入侧已被替换并且现在已关闭(dup2(2)
说:“如果文件描述符新FD以前是打开的,在重新使用之前它会被静默关闭。”)。不会向管道写入任何内容。终端接收test
并显示它。- 并行
cat
打开并截断目标文件result
并开始从管道读取。这会触发 EOFpipe(7)
因为写入端已关闭:cat
命令结束。 - 主 shell 进程没有剩余子进程:执行结束
结果:test
在终端上且result
文件为空。