如何使用命名管道在进程之间转发?

如何使用命名管道在进程之间转发?

/tmp/in/tmp/out和是命名管道/tmp/err,已由某个进程创建并打开(分别用于读取、写入和写入)。

我想创建一个新进程,将其 stdin 通过管道传输到/tmp/in,并将 的内容写入其 stdout ,并在可用时将/tmp/out其内容写入其 stderr 。/tmp/err一切都应该在一个行缓冲时尚。当创建的另一个进程/tmp/in停止读取并关闭时,该进程应该退出/tmp/in。该解决方案应该在 Ubuntu 上运行,最好不安装任何额外的软件包。我想在 bash 脚本中解决它。


麦克塞夫指出,如果没有SSCCE,很难理解我想要什么。因此,下面是一个 SSCCE,但请记住,这是一个最小的示例,因此它非常愚蠢。

原来的设置

父进程启动子进程并通过子进程的 stdin 和 stdout 逐行与其通信。如果我运行它,我会得到:

$ python parent.py 
Parent writes to child:  a
Response from the child: A

Parent writes to child:  b
Response from the child: B

Parent writes to child:  c
Response from the child: C

Parent writes to child:  d
Response from the child: D

Parent writes to child:  e
Response from the child: E

Waiting for the child to terminate...
Done!
$ 

父级.py

from __future__ import print_function
from subprocess import Popen, PIPE
import os

child = Popen('./child.py', stdin=PIPE, stdout=PIPE)
child_stdin  = os.fdopen(os.dup(child.stdin.fileno()), 'w')
child_stdout = os.fdopen(os.dup(child.stdout.fileno()))

for letter in 'abcde':
    print('Parent writes to child: ', letter)
    child_stdin.write(letter+'\n')
    child_stdin.flush()
    response = child_stdout.readline()
    print('Response from the child:', response)
    assert response.rstrip() == letter.upper(), 'Wrong response'

child_stdin.write('quit\n')
child_stdin.flush()
print('Waiting for the child to terminate...')
child.wait()
print('Done!')

孩子.py,必须是可执行的!

#!/usr/bin/env python
from __future__ import print_function
from sys import stdin, stdout

while True:
    line = stdin.readline()
    if line == 'quit\n':
        quit()
    stdout.write(line.upper())
    stdout.flush()

所需的设置和黑客解决方案

父级源文件和子级源文件均不可编辑;不允许。

我将 child.py 重命名为 child_original.py (并使其可执行)。然后,我放置一个名为 child.py 的 bash 脚本(如果您愿意,可以是代理或中间人),在运行之前自己启动 child_original.pypython parent.py并让 Parent.py 调用假的 child.py,它现在是我的 bash 脚本,在parent.py和child_original.py之间转发。

假孩子.py

#!/bin/bash
parent=$$
cat std_out &
(head -n 1 shutdown; kill -9 $parent) &
cat >>std_in

start_child.sh在执行父进程之前启动 child_original.py :

#!/bin/bash
rm -f  std_in std_out shutdown
mkfifo std_in std_out shutdown
./child_original.py <std_in >std_out
echo >shutdown
sleep 1s
rm -f  std_in std_out shutdown

执行它们的方式:

$ ./start_child.sh & 
[1] 7503
$ python parent.py 
Parent writes to child:  a
Response from the child: A

Parent writes to child:  b
Response from the child: B

Parent writes to child:  c
Response from the child: C

Parent writes to child:  d
Response from the child: D

Parent writes to child:  e
Response from the child: E

Waiting for the child to terminate...
Done!
$ echo 

[1]+  Done                    ./start_child.sh
$ 

这个黑客解决方案有效。据我所知,它不满足行缓冲要求,并且有一个额外的关闭fifo来通知start_child.sh child_original.py已关闭管道并且start_child.sh可以安全退出。


该问题要求改进假child.py bash脚本,满足要求(行缓冲,当child_original.py关闭任何管道时退出,不需要额外的关闭管道)。



我希望我知道的事情:

  • 如果使用高级 API 将 fifo 作为文件打开,则必须将其打开阅读和写作,否则对的调用open已经阻塞。这是非常违反直觉的。也可以看看为什么命名管道的只读打开会阻塞?
  • 实际上,我的父进程是一个 Java 应用程序。如果您使用 Java 的外部进程,请从以下位置读取外部进程的 stdout 和 stderr守护进程线程(调用setDamon(true)这些线程启动它们)。否则,即使每个人都完成了,JVM 也会永远挂起。尽管与问题无关,但其他陷阱包括:绕过与 Runtime.exec() 方法相关的陷阱
  • 显然,无缓冲意味着有缓冲,但我们不会等到缓冲区满了,而是尽快刷新它。

