如何在两个程序之间建立双向管道?


63

每个人都知道如何使两个程序(绑定之间的单向管stdout第一个和stdin第二之一): first | second

但是如何制作双向管道,即交叉绑定stdinstdout两个程序?有没有简单的方法可以在Shell中完成?

shell  pipe 

Answers:


30

如果系统上的管道是双向的(至少在Solaris 11和某些BSD上是这样,但在Linux上不是):

cmd1 <&1 | cmd2 >&0

不过要当心死锁。

另请注意,某些系统上的某些版本的ksh93 |使用套接字对实现管道()。套接字对是双向的,但是ksh93明确地关闭了反向连接,因此,即使在管道(由pipe(2)系统调用创建)是双向的系统上,上述命令也不适用于那些ksh93 。


1
有谁知道这是否也适用于Linux?(在此处使用archlinux)
heinrich5991 2012年


41

好吧,使用命名管道(mkfifo)相当“容易” 。我用引号括起来很容易,因为除非程序是为此目的设计的,否则很可能发生死锁。

mkfifo fifo0 fifo1
( prog1 > fifo0 < fifo1 ) &
( prog2 > fifo1 < fifo0 ) &
( exec 30<fifo0 31<fifo1 )      # write can't open until there is a reader
                                # and vice versa if we did it the other way

现在,通常在编写标准输出时涉及缓冲。因此,例如,如果两个程序都是:

#!/usr/bin/perl
use 5.010;
say 1;
print while (<>);

您会期待一个无限循环。但是相反,两者都会陷入僵局。您需要添加$| = 1(或等效功能)以关闭输出缓冲。造成死锁的原因是,两个程序都在stdin上等待,但由于它位于另一个程序的stdout缓冲区中而尚未写入管道,因此他们没有看到它。

更新:结合了StéphaneCharzelas和Joost的建议:

mkfifo fifo0 fifo1
prog1 > fifo0 < fifo1 &
prog2 < fifo0 > fifo1

相同,更短,更便于携带。


23
一个命名管道就足够了:prog1 < fifo | prog2 > fifo
Andrey Vihrov

2
@AndreyVihrov是的,您可以用匿名管道替换其中一个命名管道。但是我喜欢对称性:-P
derobert

3
@ user14284:在Linux上,您可以使用来完成此操作prog1 < fifo | tee /dev/stderr | prog2 | tee /dev/stderr > fifo
Andrey Vihrov

3
如果您做到了prog2 < fifo0 > fifo1,则可以避免与之跳舞exec 30< ...(顺便说一句,它只能与10个bash或以上yash的fds 一起使用)。
斯特凡Chazelas

1
@Joost Hmmm,看来您是正确的,至少在bash中是没有必要的。我可能担心,由于Shell执行重定向(包括打开管道),它可能会阻塞-但至少在打开fifos之前是bash fork。dash似乎也不错(但行为有所不同)
derobert

13

我不确定这是否是您要尝试的操作:

nc -l -p 8096 -c second &
nc -c first 127.0.0.1 8096 &

首先,打开端口8096上的侦听套接字,一旦建立连接,就将secondstdin作为流输出和stdout流输入生成程序。

然后,nc启动第二个程序first,该程序连接到侦听端口,并以其stdout作为流输入并以其stdin作为流输出生成程序。

使用管道并不能完全做到这一点,但是它似乎可以满足您的需求。

由于它使用网络,因此可以在两台远程计算机上完成。这几乎是网络服务器(second)和网络浏览器(first)的工作方式。


1
nc -U对于UNIX域套接字,只有采取文件系统的地址空间。
Ciro Santilli新疆改造中心法轮功六四事件

