Bash 协进程

作者:Mitch Frazier

bash 4.0 的新特性之一是coproc语句。这个coproc语句允许您创建一个协进程,该协进程通过两个管道连接到调用它的 shell:一个用于向协进程发送输入,另一个用于从协进程获取输出。

我发现的第一个用途是在尝试进行日志记录并使用 exec 重定向 时发现的。目标是允许您选择性地开始将脚本的所有输出写入日志文件,一旦脚本已经开始(例如,由于使用了 --log 命令行选项)。

在脚本已经开始后记录输出的主要问题是,脚本可能在调用时输出已经被重定向(到文件或管道)。如果我们改变输出的去向,当输出已经被重定向时,我们将不会按照用户的意图执行命令。

之前的 尝试最终使用了命名管道

#!/bin/bash

echo hello

if test -t 1; then
    # Stdout is a terminal.
    exec >log
else
    # Stdout is not a terminal.
    npipe=/tmp/$$.tmp
    trap "rm -f $npipe" EXIT
    mknod $npipe p
    tee <$npipe log &
    exec 1>&-
    exec 1>$npipe
fi

echo goodbye

来自之前的文章

在这里,如果脚本的 stdout 没有连接到终端,我们使用 mknod 创建一个命名管道(一个存在于文件系统中的管道),并设置一个 trap 在退出时删除它。然后我们在后台启动 tee,从命名管道读取并写入日志文件。请记住,tee 也会将其 stdin 上读取的任何内容写入其 stdout。还要记住,tee 的 stdout 也与脚本的 stdout(我们的主脚本,即调用 tee 的脚本)相同,因此来自 tee 的 stdout 的输出将转到我们当前的 stdout 的任何位置(即,用户在命令行上指定的重定向或管道)。因此,此时我们让 tee 的输出转到它需要去的地方:进入用户指定的重定向/管道。

我们可以使用协进程做同样的事情

echo hello

if test -t 1; then
    # Stdout is a terminal.
    exec >log
else
    # Stdout is not a terminal.
    exec 7>&1
    coproc tee log 1>&7
    #echo Stdout of coproc: ${COPROC[0]} >&2
    #echo Stdin of coproc: ${COPROC[1]} >&2
    #ls -la /proc/$$/fd
    exec 7>&-
    exec 7>&${COPROC[1]}-
    exec 1>&7-
    eval "exec ${COPROC[0]}>&-"
    #ls -la /proc/$$/fd
fi
echo goodbye
echo error >&2

如果我们的标准输出要输出到终端,那么我们就像以前一样,只使用 exec 将我们的输出重定向到所需的日志文件。如果我们的输出不输出到终端,那么我们使用 coproc 运行tee作为协进程,并将我们的输出重定向到 tee 的输入,并将 tee 的输出重定向到我们输出最初要去的地方。

使用 coproc 语句运行 tee 本质上与在后台运行 tee 相同(例如,tee log &),主要的区别在于 bash 运行 tee 时,其输入和输出都连接到管道。Bash 将这些管道的文件描述符放入一个名为COPROC(默认情况下)的数组中

  • COPROC[0]是连接到协进程标准输出的管道的文件描述符
  • COPROC[1]连接到协进程的标准输入。

请注意,这些管道是在命令中进行任何重定向之前创建的。

关注原始脚本的输出未连接到终端的部分。以下行将我们的标准输出复制到文件描述符 7。

exec 7>&1

然后我们启动 tee,将其输出重定向到文件描述符 7。

coproc tee log 1>&7

因此,tee 现在会将其标准输入上读取的任何内容写入名为log的文件和文件描述符 7,即我们原始的标准输出。

现在我们使用以下命令关闭文件描述符 7(请记住,tee 仍然将文件描述符 7 上打开的“文件”作为其标准输出打开):

exec 7>&-

由于我们已经关闭了 7,我们可以重用它,所以我们将连接到 tee 输入的管道移动到 7,使用

exec 7>&${COPROC[1]}-

然后我们通过以下命令将我们的标准输出移动到连接到 tee 标准输入的管道(我们的文件描述符 7):

exec 1>&7-

最后,我们关闭连接到 tee 输出的管道,因为我们不需要它,使用

eval "exec ${COPROC[0]}>&-"

这里的eval是必需的,因为否则 bash 会认为${COPROC[0]}的值是命令名称。另一方面,在上面的语句中(exec 7>&${COPROC[1]}-)不需要,因为在那条语句中 bash 可以识别出 "7" 是文件描述符操作的开始,而不是命令。

另请注意注释掉的命令

#ls -la /proc/$$/fd

这对于查看当前进程打开的文件很有用。

我们现在已经达到了预期的效果:我们的标准输出正在进入 tee。Tee 正在将其“记录”到我们的日志文件中,并将其写入我们输出最初要去的管道或文件。

到目前为止,我还没有想出协进程的其他用途,至少不是人为的用途。有关协进程的更多信息,请参阅 bash 手册页。

Mitch Frazier 是 Emerson Electric Co. 的嵌入式系统程序员。自 2000 年代初期以来,Mitch 一直是 Linux Journal 的贡献者和朋友。

加载 Disqus 评论