答案1

如果你摆脱了杀戮和关闭的东西(这是不安全的,在极端但并非深不可测的情况下,你可能会在子shell最终结束一些无辜的进程child.py之前死亡 ),那么就不会终止,因为你不是表现得不像一个好的 UNIX 公民。(head -n 1 shutdown; kill -9 $parent) &kill -9child.pyparent.py

当您发送消息时,子进程cat std_out &将完成quit,因为写入者是std_outchild_original.py它在接收消息时完成,quit此时关闭其stdout,这是std_out管道,这close将使cat子进程完成。

尚未cat > std_in完成,因为它正在从源自该进程的管道中读取数据parent.py,并且该parent.py进程没有费心关闭该管道。如果确实如此,cat > stdin_in那么整个过程child.py将自行完成,并且您不需要关闭管道或killing部分(如果由于快速引起的竞争条件,在 UNIX 上杀死不是您子进程的进程始终是一个潜在的安全漏洞)应发生 PID 回收)。

管道右端的进程通常只有在读取完标准输入后才会完成,但由于您没有关闭该 ( child.stdin),因此您隐式地告诉子进程“等等,我有更多输入给您”并且然后你就可以杀死它,因为它确实会等待你的更多输入。

简而言之,让parent.py行为合理:

from __future__ import print_function
from subprocess import Popen, PIPE
import os

child = Popen('./child.py', stdin=PIPE, stdout=PIPE)

for letter in 'abcde':
    print('Parent writes to child: ', letter)
    child.stdin.write(letter+'\n')
    child.stdin.flush()
    response = child.stdout.readline()
    print('Response from the child:', response)
    assert response.rstrip() == letter.upper(), 'Wrong response'

child.stdin.write('quit\n')
child.stdin.flush()
child.stdin.close()
print('Waiting for the child to terminate...')
child.wait()
print('Done!')

child.py可以像这样简单

#!/bin/sh
cat std_out &
cat > std_in
wait #basically to assert that cat std_out has finished at this point

(请注意,我删除了 fd dup 调用,因为否则您需要关闭两者child.stdinchild_stdin重复项)。

由于parent.pygnu 以面向行的方式运行,因此 gnucat是无缓冲的(正如 mikeserv 指出的那样)并且child_original.py以面向行的方式运行,因此您实际上已经得到了整个行缓冲。


关于猫的注意事项:无缓冲可能不是最幸运的术语,因为 gnucat确实使用缓冲区。它不会做的是在写出内容之前尝试使整个缓冲区充满(与 stdio 不同)。基本上,它向操作系统发出特定大小(其缓冲区大小)的读取请求,并写入收到的任何内容,而无需等待获取整行或整个缓冲区。 (阅读(2)可能会很懒,只给你当前可以给你的东西,而不是你所要求的整个缓冲区。)

