web-dev-qa-db-ja.com

名前付きパイプを使用してプロセス間を転送するにはどうすればよいですか?

_/tmp/in_、_/tmp/out_、および_/tmp/err_は名前付きパイプであり、何らかのプロセス(それぞれ読み取り、書き込み、書き込み用)によって既に作成および開かれています。

Stdinを_/tmp/in_にパイプし、_/tmp/out_の内容をstdoutに書き込み、_/tmp/err_の内容が利用可能になったときにstderrに書き込む新しいプロセスを作成したいと思います。すべてが行バッファーの方法で機能するはずです。プロセスは、_/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!
$ 
_

parent.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!')
_

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()
_

必要なセットアップとハックの解決策

親のソースファイルも子のソースファイルも編集できません。それは許可されていません。

Child.pyの名前をchild_original.pyに変更します(そして実行可能にします)。次に、child.pyというbashスクリプト(必要に応じてプロキシまたは仲介者)を配置し、_python parent.py_を実行する前に自分でchild_original.pyを起動し、parent.pyに偽のchild.pyを呼び出させます。これで私のbashスクリプトは、parent.pyとchild_original.pyの間で転送されます。

偽のchild.py

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

親を実行する前にchild_original.pyを開始する_start_child.sh_:

_#!/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
$ 
_

このハック的なソリューションは機能します。私の知る限り、これはラインバッファの要件を満たしておらず、child_original.pyがパイプを閉じてstart_child.shが安全に終了できることをstart_child.shに通知するための追加のシャットダウンFIFOがあります。


質問は、要件を満たす、改善された偽のchild.py bashスクリプトを要求します(行がバッファリングされ、child_original.pyがパイプのいずれかを閉じると終了し、追加のシャットダウンパイプは必要ありません)。



私が知っていたらよかったこと:

  • Fifoをファイルとして開くために高水準APIを使用する場合は、読み取りと書き込みの両方で開く必要があります。そうでない場合、openの呼び出しはすでにブロックされます。これは信じられないほど直感に反します。 名前付きパイプブロックを読み取り専用で開くのはなぜですか? も参照してください。
  • 実際には、私の親プロセスはJavaアプリケーションです。Javaの外部プロセスを使用する場合は、外部プロセスのstdoutとstderrをdaemonスレッドから読み取ります(それらのスレッドでsetDamon(true)を呼び出しますbeforeそれらを開始します)。そうしないと、全員が完了したとしても、JVMは永久にハングします。質問とは関係ありませんが、他の落とし穴は次のとおりです。 Runtime.exec()メソッド に関連する落とし穴を回避します。
  • どうやら、バッファリングされていないということはバッファリングされていることを意味しますが、バッファがいっぱいになるまで待たずに、できるだけ早くフラッシュします。
5
Ali

殺害やシャットダウンを取り除く場合(これは安全ではなく、極端な場合、child.py(head -n 1 shutdown; kill -9 $parent) &サブシェルの前に死ぬとkill -9が発生する可能性があります。無実のプロセス)の場合、child.pyはUNIX市民のように振る舞っていないため、parent.pyは終了しません。

cat std_out &サブプロセスはquitメッセージを送信するまでに終了します。これは、std_outへのライターがchild_original.pyであり、quitを受信すると終了し、その時点でstdoutを閉じます。 std_outパイプとそのcloseにより、catサブプロセスが終了します。

cat > std_inは、parent.pyプロセスで発生したパイプから読み取っていて、parent.pyプロセスがわざわざそのパイプを閉じなかったため、終了していません。もしそうなら、cat > stdin_in、したがってchild.py全体がそれ自体で終了し、シャットダウンパイプやkilling部分は必要ありません(UNIXであなたの子供ではないプロセスを殺すことは常に迅速なPIDリサイクルによって引き起こされた競合状態が発生した場合の潜在的なセキュリティホール)。

パイプラインの右端のプロセスは、通常、stdinの読み取りが完了した後にのみ終了しますが、それを閉じていないため(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.pyは行指向で動作するため、gnu catはバッファリングされておらず(mikeservが指摘しているように)、child_original.pyは行指向で動作するため、事実上すべてが行バッファ化されます。


Catに関する注記:gnu catはバッファーを使用するため、バッファーなしは最も幸運な用語ではない可能性があります。それがしないのは、(stdioとは異なり)書き出す前にバッファ全体をいっぱいにしようとすることです。基本的には、特定のサイズ(バッファサイズ)のOSに対して読み取り要求を行い、行全体またはバッファ全体を取得するのを待たずに、受信したものをすべて書き込みます。 ( read(2) 怠惰になる可能性があり、要求したバッファー全体ではなく、現時点で提供できるものだけを提供します。)

(ソースコードは http://git.savannah.gnu.org/cgit/coreutils.git/tree/src/cat.c ; safe_read(の代わりに使用)で調べることができます。プレーンなread)はgnulibサブモジュールにあり、 read(2) の非常に単純なラッパーであり、EINTRを抽象化します(manページを参照))。

3
PSkocik

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(つまり バッファをブロックしないように指定はその出力をに書き込みますターミナルとsediパイプに同時に接続します。 sediを行ごとに読み取り、writesは、読み取る各行をoutパイプに送信します。 ddoutパイプを一度に1バイト読み取り、teeとstdoutを共有するため、両方が同時に出力を端末に書き込みます。 sedが明示的にラインバッファを行わなかった場合、これは発生しません。これは同じ実行ですが、writeコマンドはありません。

(       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(オプションなしで呼び出された場合)neverブロックバッファ。GNU catも使用している場合、問題はリレーではなく、=にあることは明らかです。 Javaプログラム。ただし、notの場合は、GNU catの場合、出力をバッファリングする可能性があります。幸運なことに、たった1つしかありません POSIX仕様のcat オプション、それは-unbuffered出力。あなたはそれを試すかもしれません。

私はあなたのことを見ていて、しばらくそれをいじった後、あなたの問題はデッドロック状態であると確信しています。最後に1つの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

残念ながら、リターンコードの処理についてはちょっと雑です。しかし、より多くの作業を行うことで、確実にそうすることができます。とにかく、ご覧のとおり、catsの背景allは、stdoutに空の行を書き込み、次にwaits catsの1つが終了するまで、すべての場合にそれらすべてを殺すチェーンを開始する必要があると思います。

2
mikeserv

Bashでは、次のことを試すことができます。

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

次に、_stdbuf -i0 -oL_を使用してスクリプトを実行します。

forward関数は、基本的に、python srcおよびdestがデフォルトでstdinおよびstdoutになり、明示的なフラッシュがないコードからのpipeメソッドであり、stdbufそれをするかもしれません。

すでにパフォーマンスが心配で、Cコードにしたい場合は、Cコードに入れてください。私はstdbufに適したcatの代替手段に精通していませんが、catting stdinからstdoutへのC++ワンライナー(ほぼ)は次のとおりです。

_#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に任せる方が設計上の決定として優れていると思いますが、Cの例でfflush(stdout)を呼び出すことでいつでも追加できます。各行の後で、C++の例では_'\n'_をendlに置き換えるか、どちらの場合もバッファリングを行バッファリングに事前設定して、これらを「高価な」Cにする必要がないようにします。/C++関数呼び出し。

0
PSkocik