-c选项在Linux中不可用。我的幸福是短暂的幸福:-(
pablochacin


6

bash版本4的coproc命令允许在bash不使用命名管道的情况下完成此操作:

coproc cmd1
eval "exec cmd2 <&${COPROC[0]} >&${COPROC[1]}"

其他一些壳也可以做coproc

下面是更详细的答案,但链接了三个命令,而不是两个,这使命令变得更加有趣。

如果您也乐于使用catstdbuf则可以使构造更易于理解。

bashcat和一起使用的版本stdbuf,易于理解:

# start pipeline
coproc {
    cmd1 | cmd2 | cmd3
}
# create command to reconnect STDOUT `cmd3` to STDIN of `cmd1`
endcmd="exec stdbuf -i0 -o0 /bin/cat <&${COPROC[0]} >&${COPROC[1]}"
# eval the command.
eval "${endcmd}"

注意,必须使用eval,因为在我的bash 4.2.25版本中,<&$ var中的变量扩展是非法的。

使用pure的版本bash:分为两部分,在coproc下启动第一个管道,然后在第二部分进餐,(将单个命令或管道)重新连接到第一部分:

coproc {
    cmd 1 | cmd2
}
endcmd="exec cmd3 <&${COPROC[0]} >&${COPROC[1]}"
eval "${endcmd}"

概念证明:

文件./prog,只是一个伪编,用于消耗,标记和重新打印行。使用子shell来避免缓冲问题可能会导致过大杀伤力,这不是重点。

#!/bin/bash
let c=0
sleep 2

[ "$1" == "1" ] && ( echo start )

while : ; do
  line=$( head -1 )
  echo "$1:${c} ${line}" 1>&2
  sleep 2
  ( echo "$1:${c} ${line}" )
  let c++
  [ $c -eq 3 ] && exit
done

文件./start_cat 这是一个版本的使用bashcat以及stdbuf

#!/bin/bash

echo starting first cmd>&2

coproc {
  stdbuf -i0 -o0 ./prog 1 \
    | stdbuf -i0 -o0 ./prog 2 \
    | stdbuf -i0 -o0 ./prog 3
}

echo "Delaying remainer" 1>&2
sleep 5
cmd="exec stdbuf -i0 -o0 /bin/cat <&${COPROC[0]} >&${COPROC[1]}"

echo "Running: ${cmd}" >&2
eval "${cmd}"

或文件./start_part。这是bash仅使用纯的版本。出于演示目的,我仍在使用,stdbuf因为您的实际编要无论如何都要在内部处理缓冲,以避免由于缓冲而造成阻塞。

#!/bin/bash

echo starting first cmd>&2

coproc {
  stdbuf -i0 -o0 ./prog 1 \
    | stdbuf -i0 -o0 ./prog 2
}

echo "Delaying remainer" 1>&2
sleep 5
cmd="exec stdbuf -i0 -o0 ./prog 3 <&${COPROC[0]} >&${COPROC[1]}"

echo "Running: ${cmd}" >&2
eval "${cmd}"

输出:

> ~/iolooptest$ ./start_part
starting first cmd
Delaying remainer
2:0 start
Running: exec stdbuf -i0 -o0 ./prog 3 <&63 >&60
3:0 2:0 start
1:0 3:0 2:0 start
2:1 1:0 3:0 2:0 start
3:1 2:1 1:0 3:0 2:0 start
1:1 3:1 2:1 1:0 3:0 2:0 start
2:2 1:1 3:1 2:1 1:0 3:0 2:0 start
3:2 2:2 1:1 3:1 2:1 1:0 3:0 2:0 start
1:2 3:2 2:2 1:1 3:1 2:1 1:0 3:0 2:0 start

做到了。


5

编写此类双向管道的便捷构建模块是将当前进程的stdout和stdin连接在一起的工具。让我们称之为ioloop。调用此函数后,只需要启动常规管道即可:

ioloop &&     # stdout -> stdin 
cmd1 | cmd2   # stdin -> cmd1 -> cmd2 -> stdout (-> back to stdin)

如果您不想修改顶级外壳程序的描述符,请在子外壳程序中运行它:

( ioloop && cmd1 | cmd2 )

这是使用命名管道的ioloop的可移植实现:

ioloop() {
    FIFO=$(mktemp -u /tmp/ioloop_$$_XXXXXX ) &&
    trap "rm -f $FIFO" EXIT &&
    mkfifo $FIFO &&
    ( : <$FIFO & ) &&    # avoid deadlock on opening pipe
    exec >$FIFO <$FIFO
}

命名管道仅在ioloop设置期间短暂存在于文件系统中。此功能不是POSIX,因为不建议使用mktemp(并且可能容易受到种族攻击)。

使用/ proc /的特定于Linux的实现是可能的,不需要命名管道,但是我认为这已经足够了。


有趣的功能,+ 1。可能会使用一个句子或添加2个( : <$FIFO & )更详细的解释。感谢您的发表。
Alex Stragies,2016年

我环顾四周,一片空白。在哪里可以找到有关折旧的信息mktemp?我已经广泛使用它,如果已经有了更新的工具,我想开始使用它。
DopeGhoti '16

亚历克斯(Alex):命名管道上的open(2)系统调用被阻止。如果尝试“ exec <$ PIPE> $ PIPE”,它将陷入等待另一个进程打开另一端的困境。命令“:<$ FIFO&”在后台的子外壳中执行,并使双向重定向成功完成。
user873942 '16

DopeGhoti:不推荐使用mktemp(3)C库函数。mktemp(1)实用程序不是。
user873942 '16

4

也有

正如@StéphaneChazelas在注释中正确指出的那样,以上示例是“基本形式”,他有不错的示例,并在选项中回答了类似的问题


请注意,默认情况下,socat使用套接字而不是管道(可以使用进行更改commtype=pipes)。您可能希望添加该nofork选项,以避免额外的socat进程在管道/插座之间填充数据。(感谢在编辑我的答案 BTW)
斯特凡Chazelas

0

这里有很多很棒的答案。因此,我只想添加一些内容以方便与他们一起玩耍。我认为stderr没有重定向到任何地方。创建两个脚本(假设a.sh和b.sh):

#!/bin/bash
echo "foo" # change to 'bar' in second file

for i in {1..10}; do
  read input
  echo ${input}
  echo ${i} ${0} got: ${input} >&2
done

然后,当您以任何好的方式连接它们时,您应该在控制台上看到:

1 ./a.sh got: bar
1 ./b.sh got: foo
2 ./a.sh got: foo
2 ./b.sh got: bar
3 ./a.sh got: bar
3 ./b.sh got: foo
4 ./a.sh got: foo
4 ./b.sh got: bar
...
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.