你如何在各种shell中使用命令coproc?

slm*_*slm 100 shell bash ksh zsh coprocesses

有人可以提供几个关于如何使用的例子coproc吗?

Sté*_*las 148

协同进程是一个ksh特性(已经在 中ksh88)。zsh不得不从一开始(90年代初)的功能,而它仅仅只被添加到bash4.0(2009年)。

但是,这 3 个外壳之间的行为和界面显着不同。

不过,这个想法是一样的:它允许在后台启动一项工作,并能够发送输入并读取其输出,而不必求助于命名管道。

这是通过在某些系统上使用具有最新版本的 ksh93 的大多数外壳和套接字对的未命名管道来完成的。

在 中a | cmd | ba将数据提供给cmdb读取其输出。cmd作为协同进程运行允许 shellab.

ksh 协同进程

在 中ksh,您启动一​​个协进程:

cmd |&
Run Code Online (Sandbox Code Playgroud)

您可以cmd通过执行以下操作来向其提供数据:

echo test >&p
Run Code Online (Sandbox Code Playgroud)

或者

print -p test
Run Code Online (Sandbox Code Playgroud)

并 readcmd的输出如下:

read var <&p
Run Code Online (Sandbox Code Playgroud)

或者

read -p var
Run Code Online (Sandbox Code Playgroud)

cmd作为任何后台作业启动,您可以在其上使用fg, bg,kill并通过%job-number或 via引用它$!

要关闭cmd正在读取的管道的写入端,您可以执行以下操作:

exec 3>&p 3>&-
Run Code Online (Sandbox Code Playgroud)

并关闭另一个管道的读取端(cmd正在写入的管道):

exec 3<&p 3<&-
Run Code Online (Sandbox Code Playgroud)

除非您首先将管道文件描述符保存到其他一些 fds,否则您无法启动第二个协同进程。例如:

tr a b |&
exec 3>&p 4<&p
tr b c |&
echo aaa >&3
echo bbb >&p
Run Code Online (Sandbox Code Playgroud)

zsh 协程

在 中zsh,协同过程几乎与 中的相同ksh。唯一真正的区别是zsh协同进程以coproc关键字开始。

coproc cmd
echo test >&p
read var <&p
print -p test
read -p var
Run Code Online (Sandbox Code Playgroud)

正在做:

exec 3>&p
Run Code Online (Sandbox Code Playgroud)

注意:这不会将coproc文件描述符移动到 fd 3(如 in ksh),而是复制它。因此,没有明确的方法来关闭馈送或读取管道,而是启动另一个 coproc.

例如,关闭进料端:

coproc tr a b
echo aaaa >&p # send some data

exec 4<&p     # preserve the reading end on fd 4
coproc :      # start a new short-lived coproc (runs the null command)

cat <&4       # read the output of the first coproc
Run Code Online (Sandbox Code Playgroud)

除了基于管道的协同进程,zsh(自 3.1.6-dev19 起,于 2000 年发布)还有基于伪 tty 的构造,如expect. 要与大多数程序交互,ksh 样式的协同进程将不起作用,因为程序在其输出是管道时开始缓冲。

这里有些例子。

启动协程x

zmodload zsh/zpty
zpty x cmd
Run Code Online (Sandbox Code Playgroud)

(这里cmd是一个简单的命令。但是你可以用eval或 函数做更漂亮的事情。)

馈送协处理数据:

zpty -w x some data
Run Code Online (Sandbox Code Playgroud)

读取协处理数据(在最简单的情况下):

zpty -r x var
Run Code Online (Sandbox Code Playgroud)

就像expect,它可以等待来自与给定模式匹配的协同处理的一些输出。

bash 协同进程

bash 语法更新了很多,并且建立在最近添加到 ksh93、bash 和 zsh 的新功能之上,该功能提供了一种允许处理 10 以上动态分配的文件描述符的语法。

bash提供基本 coproc语法和扩展语法。

基本语法

启动协同进程的基本语法如下所示zsh

coproc cmd
Run Code Online (Sandbox Code Playgroud)

kshzsh,管道和从共同处理进行访问>&p<&p

