last-modified: 2024-02-07 00:15:51
参照トップページ
リファラー: | |
スクリーン横幅: | 1280 |
スクリーン色深度: | 720 |
スクリーン色深度: | 24 |
foundid | |
言語: | en-US |
ユーザーエージェント | Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; ClaudeBot/1.0; +claudebot@anthropic.com) |
ブラウザ:: | Netscape |
ブラウザバージョン: | 5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; ClaudeBot/1.0; +claudebot@anthropic.com) |
ホスト名: | laboict.php.xdomain.jp |
実行ファイルパス: | / |
入力のみや出力のみといった一方向のプロセス間通信であれば、 popen() で行うことができました。
しかし、 popen() は双方向によるプロセス間通信には対応しておらず、 他の方法で実現するしかありません。
他のプロセスとの送受信を実現するにはいくつかの方法がありますが、 ここでは pipe() を使った無名パイプを利用する方法を検討しましょう。
pipe() は無名パイプを作成する関数です。
pipe() によって作成されるパイプは、 一種のバッファのようなものです。 パイプを通じたデータ通信は、 一時ファイルを介してデータをやりとりするようなイメージになります。
ただし、 一時ファイルと異なりパイプが保持しているデータは、 一度読み取られるとその読み取られた分の内容は消え去ってしまいます。
pipe() の引数には、 2つの要素をもった int の配列を渡してやります。
pipe() によるパイプの作成が成功すると、 ここの配列に書き込み用と読み込み用のファイルディスクリプタ*1が格納されます。
pipe() のパラメータに私た配列のうち、 「 配列[0] 」には読み込み用のファイルディスクリプタがセットされ、 「 配列[1] 」には書き込み用のファイルディスクリプタがセットされます。
セットされたあとはファイルの読み書きと同様の手順で入出力を行うことができます。 ただ、パイプからデータを読み込むときには、 そのパイプにデータが存在しないと、 新たにデータが書き込まれるまで処理がブロックされて入力待ち状態になります。
なお、 pipe() はシステムコールによる補助が必要となるため、 どの環境でも使えるとは限りません。 POSIX規定のインターフェースですので、 POSIX準拠の環境であるかどうかを確認しましょう。
それでは、 実際に pipe() の動作を検討してみましょう。
次のコードは、 パイプの動作をわかりやすくするために、 単一のプロセス内でパイプの入出力を行うコードです。
●パイプの作成と入出力例
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #define ERR(m,c) do { perror(m); exit(c); } while (0) enum PIPE { R = 0, W = 1, SIZE = 2 }; int main() { ssize_t sz; int fpipe[SIZE]; if (pipe(fpipe) < 0) ERR("pipe()", -1); // パイプへの書き込み char wbuf[128] = "write data to pipe"; sz = write(fpipe[W], wbuf, sizeof(wbuf)); if (sz < 0) ERR("write()", -2); // パイプからの読み込み char rbuf[128]; sz = read(fpipe[R], rbuf, sizeof(rbuf)); if (sz < 0) ERR("read()", -3); rbuf[sz] = '\0'; printf("rbuf = [%s]\n", rbuf); // パイプを閉じる close(fpipe[W]); close(fpipe[R]); return 0; }
●実行結果
rbuf = [write data to pipe]
パイプへの読み書きは、 ファイルを書き込んだり、 ファイルを読み込んだりするのと非常によく似ていますね。
pipe() により読み込み用のファイルディスクリプタと書き込み用のファイルディスクリプタが fpipe 配列にセットされるので、 それを利用してパイプへデータの入出力を行っているのです。
パイプを生成するには、 pipe関数やpipe2関数を利用してシステムコールを呼び出します。
●unistd.h
#define __NR_pipe 42
●書式:pipe関数
#include <unistd.h> int pipe(int pipefd[2]);
pipeはPOSIX準拠です。 パイプは、プロセス間通信に使用できる単方向のデータチャネルです。
引数となる配列 pipefd には、 パイプの両端を参照する二つのファイルディスクリプタがセットされます。
pipe関数により無事パイプが作成されたとき、 引数となるpipefd配列のうち、 pipefd[0] がパイプの読み出し側、 pipefd[1] がパイプの書き込み側となります。
パイプの書き込み側に書き込まれたデータは、 パイプの読み出し側から読み出されるまでカーネルでバッファリングされます。
戻り値には、 処理に成功した場合は 0 が返されます。 エラーの場合は -1 が返され、 エラー内容に沿ってerrno が設定されます。
パイプの書き込み側を参照しているファイルディスクリプタがすべてクローズされた後で、 そのパイプから read を行おうとした場合、 EOFとなりread()はEOFを返します*2。
パイプの読み出し側を参照しているファイルディスクリプタがすべてクローズされた後で、 write() を行うと、 呼び出し元プロセスに SIGPIPE シグナルが送られます。
呼び出し元プロセスがこのシグナルを無視しているときには、 write() は errno に EPIPE を設定して失敗します。
●一方のパイプが閉じられたときの挙動
対象 | 条件 | 結果 |
入力 | 対となる出力パイプがクローズ | EOFが返される |
出力 | 対となる入力パイプがクローズ | SIGPIPEが発生。errnoにEPIPEをセット |
pipe() と fork() を使用する場合、 close() を適切に使って不必要なファイルディスクリプタの複製を クローズしていくようにしましょう。
これにより、 必要な時に確実に 「end-of-file(EOF)」 や 「SIGPIPE/EPIPE」 が配送されるようになります。
たとえば、 親プロセスから子プロセスへデータを送る場合は、 親プロセス側の読み出しパイプはクローズし、 子プロセス側の書き込みパイプはクローズした上で、 データのやり取りをするということです。
もうひとつのpipe2関数はLinux固有の関数です。
パイプを作成するときに、 そのパイプのハンドルとなるファイルディスクリプタの振る舞いも一緒に設定することができます。
●書式:pipe2関数
#include <fcntl.h> /* O_* 定数 */ #include <unistd.h> int pipe2(int pipefd[2], int flags);
●flagsに設定できる定数
定数 | 説明 |
O_CLOEXEC | open の O_CLOEXEC フラグと同じ |
O_DIRECT | 「パケット」モードで入出力を行うパイプを作成する。 |
O_NONBLOCK | O_NONBLOCK ファイルステータスフラグをセットする。ブロックを行わなくなる |
上記のフラグは、fcntlを利用してもセットすることができるので、移植性を重視するのであれば 「pipe + fcntl」 のセットを利用するほうがよいでしょう。
パイプには、 2つのファイルディスクリプタが対応付けられます。
パイプの内部ではパイプバッファという専用のメモリが確保され、2つのファイルディスクリプタを用い、 パイプバッファの入出力を行うことができます。
デフォルトのパイプでの入出力は、 通常のファイル入出力とは異なり、次のような動作が発生します。
このブロック処理の条件を頭にいれておかないと、 簡単にデッドロックが発生してしまうため、 注意しましょう。
たとえば、 次のコードのように、 パイプへデータを書き込む側のプロセスでブロックが起こらないと想定して、 必ずパイプへの書き込み処理ですぐに制御が返ることを前提に、 読み取り側のプロセスのコードを実装すると、 デッドロックが発生するバグの要因となってしまいます。
●デッドロックが発生する可能性のあるコード
int fd[2]; pid_t pid; pipe(fd); pid = fork(); if (!pid) { /* 子プロセス */ write(fd[1], ...); // データ量によってはブロックする可能性がある return 0; } else if (pid > 0) { /* 親プロセス */ wait(NULL); // write() でブロックが発生すると永遠に待機する read(fd[0], ...); }
子プロセスはパイプにデータを出力したあと、 そのまま終了することを前提に、 親プロセスの処理を記述しています。
しかし、 子プロセスの出力側がパイプバッファのサイズ以上のデータを出力しようとすると、 パイプバッファが読み取られて空きがでるまで、 そのまま制御が返らなくなります。
この場合、 親プロセスでは、 wait() で子プロセスの終了を待っているのですから、 パイプの入力処理が発生せずにいつまでも待ち続けることになります。
この場合、 パイプバッファにデータが入るまで入力でブロックが起こるため、 wait() と read() を入れ替えることで、 回避することができます。
複数回パイプへ入出力する場合は、 パイプを入出力する際のバッファのサイズを入力側と出力側であわせるようにしたほうが無難でしょう。
たとえば次のようにパイプへ2回出力すると、 パイプには8バイトのデータが存在します。
次に、入力時にはパイプから256バイト分のデータを読み取るので、 1回目の入力でパイプに存在する8バイトのデータをすべて読み取ってしまいます。
そのため、2回目の入力は入力待ちとなってブロック状態となります
●ブロックが発生する例
#include <unistd.h> int main() { enum PIPE { R = 0, W = 1 }; int fpipe[2]; pipe(fpipe); char buf[256] = {0}; write(fpipe[W], "abc", 4); // 「abc\0」4バイト書き込む write(fpipe[W], "def", 4); // 「def\0」4バイト書き込む read(fpipe[R], buf, sizeof(buf)); // 「abc\0def\0」を読み取る read(fpipe[R], buf, sizeof(buf)); // データがないのでブロック状態となる return 0; }
この場合、出力側の出力サイズを入力側の256バイトにあわせておけば、 期待どおり1回目の出力データは1回目の入力で得られ、 2回目の出力データは2回目の入力で得ることができます。
●ブロックが発生しない例
#include <string.h> #include <unistd.h> int main() { enum PIPE { R = 0, W = 1 }; int fpipe[2]; pipe(fpipe); char buf[256] = {0}; strcpy(buf, "abc"); write(fpipe[W], buf, sizeof(buf)); // 256バイト書き込む strcpy(buf, "def"); write(fpipe[W], buf, sizeof(buf)); // 256バイト書き込む read(fpipe[R], buf, sizeof(buf)); // 「abc\0\0\0・・・」を読み取る read(fpipe[R], buf, sizeof(buf)); // 「def\0\0\0・・・」を読み取る return 0; }
pipe関数で作成したパイプは、 fork() で作成した子プロセスに承継して共有することができます。
pipe関数によって割り当てられたファイルディスクリプタも、 子プロセスでそのまま使うことができます。
fork() によって分離された親プロセスと子プロセスで同じパイプへアクセスできることから、 そのパイプへの入出力を通じて、 プロセス間の通信を実現することができます。
それでは、 fork() で作成した子プロセスと、 パイプを使ったプロセス間通信を行うコードを検討してみましょう。
●子プロセスとのパイプ通信例
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <wait.h> #define ERR(m,c) do { perror(m); exit(c); } while (0) enum PIPE { R = 0, W = 1, SIZE = 2 }; int main() { ssize_t sz; int fpipe[SIZE]; if (pipe(fpipe) < 0) ERR("pipe()", -1); pid_t pid = fork(); if (pid < 0) { ERR("fork()", -2); } else if (pid == 0) { // 子プロセス char buf[128]; sz = read(fpipe[R], buf, sizeof(buf)); if (sz < 0) ERR("read()", -3); buf[sz] = '\0'; printf("子の読み取り=「%s」\n", buf); char str1[128] = "子の書き込み"; sz = write(fpipe[W], str1, strlen(str1)); if (sz < 0) ERR("write()", -4); close(fpipe[R]); close(fpipe[W]); return 0; } else { // 親プロセス char str2[128] = "親の書き込み"; sz = write(fpipe[W], str2, strlen(str2)); if (sz < 0) ERR("write()", -5); sleep(1); char buf[128]; sz = read(fpipe[R], buf, sizeof(buf)); if (sz < 0) ERR("read()", -6); buf[sz] = '\0'; printf("親の読み取り=「%s」\n", buf); close(fpipe[R]); close(fpipe[W]); wait(NULL); return 0; } }
forkを使ってもデータセグメントやヒープやスタックに確保した領域などと異なり、 子プロセスにコピーされたパイプのディスクリプタは、 親プロセスのパイプのディスクリプタと実体は同じで共有をしています。
そのため、 コピー元のパイプとコピーされたパイプのディスクリプタを利用して、 親プロセスと子プロセスとデータのやりとりが可能です。
しかしながら、 親プロセスが書き込んだ内容を親プロセスが読み込んでしまうとパイプが保持している内容は消えてしまうので、 子プロセスは何も読み込めなくなってしまいます。
上記の場合は、親プロセスが書き込んだ後、1秒間とめて、子プロセスが読み込むのを待ってから、パイプを読み込んでいます。
sleep() を消すとタイミングがとれなくなり、親プロセスが自身が書き込んだ内容を子プロセスより先に読み込んでしまうことがあります。
パイプは単方向の通信を想定しています。
上記コードでは一つのパイプで双方向通信していますが、 実際に双方向通信の実装をするときはパイプを2つ用意し、 親から子に送るパイプと子から親へ送るパイプをわけて使うと良いでしょう。
pipe関数をもちいてパイプを作成すると、 プロセスに対して新たなファイルディスクリプタが割り当てられます。
そのパイプに割り当てられたファイルディスクリプタの入出力先がパイプとなり、 このファイルディスクリプタを通して、 パイプとデータのやりとりができるようになります。
つまり、 pipe関数によって作成したパイプは、 同時にpipe関数によって割り当てられたファイルディスクリプタを利用して、 入出力を行うことができます。
ファイルの入出力でも、 open() を用いてファイルを開いたあと、 同時に割り当てられたファイルディスクリプタを利用して入出力を行うことと似ていますね。
BSD系やLinux系のOSでは、 プロセス情報がファイルシステム上にエクスポートされています。 この情報を利用して、 ファイルディスクリプタの割り当て情報を出力するコードを検討してみましょう。
●パイプに割り当てられたファイルディスクリプタの確認
#include <stdio.h> #include <unistd.h> enum PIPE { R = 0, W = 1, SIZE = 2 }; int main() { int fpipe[SIZE]; char cmd[255] = {0}; if (pipe(fpipe) < 0) { perror(NULL); return -1; } printf("fpipe[R] = %d, fpipe[W] = %d\n", fpipe[R], fpipe[W]); sprintf(cmd, "ls -l /proc/%d/fd", getpid()); system(cmd); close(fpipe[W]); close(fpipe[R]); return 0; }
●実行結果
fpipe[R] = 3, fpipe[W] = 4 total 0 0 -> /dev/pts/1 1 -> /dev/pts/1 2 -> /dev/pts/1 3 -> pipe:[39693] 4 -> pipe:[39693]
BSD系やLinux系のOSでは、 「/proc/pid/fd」ディレクトリに、 そのプロセスに割り当てられたファイルディスクリプタが収められています。
これを確認することで、 パイプ作成後にどのようなディスクリプタが割り当てられているかを確認することができます。
実行結果をみると、 ファイルディスクリプタに「pipe:[39693]」が結び付けられているのが確認できます。
また、 0から2のディスクリプタは標準入力、標準出力、標準エラー出力です。 これらはプログラム起動時に既定でOSから割り当てられます。 上記コード例ではそれぞれ擬似端末「/dev/pts/1」に結び付けられているのが確認できます。
「/dev/pts/1」は、 疑似端末を表しており、 標準入力、標準出力、標準エラー出力は、 ターミナルエミュレータに繋がっていることを示しています。
パイプに入出力を行う際に、 ブロックを行わないようにするには、 pipe2関数を利用するか、 fcntl関数でファイルディスクリプタの設定を行います。
pipe2関数は、 Linux固有であるため、 通常はfcntl関数による設定変更を利用します。
ノンブロッキングを指定したときで、 パイプにデータが存在せずに読み取りができなかったときや、 パイプのバッファがデータで一杯となっているときは、 errnoにEAGAINやEWOULDBLOCKがセットされます。
これを処理分岐の目印にして、 ポーリングを行うなどすることで、 ほかの処理を行うことができるようになります
●非同期通信例
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> enum PIPE { R = 0, W = 1, SIZE = 2 }; ssize_t pipe_read(int fd, char *rbuf, size_t sz) { ssize_t rc; errno = 0; rc = read(fd, rbuf, sz); if (errno == EAGAIN || errno == EWOULDBLOCK) perror("read() = data no exist in pipe."); return rc; } ssize_t pipe_write(int fd, char *wbuf, size_t sz) { ssize_t rc; errno = 0; rc = write(fd, wbuf, sz); if (errno == EAGAIN || errno == EWOULDBLOCK) perror("write() = write data to pipe."); return rc; } int main() { int fpipe[SIZE]; int flags; char rbuf[128] = { 0 }; char wbuf[128] = "write data to pipe"; if (pipe(fpipe) < 0) { perror("pipe()"); exit(-1); } // 既存のフラグを取得 if ((flags = fcntl(fpipe[R], F_GETFL, 0)) < 0) { perror("fcntl() F_GETFL R"); exit(-1); } // ノンブロッキングに指定 fcntl(fpipe[R], F_SETFL, flags | O_NONBLOCK); // 既存のフラグを取得 if ((flags = fcntl(fpipe[W], F_GETFL, 0)) < 0) { perror("fcntl() F_GETFL W"); exit(-1); } // ノンブロッキングに指定 fcntl(fpipe[W], F_SETFL, flags | O_NONBLOCK); // パイプからの読み込み // データが存在しないので通常であれば // ここで処理が止まる pipe_read(fpipe[R], rbuf, sizeof(rbuf)); // パイプへの書き込み pipe_write(fpipe[W], wbuf, sizeof(wbuf)); // パイプからの読み込み pipe_read(fpipe[R], rbuf, sizeof(rbuf)); printf("rbuf = [%s]\n", rbuf); close(fpipe[W]); close(fpipe[R]); return 0; }