パイプによるプロセス間通信 [Linux]

起動されるプロセス

起動されるプロセスは次の通り、何の変哲もないプログラムです。ポイントは fread を使って stdin から READWRITE_BYTE (16) バイトだけ読み取り、その後 fwrite を使って stdout へ READWRITE_BYTE (16) バイトだけ書き出しているところです。このプログラムをここでは stdiotest.exe としてビルドしました。

#include <stdio.h>
#include <stdlib.h>
#include <memory.h>

#define READWRITE_BYTE    (16)

int main ( int argc , char* argv[] ) {

    int nTotalReadByte = 0;
    int nReadByte = 0;
    int nTotalWriteByte = 0;
    int nWriteByte = 0;
   
    char* p = NULL;
    char* pBuffer = NULL;

    if ( 1 != sizeof(char) ) abort();
  
    pBuffer = (char*) malloc ( READWRITE_BYTE );

    if ( !pBuffer ) {
        ...
    }

    p = pBuffer;
   
    do {
        nReadByte = fread ( p, 1, READWRITE_BYTE - nTotalReadByte, stdin );

        p += nReadByte;
        nTotalReadByte += nReadByte;
    } while ( nTotalReadByte < READWRITE_BYTE && nReadByte );

    p = pBuffer;

    do {
        nWriteByte = fwrite ( p, 1, READWRITE_BYTE - nTotalWriteByte, stdout );

        p += nWriteByte;
        nTotalWriteByte += nWriteByte;
    } while ( nTotalWriteByte < READWRITE_BYTE & nWriteByte );

    free ( pBuffer );
    exit ( EXIT_SUCCESS );
}

この資料ではこの stdiotest.exe を新しく起動し、データをやり取りするプログラムを作成します。プロセス間のデータ通信にパイプを使うというわけです。

起動側プロセス

ここで使う知識は次の通り:

  • 「fork でプロセスを作成すると、新しく作成されたプロセスでは、元のプロセスで開いていたファイルディスクリプタをそのまま使える」 
  • 「exec で起動したプロセスでも、元のプロセスで開いていたファイルディスクリプタが使える (fcntl 関数で Close-on-Exec (FD_CLOEXEC) フラグを立てない限り)」
  • 「標準入出力のファイルディスクリプタは 0 (入力), 1(出力) と決まっている」
  • 「dup 関数は閉じているファイルディスクリプタの内でもっとも小さい番号のディスクリプタに、指定したファイルディスクリプタを複製する」 
  • 「fork は一度の呼び出しで二度返る。ひとつは親プロセス(戻り値は子プロセスID)内で返り、もうひとつは複製されたプロセス内で返る。子プロセスでは戻り値は 0。」

以上を予備知識として、次の状況を設定することを試みます。

青い太線と黄土色の太線がパイプで す。それぞれ、矢印の向きにデータフローがあるとします。番号の書いてあるスロットはファイルディスクリプタを 表します。プロセス2では、stdin と stdout のファイルディスクリプタが、それぞれプロセス 1 のパイプ p1[0] と p2[1] に接続しています。この 状態が作られれば、プロセス2 から exec を使って目的のプロセスを起動することにより、プロセス 1 と目的のプロセス間でパイプを使ってデータをやり取りすることができます。では、この状況をどのように作ればよいでしょうか。 順番に図解します。

まず、プロセス1 で pipe 関数を二度呼び出してから fork します。すると下図のようにパイプでつながります。プロセス1, 2 がそれぞれ親プロセス、子プロセスです。

ここでは、プロセス1 の pipes1[1] からデータを書き込み、pipes2[0] からデータを読み取ることを考えていますから、不必要な pipes1[0] と pipes2[1] を close してしまいます。すると、下図のようになります。[X] は閉じており、利用不可能であることを示します。

続けて、プロセス2 (子プロセス) でファイルディスクリプタ 0 と 1 を close します。0, 1 はそれぞれ、標準入力、標準出力をあらわします。

この状態でプロセス 2 で dup (pipes1[0])  を呼び出すと、 dup は番号の若いディスクリプタを使いますから 0 (stdin) に pipes1[0] が複製されます。