但是在 中bash,从协同进程和另一个管道到协同进程的管道的文件描述符在$COPROC数组中返回(分别是${COPROC[0]}${COPROC[1]}。所以......

向协同进程提供数据:

echo xxx >&"${COPROC[1]}"
Run Code Online (Sandbox Code Playgroud)

从协程读取数据:

read var <&"${COPROC[0]}"
Run Code Online (Sandbox Code Playgroud)

使用基本语法,您一次只能启动一个协同进程。

扩展语法

在扩展语法中,您可以命名您的协同进程(如在zshzpty 协同进程中):

coproc mycoproc { cmd; }
Run Code Online (Sandbox Code Playgroud)

该命令具有是一个复合命令。(请注意上面的示例如何让人想起function f { ...; }。)

这一次,文件描述符在${mycoproc[0]}和 中${mycoproc[1]}

您一次可以启动多个协同进程——但是当您在一个协同进程仍在运行时启动协同进程(即使在非交互模式下),您确实会收到警告。

您可以在使用扩展语法时关闭文件描述符。

coproc tr { tr a b; }
echo aaa >&"${tr[1]}"

exec {tr[1]}>&-

cat <&"${tr[0]}"
Run Code Online (Sandbox Code Playgroud)

请注意,以这种方式关闭在 4.3 之前的 bash 版本中不起作用,您必须改为编写它:

fd=${tr[1]}
exec {fd}>&-
Run Code Online (Sandbox Code Playgroud)

由于在kshzsh那些管文件描述符被标记为近距离上EXEC。

但是bash,通过对那些执行命令的唯一方法是将它们复制到FDS 012。这限制了您可以为单个命令交互的协同进程的数量。(请参见下面的示例。)

yash 进程和管道重定向

yash本身没有协同处理功能,但可以通过其管道流程重定向功能实现相同的概念。yash有一个pipe()系统调用的接口,所以这种事情可以在那里用手相对容易地完成。

你会开始一个协同进程:

exec 5>>|4 3>(cmd >&5 4<&- 5>&-) 5>&-
Run Code Online (Sandbox Code Playgroud)

它首先创建一个pipe(4,5)(5 写入端,4 读取端),然后将 fd 3 重定向到一个管道,该管道到一个在另一端运行其 stdin 的进程,并且 stdout 进入之前创建的管道。然后我们关闭我们不需要的父管道的写入端。所以现在在 shell 中,我们将 fd 3 连接到 cmd 的 stdin,fd 4 通过管道连接到 cmd 的 stdout。

请注意,这些文件描述符上未设置 close-on-exec 标志。

要馈送数据:

echo data >&3 4<&-
Run Code Online (Sandbox Code Playgroud)

读取数据:

read var <&4 3>&-
Run Code Online (Sandbox Code Playgroud)

您可以像往常一样关闭 fds:

exec 3>&- 4<&-
Run Code Online (Sandbox Code Playgroud)

使用命名管道几乎没有任何好处

使用标准命名管道可以轻松实现协同处理。我不知道什么时候引入了确切命名的管道,但有可能是在ksh提出协同进程之后(可能在 80 年代中期,ksh88 在 88 年“发布”,但我相信ksh几年前在 AT&T 内部使用那)这将解释为什么。

cmd |&
echo data >&p
read var <&p
Run Code Online (Sandbox Code Playgroud)

可以写成:

mkfifo in out

cmd <in >out &
exec 3> in 4< out
echo data >&3
read var <&4
Run Code Online (Sandbox Code Playgroud)

与这些交互更直接——尤其是当您需要运行多个协同进程时。(请参阅下面的示例。)

使用的唯一好处coproc是您不必在使用后清理那些命名管道。

容易死锁

Shell 在一些构造中使用管道:

  • 壳管: cmd1 | cmd2
  • 命令替换: $(cmd) ,
  • 进程替换: <(cmd) , >(cmd).

其中,数据在不同进程之间沿一个方向流动。

但是,对于协同进程和命名管道,很容易陷入僵局。您必须跟踪哪个命令打开了哪个文件描述符,以防止一个保持打开状态并使进程保持活动状态。死锁可能很难调查,因为它们可能非确定性地发生;例如,仅当发送与填满一个管道一样多的数据时。

expect它的设计目的更糟糕

协同进程的主要目的是为 shell 提供一种与命令交互的方式。但是,它并没有那么好用。

上面提到的最简单的死锁形式是:

tr a b |&
echo a >&p
read var<&p
Run Code Online (Sandbox Code Playgroud)

因为它的输出没有到达终端,所以tr缓冲它的输出。所以它不会输出任何东西,直到它看到它的文件结束stdin,或者它已经积累了一个充满缓冲区的数据来输出。所以上面,在shell有输出 a\n(只有2个字节)之后,read将无限期地阻塞,因为tr正在等待shell向它发送更多数据。

简而言之,管道不适合与命令交互。协同进程只能用于与不缓冲其输出的命令可以被告知不缓冲其输出的命令交互;例如,通过stdbuf在最近的 GNU 或 FreeBSD 系统上与某些命令一起使用。

这就是为什么expectzpty使用伪终端代替。expect是一个设计用于与命令交互的工具,它做得很好。

文件描述符处理很繁琐,而且很难正确处理

协同过程可用于进行一些比简单壳管允许的更复杂的管道。

其他 Unix.SE 答案有一个 coproc 用法示例。

这是一个简化的示例:假设您想要一个函数将命令输出的副本提供给其他 3 个命令,然后将这 3 个命令的输出连接起来。

都使用管道。

例如:输入printf '%s\n' foo barto tr a b, sed 's/./&&/g',的输出,并cut -b2-获得类似的东西:

foo
bbr
ffoooo
bbaarr
oo
ar
Run Code Online (Sandbox Code Playgroud)

首先,它不一定很明显,但是那里有死锁的可能性,并且它会在只有几千字节的数据后开始发生。

然后,根据您的 shell,您将遇到许多必须以不同方式解决的不同问题。

例如,使用zsh,您可以使用:

f() (
  coproc tr a b
  exec {o1}<&p {i1}>&p
  coproc sed 's/./&&/g' {i1}>&- {o1}<&-
  exec {o2}<&p {i2}>&p
  coproc cut -c2- {i1}>&- {o1}<&- {i2}>&- {o2}<&-
  tee /dev/fd/$i1 /dev/fd/$i2 >&p {o1}<&- {o2}<&- &
  exec cat /dev/fd/$o1 /dev/fd/$o2 - <&p {i1}>&- {i2}>&-
)
printf '%s\n' foo bar | f
Run Code Online (Sandbox Code Playgroud)

上面,协同进程 fds 设置了 close-on-exec 标志,但没有从它们复制的那些(如{o1}<&p)。因此,为了避免死锁,您必须确保在不需要它们的任何进程中关闭它们。

同样,我们必须使用子shell并exec cat在最后使用,以确保没有shell进程在保持管道打开。

有了ksh(这里ksh93),那必须是:

f() (
  tr a b |&
  exec {o1}<&p {i1}>&p
  sed 's/./&&/g' |&
  exec {o2}<&p {i2}>&p
  cut -c2- |&
  exec {o3}<&p {i3}>&p
  eval 'tee "/dev/fd/$i1" "/dev/fd/$i2"' >&"$i3" {i1}>&"$i1" {i2}>&"$i2" &
  eval 'exec cat "/dev/fd/$o1" "/dev/fd/$o2" -' <&"$o3" {o1}<&"$o1" {o2}<&"$o2"
)
printf '%s\n' foo bar | f
Run Code Online (Sandbox Code Playgroud)

注意:这不适ksh用于使用socketpairs代替 的系统pipes,以及/dev/fd/n在 Linux 上工作的系统。)