(您可以在以下位置检查源代码http://git.savannah.gnu.org/cgit/coreutils.git/tree/src/cat.c; safe_read(使用而不是 plain read)位于gnulib子模块中,它是一个非常简单的包装器阅读(2)抽象掉EINTR(参见手册页))。

答案2

使用 a 时,sed输入将始终在行缓冲区中读入,并且可以使用该w命令显式地刷新每行输出。例如:

(       cd /tmp; c=
        mkfifo i o
        dd  bs=1    <o&
        sed -n w\ o <i&
        while   sleep 1
        do      [ -z "$c" ] && rm [io]
                [ "$c" = 5 ]   && exit
                date "+%S:%t$((c+=1))"
        done|   tee i
)

44: 1
44: 1
45: 2
45: 2
46: 3
46: 3
47: 4
47: 4
48: 5
48: 5
30+0 records in
30+0 records out
30 bytes (30 B) copied, 6.15077 s, 0.0 kB/s

...在哪里tee (这是指定不阻塞缓冲区同时将其输出写入终端和sed管道i。逐行读取并sed立即将读取的每一行写入其ut 管道。一次读取ut 管道一个字节,并且它与 共享一个标准输出,因此它们同时将输出写入终端。如果没有明确的行缓冲,这种情况就不会发生。这是相同的运行,但没有rite 命令:iwoddoteesedw

(       cd /tmp; c=
        mkfifo i o
        dd  bs=1    <o&
        sed ''  >o  <i&
        while   sleep 1
        do      [ -z "$c" ] && rm [io]
                [ "$c" = 5 ]   && exit
                date "+%S:%t$((c+=1))"
        done|   tee i
)

48: 1
49: 2
50: 3
51: 4
52: 5
48: 1
49: 2
50: 3
51: 4
52: 5
30+0 records in
30+0 records out
30 bytes (30 B) copied, 6.15348 s, 0.0 kB/s

在这种情况下sed,块缓冲区不会写入任何内容,dd直到其输入关闭,此时它会刷新其输出并退出。事实上,当它的编写者在这两种情况下退出时,它就会退出,正如在dd管道末尾写入的诊断中所见证的那样。

尽管如此...

(       cd /tmp; c=
        mkfifo i o
        dd  bs=1    <o&
        cat >o      <i&
        while   sleep 1
        do      [ -z "$c" ] && rm [io]
                [ "$c" = 5 ]   && exit
                date "+%S:%t$((c+=1))"
        done|   tee i
)

40: 1
40: 1
41: 2
41: 2
42: 3
42: 3
43: 4
43: 4
44: 5
44: 5
30+0 records in
30+0 records out
30 bytes (30 B) copied, 6.14734 s, 0.0 kB/s

现在我的cat是 GNU 版本 - GNU 的cat (如果没有选项调用)从不块缓冲区。如果您也使用 GNU,cat那么对我来说很明显问题不在于您的中继,而在于您的 Java 程序。但是,如果您是不是使用 GNU cat,那么它有可能缓冲输出。不过,你很幸运,只有一个POSIX 规范选项cat,这适用于-unbuffered 输出。你可以尝试一下。

我正在查看你的东西,在玩了一段时间后,我很确定你的问题是一个僵局。cat最后,你的输入挂在了那里,如果 JVM 进程也在等待有人与之对话,那么可能什么也不会发生。所以我写了这个:

#!/bin/sh
die()   for io  in  i o e
        do      rm "$io"
                kill -9 "$(($io))"
        done    2>/dev/null
io()    while   eval "exec $((fd+=1))>&-"
        do      [ "$fd" = 9 ] &&
                { cat; kill -1 0; }
        done
cd /tmp; fd=1
mkfifo   i o e
{   io <o >&4 & o=$!
    io <e >&5 & e=$!
    io >i <&3 & i=$!
}   3<&0  4>&1  5>&2
trap "die; exit 0" 1
echo; wait

不幸的是,处理返回码有点草率。不过,通过更多的工作,可以使其可靠地做到这一点。无论如何,正如你所看到的背景全部cat我认为,这应该会引发wait一条cat链,在所有情况下都会杀死所有这些。

答案3

在 bash 中,你可以尝试:

forward() { while read line; do echo "$line"; done; } 
forward </tmp/out & 
forward </tmp/err >&2 &
forward >/tmp/in
wait

然后使用 运行脚本stdbuf -i0 -oL

forward函数基本上是pipepython 代码中的方法,其中 src 和 dest 默认为 stdin 和 stdout,并且没有显式刷新,希望stdbuf可以做到这一点。

如果您已经关心性能并希望将其放在 C 代码中,请将其放在 C 代码中。我不熟悉 stdbuf 友好的cat替代方案,但这里有一个 C++ 单行(几乎)用于catting :stdinstdout

#include <iostream>
using namespace std;
int main() { for(string line; getline(cin,line); ){ cout<<line<<'\n'; }; }

或者,如果您绝对必须有 C 代码:

#include <stdlib.h>
#include <stdio.h>
int main()
{
  const size_t N = 80;
  char *lineptr = (char*)malloc(N); //this will get realloced if a longer line is encountered
  size_t length = N;
  while(getline(&lineptr, &length, stdin) != -1){
    fputs(lineptr, stdout);
  }
  free(lineptr);
  return 0;
}

C 和 C++ 示例都没有在每行之后进行显式刷新,因为我认为将其保留为stdbuf更好的设计决策,但您始终可以通过fflush(stdout)在每行之后调用 C 示例来添加它,在 C++ 示例中替换'\n'endl,或者通过在这两种情况下将缓冲预先设置为行缓冲,可以更有效地实现,这样您就不必进行那些“昂贵的”C/C++ 函数调用。

相关内容