同様にプロセス2 で dup (pipes2[1]) を呼び出すと、1 (stdout) に pipes2[1] が複製されます。

最後に、プロセス 2 において、次のように不要なパイプをクローズします。

これで当初の目的が達成できました。この状態を設定してから、プロセス2 で exec を使ってプロセスを起動すればそのプロセスの stdin, stdout 呼び出しはプロセス1 の pipes1[1], pipes2[0] とそれぞれつながっていることになります。

上記をそのままコードにすると、下記のリストを得ます。

#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

void PrintHexDump ( int length, char* buffer ) ;

#define PIPE_READ (0)
#define PIPE_WRITE (1)

#define MESSAGE ("Hello, Waikiki!!")

int main ( int argc, char* argv[] ) {

    int nRet;
    int pipes1[2];
    int pipes2[2];
    char buffer [16];
    int nRead = 0;
    int child_stat = 0;
    pid_t childpid = 0;

    nRet = pipe ( pipes1 );

    if ( EXIT_SUCCESS != nRet ) {
        printf ("pipe (1) failed\n" );
        exit ( EXIT_FAILURE );
    }

    nRet = pipe ( pipes2 );

    if ( EXIT_SUCCESS != nRet ) {
        printf ( "pipe (2) failed.\n");
        exit ( EXIT_FAILURE );
    }

    printf ( "[%d] pipe (1) -> Read: %d, Write: %d\n", (int) getpid(), pipes1[PIPE_READ], pipes1[PIPE_WRITE] );
    printf ( "[%d] pipe (2) -> Read: %d, Write: %d\n", (int) getpid(), pipes2[PIPE_READ], pipes2[PIPE_WRITE] );

    nRet = fork ();

    if ( -1 == nRet ) {

        printf ( "[%d] fork() failed.\n", (int) getpid() );
        exit ( EXIT_FAILURE );
       
    }
    else if ( 0 == nRet ) { // Child Process

        printf ( "[%d] CHILD\n", (int) getpid() );

        close ( 0 );
        close ( 1 );

        dup ( pipes1 [0] ); // stdin
        dup ( pipes2 [1] ); // stdout

        close ( pipes1 [0] );
        close ( pipes1 [1] );
        close ( pipes2 [0] );
        close ( pipes2 [1] );

        execl ( "./stdiotest.exe", NULL );

        exit ( EXIT_SUCCESS );

    }
    else { // Parent Process

        close ( pipes1[0] );
        close ( pipes2[1] );

        write ( pipes1 [1], MESSAGE, strlen (MESSAGE) );

        nRead = read ( pipes2 [0], buffer, sizeof ( buffer ) );

        PrintHexDump ( nRead, buffer );

        childpid = wait ( &child_stat );

        printf ( "[%d] Child has finished. PID=%d\n", (int) getpid(), childpid);       
       
    }

    exit ( EXIT_SUCCESS );

}

実行結果は次の通り。

$ gcc ptest.cpp -o ptest.exe

$ ./ptest.exe
[2860] pipe (1) -> Read: 3, Write: 4
[2860] pipe (2) -> Read: 5, Write: 6
[3444] CHILD
[2860] 0000  48 65 6c 6c 6f 2c 20 57:61 69 6b 69 6b 69 21 21  Hello, Waikiki!!
[2860] Child has finished. PID=3444

補足 (Win32 の場合)

ち なみに、Win32 の場合は CreatePipe でパイプの作成、DuplicateHandle で複製を行います。もっとも流儀?やり方が違うと感じたのは、標準入出力のハンドルの渡し方です。Win32 の場合はStandard I/O のハンドルは、CreateProcess のパラメータとして渡します。STARTUPINFO 構造体の hStdInput/hStdOutput にパイプのハンドルを渡せば OK です (このときフラグに STARTF_USESTDHANDLES を設定します)。

ここまでお読みいただき、誠にありがとうございます。SNS 等でこの記事をシェアしていただけますと、大変励みになります。どうぞよろしくお願いします。

© 2024 Web/DB プログラミング徹底解説