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

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

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

答案1

协同进程是一项ksh功能(已在 中ksh88)。从一开始(90 年代初)就有该功能,而它是在(2009 年)zsh才添加的。bash4.0

然而,这 3 个 shell 之间的行为和界面有显着差异。

不过,想法是相同的:它允许在后台启动作业,并且能够向其发送输入并读取其输出,而无需诉诸命名管道。

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

在 中a | cmd | ba将数据馈送到cmdb读取其输出。cmd作为协进程运行允许 shell 既是 又ab

ksh 协同处理

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

cmd |&

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

echo test >&p

或者

print -p test

cmd使用以下内容读取 的输出:

read var <&p

或者

read -p var

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

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

exec 3>&p 3>&-

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

exec 3<&p 3<&-

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

tr a b |&
exec 3>&p 4<&p
tr b c |&
echo aaa >&3
echo bbb >&p

zsh 协同进程

在 中zsh,协进程几乎与 中的相同ksh。唯一真正的区别是zsh协同进程是用coproc关键字启动的。

coproc cmd
echo test >&p
read var <&p
print -p test
read -p var

正在做:

exec 3>&p

注意:这不会将coproc文件描述符移动到 fd 3(如 中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

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

这里有些例子。

启动协同进程x

zmodload zsh/zpty
zpty x cmd

(这里cmd是一个简单的命令。但是您可以使用eval或 函数做更奇特的事情。)

馈送协同处理数据:

zpty -w x some data

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

zpty -r x var

与 类似expect,它可以等待协同进程匹配给定模式的某些输出。

bash 协同进程

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

bash提供基本的 coproc语法,以及扩展一。

基本语法

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

coproc cmd

ksh或 中zsh,进出协进程的管道可通过>&p和访问<&p

但是在 中bash,来自协进程的管道和另一个通向协进程的管道的文件描述符在数组中返回$COPROC(分别为${COPROC[0]}${COPROC[1]}。所以……

将数据提供给协同进程:

echo xxx >&"${COPROC[1]}"

从协进程读取数据:

read var <&"${COPROC[0]}"

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

扩展语法

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

coproc mycoproc { cmd; }

命令成为复合命令。 (注意上面的例子如何让人想起function f { ...; }。)

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

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

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

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

exec {tr[1]}>&-

cat <&"${tr[0]}"

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

fd=${tr[1]}
exec {fd}>&-

与 和 中一样kshzsh这些管道文件描述符被标记为 close-on-exec。

但在 中bash,将它们传递给已执行命令的唯一方法是将它们复制到 fds 012。这限制了您可以与单个命令交互的协同进程的数量。 (请参阅下面的示例。)

yash 流程和管道重定向

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

您可以与以下人员启动协同流程:

exec 5>>|4 3>(cmd >&5 4<&- 5>&-) 5>&-

首先创建一个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<&-

读取数据:

read var <&4 3>&-

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

exec 3>&- 4<&-

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

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

cmd |&
echo data >&p
read var <&p

可以写成:

mkfifo in out

cmd <in >out &
exec 3> in 4< out
echo data >&3
read var <&4

与这些交互更加简单,尤其是当您需要运行多个协同进程时。 (参见下面的示例。)

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

容易出现死锁

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

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

其中,数据流入只有一个不同进程之间的方向。

然而,使用协同进程和命名管道,很容易陷入死锁。您必须跟踪哪个命令打开了哪个文件描述符,以防止文件描述符保持打开状态并使进程保持活动状态。死锁的调查可能很棘手,因为它们的发生可能是不确定的。例如,仅当发送的数据足以填满一个管道时。

expect效果比设计的要差

协同进程的主要目的是为 shell 提供一种与命令交互的方式。然而,它的效果并不那么好。

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

tr a b |&
echo a >&p
read var<&p

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

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

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

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

协同处理可用于完成一些比简单壳管允许的更复杂的管道工作。

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

这是一个简化的示例:想象一下,您需要一个函数,将一个命令的输出副本提供给另外 3 个命令,然后将这 3 个命令的输出连接起来。

全部使用管道。

例如:将 的输出提供给printf '%s\n' foo bartr a bsed 's/./&&/g'cut -b2-以获得类似以下内容:

foo
bbr
ffoooo
bbaarr
oo
ar

首先,这并不一定是显而易见的,但是那里有可能出现死锁,并且只有几千字节的数据后就会开始发生死锁。

然后,根据您的 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

上面,协进程 fd 设置了 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

笔记:ksh这在使用socketpairs代替 的系统上不起作用pipes,并且/dev/fd/n像 Linux 上那样工作。)

在 中ksh,上面的 fd2被标记为 close-on-exec 标志,除非它们在命令行上显式传递。这就是为什么我们不必像 with 那样关闭未使用的文件描述符zsh- 但这也是为什么我们必须将{i1}>&$i1eval新值$i1传递给teeand cat

这是bash不可能的,因为你无法避免 close-on-exec 标志。

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

将上面的内容与使用命名管道的内容进行比较:

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

结论

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

如果您想使用管道进行一些奇特的管道操作,请使用命名管道。

协同进程可以完成上述一些任务,但要准备好为任何不平凡的事情做一些严重的令人头疼的事情。

答案2

协进程首次在 shell 脚本语言中与ksh88shell 一起引入(1988 年),后来zsh在 1993 年之前的某个时候引入。

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

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

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

$ coproc awk '{print $2;fflush();}'

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

$ echo one two three >&${COPROC[1]}

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

$ read -ru ${COPROC[0]} foo
$ echo $foo
two

在 ksh 下,这将是:

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

答案3

这是另一个很好的(并且有效的)示例——一个用 BASH 编写的简单服务器。请注意,您需要 OpenBSD netcat,经典的无法运行。当然,您可以使用 inet 套接字代替 unix 套接字。

server.sh:

#!/usr/bin/env bash

SOCKET=server.sock
PIDFILE=server.pid

(
    exec </dev/null
    exec >/dev/null
    exec 2>/dev/null
    coproc SERVER {
        exec nc -l -k -U $SOCKET
    }
    echo $SERVER_PID > $PIDFILE
    {
        while read ; do
            echo "pong $REPLY"
        done
    } <&${SERVER[0]} >&${SERVER[1]}
    rm -f $PIDFILE
    rm -f $SOCKET
) &
disown $!

client.sh:

#!/usr/bin/env bash

SOCKET=server.sock

coproc CLIENT {
    exec nc -U $SOCKET
}

{
    echo "$@"
    read
} <&${CLIENT[0]} >&${CLIENT[1]}

echo $REPLY

用法:

$ ./server.sh
$ ./client.sh ping
pong ping
$ ./client.sh 12345
pong 12345
$ kill $(cat server.pid)
$

答案4

什么是“协程”?

它是“co-process”的缩写,意思是与 shell 合作的第二个进程。它与在命令末尾以“&”开始的后台作业非常相似,不同之处在于它的标准 I/O 通过特殊的 I/O 连接到父 shell,而不是与其父 shell 共享相同的标准输入和输出。一种称为 FIFO 的管道。供参考点击这里

在 zsh 中启动 coproc:

coproc command

该命令必须准备好从 stdin 读取和/或写入 stdout,否则它作为 coproc 没有多大用处。

阅读这篇文章这里它提供了 exec 和 coproc 之间的案例研究

相关内容