UNIXプログラミング入門~プロセス編(3/4)~

c/c++でunix系osプログラミングを行うときに使うunistd.hやその周辺のライブラリのお話その5です。
このメモでは、プロセス周りのシステムコールAPIに関して取り上げます。

プロセス間通信

プロセスは資源を共有していないので、プロセス間でデータをやりとりするために少し特殊な方法が必要です。

ここでは、パイプ(単方向通信チャンネル)を利用した方法を紹介します。

unistd.hに宣言されているpipe(2)関数を利用します。

pipe(2)関数はunistd.hに以下のように宣言されています。

int pipe(int fildes[2]);

第一引数にintの配列を渡します。
配列の0番目に読み出し用の、配列の1番目に書き込み用のファイルディスクリプタが渡されます。

戻り値はパイプの作成に成功した時に0、失敗した時に-1が返されます。

一般的に以下のように利用されます。

#include <unistd.h>
#include <string.h>
#include <stdio.h>

#define BUFLEN 128
#define READ 0
#define WRITE 1

int main(void)
{
    int fd[2];
    pipe(fd);
    
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork failed");
        return -1;
    }
    else if (pid > 0)
    {
    // 親プロセス
        char str[] = "Hello World!";
        close(fd[READ]);//使わない方のファイルディスクリプタは閉じておく
        write(fd[WRITE], str, strlen(str));
    }
    else
    {
    //子プロセス
        char buf[BUFLEN];
        close(fd[WRITE]);//使わない方のファイルディスクリプタは閉じておく
        read(fd[READ], buf, BUFLEN);
        printf("buf = %s\n", buf);
    }
    return 0;
}

この例では、親から子への一方向のみの通信です。

親から子への通信では、親が子のデータを読み込むことは無いので親側の読み込み用のファイルディスクリプタを閉じます。
また、子が親側に書き込むことも無いので、子側では書き込み用のファイルディスクリプタを閉じます。

特に、書き込み用のファイルディスクリプタは閉じておかないとEOF(ファイル終端)の判定ができないので要注意です。

標準入出力につなぐ-dup2(2)-

上記の方法では、forkを利用してプロセスを分けたアプリの間でしかプロセス間の通信ができません。

パイプを標準入出力につなぐ事で、別のアプリとの通信を実現できるようになります。

標準入力、標準出力、標準エラー出力のファイルディスクリプタはそれぞれ、0, 1, 2です。

パイプを標準入出力につなぐにはdup2関数を利用します。
dup2unistd.hに以下のように宣言されています。

int dup2(int fildes, int fildes2);

第一引数には、元のファイルディスクリプタを
第二引数には、元のファイルディスクリプタとして振舞わせたいファイルディスクリプタを渡します。

いきなりパイプと組み合わせると難しいと思うので、まずは、実在するファイルを標準出力として振舞わせる例を挙げます。

#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>

int main(void)
{
    int fd = open("./open.txt", O_WRONLY|O_CREAT|O_TRUNC);
    if(fd < 0){
        perror("open failed\n");
        return -1;
    }
    close(STDOUT_FILENO);
    dup2(fd, STDOUT_FILENO);//この時点でファイルディスクリプタの`0`は、"./open.txt"を示すようになる。

    printf("printf\n");//これはファイルに出力される。

    return 0;
}

ファイルディスクリプタを複製する時は事前に元あったファイルディスクリプタを閉じる必要があるので注意してください。

同じ要領で今度は、パイプと組み合わせます。

#include <unistd.h>
#include <stdio.h>
#include <string.h>

#define READ 0
#define WRITE 1

int main(void)
{
    int fd[2];
    pipe(fd);

    pid_t pid = fork();
    if (pid < 0)
    {
        perror("fork failed\n");
        return -1;
    }
    else if (pid > 0)
    {
        //親プロセス
        close(fd[READ]);
        
        close(STDOUT_FILENO);
        dup2(fd[WRITE], STDOUT_FILENO);// この時点でファイルディスクリプタの`1`はパイプの書き込み側を示すようになる

        char* cmd[] = {"ps", NULL};
        execvp(cmd[0], cmd);
    }
    else
    {
        //子プロセス
        close(fd[WRITE]);

        close(STDIN_FILENO);
        dup2(fd[READ], STDIN_FILENO);// この時点でファイルディスクリプタの`0`はパイプの読み込み側を示すようになる

        char* cmd[] = {"cat", "-b", NULL};
        execvp(cmd[0], cmd);
    }
    return 0;
}

これを実行すると、ps | cat -bと同じ結果が得られます。

※簡略化のため一部例外処理等を省略しています。
そのため実用上は問題のあるコードになっています。

次回は、以前作った自作bash風プログラムに、リダイレクトやパイプの機能をつけてみようと思います。