最近はやたらとCが楽しいです。
(関係ないけど「LINUXシステムプログラミング」っていう本がとても面白かったです。OSがなにをしてくれているかがよく分かりました)
色々やってみたいことがあるけど、まずはプロセス間通信としてpipe
を使ったやつをごにょごにょしてみました。
ちなみにこちらの記事を参考にさせていただきました。
やったこと
とりあえずpipe
を作って、もうひとつ作ったプログラムと通信する、というごくシンプルなもの。
でも標準入力とか標準出力とかを再設定したり、色々やりたいことに近づけた感じ。
標準入力から読み込んで、標準出力に書き出すだけの応答サンプル
pipe
の前に、まずは応答するだけのサンプルを作る。以下の感じ。
#include <stdio.h>
#include <unistd.h>
int main() {
const int BUF_LEN = 255;
char buf[BUF_LEN];
int rc;
rc = read(STDIN_FILENO, buf, BUF_LEN);
if (rc == -1) {
perror("read");
return 1;
}
printf("test1: ");
fwrite(buf, 1, rc, stdout);
printf("\n");
return 0;
}
やっていることは単純で、標準入力から得られた文字列をただ標準出力に出しているだけ。
これ単体を実行すると、キーボードでなにか入力してエンターキー押すとそれがエコーする。
pipe
を使うサンプルを作る
続いて、上記サンプルとpipe
で通信するサンプルを作る。
pipe
のサンプル自体は冒頭の記事のものをほぼそのまま使わせてもらっています。
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#define READ (0)
#define WRITE (1)
/**
* @param *fd_r 読み込み用ファイルディスクリプタ
* @param *fd_w 書き込み用ファイルディスクリプタ
*/
int popen2(int *fd_r, int *fd_w) {
// 子から親への通信用パイプ
int pipe_child2parent[2];
// 親から子への通信用パイプ
int pipe_parent2child[2];
// プロセスID
int pid;
// パイプを生成
if (pipe(pipe_child2parent) < 0) {
// パイプ生成失敗
perror("popen2");
return 1;
}
// パイプを生成
if (pipe(pipe_parent2child) < 0) {
// パイプ生成失敗
perror("popen2");
// 上で開いたパイプを閉じてから終了
close(pipe_child2parent[READ]);
close(pipe_child2parent[WRITE]);
return 1;
}
// fork
if ((pid = fork()) < 0) {
// fork失敗
perror("popen2");
// 開いたパイプを閉じる
close(pipe_child2parent[READ]);
close(pipe_child2parent[WRITE]);
close(pipe_parent2child[READ]);
close(pipe_parent2child[WRITE]);
return 1;
}
// 子プロセスか?
if (pid == 0) {
// 子プロセスの場合は、親→子への書き込みはありえないのでcloseする
close(pipe_parent2child[WRITE]);
// 子プロセスの場合は、子→親の読み込みはありえないのでcloseする
close(pipe_child2parent[READ]);
// 親→子への出力を標準入力として割り当て
dup2(pipe_parent2child[READ], 0);
// 子→親への入力を標準出力に割り当て
dup2(pipe_child2parent[WRITE], 1);
// 割り当てたファイルディスクリプタは閉じる
close(pipe_parent2child[READ]);
close(pipe_child2parent[WRITE]);
// 子プロセスはここで該当プログラムを起動しリターンしない
if (execl("./test1", "./test1", NULL) < 0) {
perror("popen2");
close(pipe_parent2child[READ]);
close(pipe_child2parent[WRITE]);
return 1;
}
}
// 親プロセス側の処理
close(pipe_parent2child[READ]);
close(pipe_child2parent[WRITE]);
*fd_r = pipe_child2parent[READ];
*fd_w = pipe_parent2child[WRITE];
return pid;
}
/////////////////////////////////////////////////////////////////////////////
int main(int argc, char *argv[]) {
int fd_r = fileno(stdin);
int fd_w = fileno(stdout);
if (argc < 2) {
printf("Usage: %s <message>\n", argv[0]);
return 1;
}
popen2(&fd_r, &fd_w);
write(fd_w, argv[1], strlen(argv[1]));
char buf[255];
int size = read(fd_r, buf, 255);
if (size == -1) {
perror("error");
return 1;
}
printf("test2:");
fwrite(buf, 1, size, stdout);
printf("\n");
return 0;
}
pipe()
は、引数に渡されたint
型の配列に、それぞれ読み込み用と書き込み用のファイルディスクリプタが渡されます。
そしてそれぞれ、親→子、子→親用のパイプを作ります。(pipe_parent2child
、pipe_child2parent
)
パイプを作ったらfork()
で子プロセスを生成します。
親/子プロセスで適切にパイプを閉じる
例えば、親→子へデータを渡すには親が子のファイルディスクリプタにデータを書き込みます。
つまり、読み込みは必要ありません。(子から親にデータを渡す場合は、逆に子が親のファイルディスクリプタに書き込みます)
これは親/子で必要なパイプが逆転することを意味します。
なので、それぞれのプロセスごとに利用する分だけを残し、パイプを閉じています。
子プロセスの標準入出力を置き換える
続いて、dup2
システムコールを使って、子プロセスの標準入出力を、今生成したパイプで置き換えます。
// 親→子への出力を標準入力として割り当て
dup2(pipe_parent2child[READ], 0);
// 子→親への入力を標準出力に割り当て
dup2(pipe_child2parent[WRITE], 1);
dup2()
システムコールは、第一引数のファイルディスクリプタで、第二引数のファイルディスクリプタを上書きします。
つまり、上記の例では親から子へ書き込まれたデータを読み込むファイルディスクリプタを、標準入力に、逆に子から親にデータを書き込むファイルディスクリプタを標準出力に設定しています。
子プロセスでプログラムを起動する
execl()
システムコールは、第一引数に与えられたプログラムを起動します。
続く第二引数以降が、そのプログラムに渡されるコマンドライン引数となり、NULL
で終端します。(execlのl
はListのl
です)
(第二引数の最初の引数は、プログラム名自身を渡すようです。これは慣例?)
execl()
を実行すると、エラーでなければそのプログラムが起動され、それ以降の文は実行されません。(つまりリターンしない)
親プロセスから読み書きする
最後は、それぞれ設定したパイプを使って親プロセスからデータを書き込み、また読み込むことで通信が可能になります。
write(fd_w, argv[1], strlen(argv[1]));
char buf[255];
int size = read(fd_r, buf, 255);
if (size == -1) {
perror("error");
return 1;
}
printf("test2: %s\n", buf);
実行すると以下の結果になります。
$ ./test2 hoge
test2: test1: hoge
test1: hoge
が、./test1
のプログラムが標準出力(つまり現在はパイプ)に書きだしたデータです。そしてそれを、親プロセス側でtest2:
を付けて出力した、というわけです。