在 中ksh,上面2的fds标有 close-on-exec 标志,除非它们在命令行上明确传递。这就是为什么我们不必像 with 那样关闭未使用的文件描述符zsh的原因——但这也是为什么我们必须这样做{i1}>&$i1并使用eval的新值$i1, 传递给teeand cat...

bash这无法完成,因为您无法避免 close-on-exec 标志。

上面,比较简单,因为我们只使用简单的外部命令。当您想在其中使用 shell 构造时,情况会变得更加复杂,并且您开始遇到 shell 错误。

使用命名管道将上述内容与相同内容进行比较:

f() {
  mkfifo p{i,o}{1,2,3}
  tr a b < pi1 > po1 &
  sed 's/./&&/g' < pi2 > po2 &
  cut -c2- < pi3 > po3 &

  tee pi{1,2} > pi3 &
  cat po{1,2,3}
  rm -f p{i,o}{1,2,3}
}
printf '%s\n' foo bar | f
Run Code Online (Sandbox Code Playgroud)

结论

如果要与命令交互,请使用expect、 或zsh'szpty或命名管道。

如果你想用管道做一些花哨的管道,使用命名管道。

Co-processes 可以做上面的一些,但准备好为任何非平凡的事情做一些严肃的头脑风暴。

  • 相对于“mkfifo”的优点之一是您不必担心管道访问的竞争条件和安全性。你仍然需要担心 fifo 的死锁。 (3认同)
  • @mklement0,谢谢。`exec {tr[1]}&gt;&amp;-` 似乎确实适用于较新的版本,并且在 CWRU/changelog 条目中引用(_允许像 {array[ind]} 这样的词作为有效的重定向..._ 2012-09 -01)。`exec {tr[1]}&lt;&amp;-` (或者更正确的 `&gt;&amp;-` 等效项,尽管这没有什么区别,因为只是调用 `close()` )不会关闭 coproc 的标准输入,但是将管道末端写入该 coproc。 (2认同)

jll*_*gre 8

协同进程首先在 shell 脚本语言中与ksh88shell (1988)一起引入,后来在zsh1993 年之前的某个时间点引入。

在 ksh 下启动协同进程的语法是command |&. 从那里开始,您可以使用 写入command标准输入print -p并使用 读取其标准输出read -p

几十年后,缺少此功能的 bash 终于在其 4.0 版本中引入了它。不幸的是,选择了不兼容且更复杂的语法。

在 bash 4.0 及更新版本下,您可以使用以下coproc命令启动协同进程,例如:

$ coproc awk '{print $2;fflush();}'
Run Code Online (Sandbox Code Playgroud)

然后,您可以通过这种方式将某些内容传递给命令 stdin:

$ echo one two three >&${COPROC[1]}
Run Code Online (Sandbox Code Playgroud)

并使用以下命令读取 awk 输出:

$ read -ru ${COPROC[0]} foo
$ echo $foo
two
Run Code Online (Sandbox Code Playgroud)

在 ksh 下,应该是:

$ awk '{print $2;fflush();}' |&
$ print -p "one two three"
$ read -p foo
$ echo $foo
two
Run Code Online (Sandbox Code Playgroud)