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

Ali*_*Ali 5 shell pipe buffer fifo

/tmp/in/tmp/out并且/tmp/err已经创建的命名管道,以及一些进程打开(分别用于阅读,写作和写作)。

我想创建一个新进程,将其 stdin 导入/tmp/in,并将 的内容写入/tmp/out其标准输出,并将 的内容/tmp/err写入其标准错误。一切都应该以行缓冲的方式工作。当创建的另一个进程/tmp/in停止读取并关闭时,该进程应该退出/tmp/in。该解决方案应该适用于 Ubuntu,最好不安装任何额外的软件包。我想在 bash 脚本中解决它。


Mikeserv指出,没有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!
$ 
Run Code Online (Sandbox Code Playgroud)

父母.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!')
Run Code Online (Sandbox Code Playgroud)

child.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()
Run Code Online (Sandbox Code Playgroud)

所需的设置和骇人听闻的解决方案

父源文件和子源文件都不能编辑;不允许。

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

假孩子.py

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

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
Run Code Online (Sandbox Code Playgroud)

执行它们的方式:

$ ./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
$ 
Run Code Online (Sandbox Code Playgroud)

这个骇人听闻的解决方案有效。据我所知,它不符合行缓冲要求,并且有一个额外的关闭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() 方法相关的陷阱
  • 显然,无缓冲意味着缓冲,但我们不会等到缓冲区满了,而是尽快刷新它。

PSk*_*cik 3

如果你摆脱了杀戮和关闭的东西(这是不安全的,在极端但并非深不可测的情况下,你可能会在子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!')
Run Code Online (Sandbox Code Playgroud)

child.py可以像这样简单

#!/bin/sh
cat std_out &
cat > std_in
wait #basically to assert that cat std_out has finished at this point
Run Code Online (Sandbox Code Playgroud)

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

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


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

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