请考虑来自终端会话(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,这是标准输出)。但我不明白为什么
echo
's 的输出没有进入结果文件。从bash 手册(强调我的):
管道中每个命令的输出通过管道连接到下一个命令的输入。也就是说,每个命令都读取前一个命令的输出。此连接在命令指定的任何重定向之前执行。
我理解这一点,即在设置或应用重定向之前echo
,应该将 ' 的输出连接到cat
' 的输入。但如果这是真的,那么在命令执行后结果文件将存在(并包含“测试”)。所以我的理解显然是错误的。>&3
有人可以解释我错过了什么吗?
更新,基于 AB 和 Gilles 在下面的出色回答,并提供进一步的解释
我担心的根源是我在上面第 3 项中所写的。它只是不那样工作。另见吉尔斯的回答。
AB 是第一个提供答案的人(见下文)。但是,我需要一些时间来理解它。因此,我将解释一些段落,以便更容易理解。
该行的最后一部分:
3>&1
首先完成:指向终端输出的 fd 1 被复制到 fd 3。这意味着 fd 1 和 fd 3 现在都指向终端输出。它们是相同的,可以互换使用。在 fork 之前,通常使用系统调用在下一个可用的 fd 上创建一个管道
pipe(2)
:假设 fd 4 和 fd 5。然后准备过程分叉为 future echo 和 future cat,执行以下步骤
: 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 不再指向终端输入,而是指向管道的接收端。在这个阶段,for 的输入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 一无所知。但是,我的示例中缺少该部分,我想保留它以免分散实际问题的注意力)。]
那是因为 OP 的第 4 条,它的工作原理是这样的,fd继承了各种进程的创建/执行。我没有写所有发生 fork/exec 的地方。我当然会简化其中的一些(使用内置命令......)。为 Linux 提供的文档链接,但应该在任何 POSIX 或类似 POSIX 的系统上发生相同的行为。
3>&1
首先完成:指向终端的fd 1 被复制为fd 3(通常使用dup2(2)
系统调用)。pipe(2)
系统调用在下一个可用的fd上创建一个管道:假设 4 和 5。然后准备过程分叉到 futureecho
和 futurecat
。proto-echo dups 5 到 1(“覆盖”它指向:终端),关闭 5 和 execsecho
,proto-cat dups2() 4 到 0,关闭 4 和 execscat
。fd 3 到处都是继承的。>&3
与第 1 条相反:它将fd 3(指向终端)复制到fd 1。因此管道的写入端已被替换,现在已关闭(dup2(2)
说:“如果文件描述符newfd以前打开过,它会静默在重新使用之前关闭。”)。任何东西都不会写入管道。终端接收test
并显示它。cat
打开并截断目标文件result
并开始从管道读取。这会触发 EOF,pipe(7)
因为写入端是/已关闭:cat
命令结束。结果:
test
在终端和空result
文件上。如前所述,这是正确的,但有点奇怪,您似乎误解了这意味着什么。这并不意味着在此重定向生效的地方,写入 fd 3 等同于写入 stdout。这意味着 fd 3 连接到在设置重定向的点连接到的任何 stdout。如果您在终端中运行此代码,
3>&1
请将文件描述符 3 连接到终端。所以…FD 3 是终端。在某些时候它恰好也是其他进程的 fd 1 的事实是一个不相关的历史细节。