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


Answers:


118

协同流程是一个ksh功能(已经在中ksh88)。zsh从一开始(90年代初)就具有此功能,而bash4.02009年才被添加。

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

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

在某些系统上,使用大多数外壳程序和套接字对以及最新版本的ksh93的未命名管道即可完成此操作。

在中a | cmd | ba将数据送入cmdb读取其输出。cmd作为协同进程运行,则外壳既可以是ab

ksh协同处理

在中ksh,您以以下方式启动协同处理:

cmd |&

您可以cmd通过执行以下操作来馈送数据:

echo test >&p

要么

print -p test

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

read var <&p

要么

read -p var

cmd启动任何后台作业,您可以使用fgbgkill它并指其%job-number或通过$!

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

exec 3>&p 3>&-

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

exec 3<&p 3<&-

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

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]}和。So ${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}>&-

ksh和中一样zsh,这些管道文件描述符被标记为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转到先前创建的管道。然后,关闭不需要的父管道中的写入端。因此,现在在外壳中,我们已经使用管道将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是,使用后不必清理那些命名管道。

容易死锁

外壳在一些构造中使用管道:

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

在这些情况下,数据在不同进程之间沿一个方向流动。

但是,通过协同处理和命名管道,很容易陷入僵局。您必须跟踪哪个命令打开了哪个文件描述符,以防止一个命令保持打开状态并使进程保持活动状态。死锁可能很难研究,因为死锁可能不确定地发生。例如,仅当发送满一个管道的数据时才发送。

效果比expect设计的要差

协同处理的主要目的是为Shell提供一种与命令进行交互的方式。但是,它不能很好地工作。

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

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

因为它的输出没有到达终端,所以tr缓冲它的输出。因此,它不会输出任何内容,除非它在上看到文件结尾stdin,或者它已积累了一个充满数据的缓冲区以供输出。因此,在上述外壳输出之后 a\n(仅2个字节),readwill将无限期地阻塞,因为它tr正在等待外壳向其发送更多数据。

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

这就是为什么,expect或者zpty改用伪终端。expect是一种用于与命令进行交互的工具,它做得很好。

文件描述符处理很麻烦,而且很难正确处理

与简单的外壳管道所允许的相比,协同处理可用于执行一些更复杂的管道。

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

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

全部使用管道。

例如:饲料输出printf '%s\n' foo bartr a bsed 's/./&&/g'cut -b2-获得是这样的:

foo
bbr
ffoooo
bbaarr
oo
ar

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

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

例如,使用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

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

同样,我们必须使用子外壳并exec cat在最后使用,以确保没有任何将外壳保持打开状态的过程。

使用ksh(here 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上类似的系统上,这将不起作用。)

在中ksh2除非将fds 明确地在命令行上传递,否则上面的fds 均标记有close-on-exec标志。这就是为什么我们没有关闭未使用的文件描述符像zsh-但它也是为什么我们需要做的{i1}>&$i1,用eval了的新的价值$i1,要传递给teecat...

bash此无法完成,因为您无法避免执行时关闭标志。

上面,它相对简单,因为我们仅使用简单的外部命令。当您想在那里使用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,或zshzpty,或命名管道。

如果要对管道进行一些华丽的配管,请使用命名管道。

协同处理可以完成上述部分操作,但对于不重要的任何事情,请做好严重的头部抓伤的准备。


确实是个好答案。我不知道具体何时修复,但至少从现在开始bash 4.3.11,您现在可以直接关闭coproc文件描述符,而无需使用aux。变量; 就您的答案中的示例而言,exec {tr[1]}<&- 现在可以正常工作(关闭coproc的stdin;请注意,您的代码(间接)尝试{tr[1]}使用来关闭>&-,但{tr[1]}它是coproc的stdin,必须使用来关闭<&-)。该修复程序必须介于之间4.2.25,该地方仍然显示问题,而之间4.3.11没有。
mklement0

1
@ mklement0,谢谢。exec {tr[1]}>&-确实确实适用于较新的版本,并且在CWRU / changelog条目中进行了引用(允许将{array [ind]}之类的单词用作有效的重定向... 2012-09-01)。exec {tr[1]}<&-(或者更正确的>&-等效项,尽管没有什么区别,因为只需要close()两者都没有)不会关闭coproc的stdin,而是关闭该coproc的管道的写端。
斯特凡Chazelas

1
@ mklement0,很好,我已经更新并添加了yash
斯特凡Chazelas

1
优点之一mkfifo是您不必担心争用条件和管道访问的安全性。您仍然需要担心fifo的死锁。
奥修斯

1
关于死锁:该stdbuf命令可以帮助防止死锁。我在Linux和bash下使用它。无论如何,我相信@StéphaneChazelas在结论中是正确的:“挠头”阶段对我来说只有在我切换回命名管道时才结束。
shub

7

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

在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

-1

什么是“ coproc”?

“ co-process”的缩写,表示与外壳协作的第二个过程。它与在命令末尾以“&”开头的后台作业非常相似,除了它的标准I / O通过特殊的方式连接到父外壳,而不是与其父外壳共享相同的标准输入和输出。一种称为FIFO的管道。有关参考,请单击此处

一个人在zsh中使用coproc启动

coproc command

必须准备好从stdin读取和/或写入stdout的命令,否则它不能用作coproc。

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


您可以在回答中添加一些文章吗?我试图使该主题涵盖在U&L中,因为该主题似乎人数不足。感谢您的回答!还要注意,我将标签设置为Bash,而不是zsh。
slm

@slm您已经指出了Bash黑客。我看到了足够的例子。如果您的目的是要引起人们的注意,那么您是成功的:>
Valentin Bajrami 2013年

它们不是特殊的管道,它们与一起使用|。(即在大多数shell中使用管道,在ksh93中使用套接字对)。管道和套接字对都是先进先出的,它们都是先进先出的。mkfifo创建命名管道,协进程不使用命名管道。
斯特凡Chazelas

@slm对zsh感到抱歉...实际上我在zsh上工作。有时我倾向于顺其自然。它也可以在Bash中正常工作
Munai Das Udasin

@ Stephane Chazelas我很确定我在某处读到它的I / O与称为FIFO的特殊管道连接的情况
Munai Das Udasin

-1

这是另一个很好的(并且可以正常工作的)示例-用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)
$
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.