努力理解管道和子外壳中的重定向:代码解释将受到高度赞赏

努力理解管道和子外壳中的重定向:代码解释将受到高度赞赏

请考虑终端会话(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”,但我不明白为什么结果文件中没有任何内容。我对发生的情况的理解如下:

  1. 首先,fd 3 被设置为 的副本stdout。我确信这种情况发生在管道执行之前,因为否则管道中的任何命令都无法访问 fd 3,这将导致“错误描述符”错误消息。

  2. 管道不是一个简单的命令,因此需要生成一个子 shell 来执行它。子 shell 继承父 shell 的执行环境,包括文件描述符和重定向。[1]

  3. 管道中的每个命令也在其自己的子 shell 中执行[2],再次继承执行环境和文件描述符。echo的输出被重定向到 fd 3,而 fd 3 又从之前被复制stdout,总之,这导致echo的输出出现stdout(输出转到 fd 3,再转到 fd 1,即 stdout)。

  4. 但我不明白为什么echo的输出没有进入结果文件。来自bash手册(强调我的):

管道中每个命令的输出通过管道连接到下一个命令的输入。也就是说,每个命令都会读取前一个命令的输出。此连接在命令指定的任何重定向之前执行。

我的理解是,echo的输出应该连接到cat的输入>&3分别设置或应用重定向。但如果这是真的,则在执行命令后结果文件将存在(并包含“test”)。所以我的理解显然是错误的。

有人可以解释一下我缺少什么吗?

更新,基于下面 AB 和 Gilles 的出色回答,并附有进一步的解释

我的担忧源于我在上面第 3 条中所写的内容。但事实并非如此。另请参阅吉尔斯的回答。

AB 是第一个提供答案的人(见下文)。然而,我需要一些时间来理解它。因此,我将解释一些段落,以便更容易理解。

  1. 该行的最后一部分:3>&1首先完成:指向终端输出的 fd 1 被复制到 fd 3。这意味着 fd 1 和 fd 3 现在都指向终端输出。它们是相同的并且可以互换使用。

  2. 在分叉之前,通常使用系统调用在下一个可用的 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. >&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 系统上。

  1. 该行的最后一部分:3>&1首先完成:FD1 指向终端被复制为FD3(通常使用dup2(2)系统调用)。
  2. 在分叉之前,会创建一个管道,通常使用pipe(2)系统调用,下次可用时FDs:比方说 4 和 5。准备过程然后分为 futureecho和 future cat。 proto-echo dups 5 到 1 (“覆盖”它指向的位置:终端),关闭 5 和 execs echo, proto-cat dups2() 4 到 0,关闭 4 和 execs catFD3 到处继承。
  3. >&3与第 1 点相反:它重复FD3(指向终端)FD1. 因此管道的写入侧已被替换并且现在已关闭(dup2(2)说:“如果文件描述符新FD以前是打开的,在重新使用之前它会被静默关闭。”)。不会向管道写入任何内容。终端接收test并显示它。
  4. 并行cat打开并截断目标文件result并开始从管道读取。这会触发 EOFpipe(7)因为写入端已关闭:cat命令结束。
  5. 主 shell 进程没有剩余子进程:执行结束

结果:test在终端上且result文件为空。

相关内容