コンピューター:C言語講座:FIFO(名前付きパイプ)について
概要
FIFOは本来、First In First Outというデータ形式の名称で、はじめに入ったデータがはじめに取り出されるという意味で、STACK形式のように最後に入ったデータがはじめに取り出されるのに対比するものですが、ここではプロセス間の通信に使用するFIFOを取り上げます。
TCP/IPプログラムでプロセス間で通信を行なう説明を行ないましたし、fork,exec,pipeでも同様にプロセス間で通信を行なえました。今回の話題のFIFOはPIPEによく似た感じのものですが、パイプは親があらかじめ準備してから子を起動して通信するのですが、FIFOはお互いに勝手に通信をはじめることが出来ます。ファイルのように名前をつけたパイプというイメージで、実際にUNIXではファイルのようにディレクトリ中に作成します。したがって、FIFOを準備する段階では誰が相手になるかは全くわからない状態ではじめることが出来ます。
個人的にはネットワークを使用した通信ではソケット、1つのマシン内ではPIPEをよく使い、FIFOはほとんど使ったことが無かったのですが、あるプロジェクトで多くのモジュールを連携させる場合にソケット程大げさではなく、PIPEより自由がきくことがわかり、取り上げることにしました。
使用方法
まず、FIFOを作成する必要が有ります。システムコールmknod()を使用して以下のように作成します。
mknod(path,S_IFIFO|0666,0);
または、mkfifo()を使用して以下のように作成も出来ます。
mkfifo(path,0666);
mkfifo()は内部でmknod()を呼び出すので、どちらでも同じ事ですが、普通はmkfifo()を使うようです。いずれも失敗時には-1が返り、errnoにエラー情報が格納されます。成功すると、0が返ります。
モードは上の例では0666を指定していますが、一般ファイルと同様にパーミッションを指定します。
pathには一般のファイルを作成するのと同様にFIFOを作成するパスを指定します。成功すると指定されたパスにファイルのように名前のついたFIFOが作成されます。
データの送受信は作成したFIFOに読み込み・書き込みの許可さえ有ればどんなプロセスからでも行なえます。作成したFIFOに対してopen()システムコールでファイル記述子を得れば、ファイル同様に送信にはwrite(),受信にはread()システムコールを使用出来ます。もちろんfwrite(),fprintf(),fpus(),fread(),fscanf(),fgets()など高水準入出力を使用しても全く問題はありません。
UNIXではソケットでもFIFOでもPIPEでも一般ファイルでもデバイスファイルでも全て同じシステムコールが使用できるのが素晴らしい点で、FIFOでも入力を多重化して待ちたければselect()システムコールが使用できますし、待ち無しで入出力したければモードにO_NDELAYを指定することも出来ます。
注意点
ソケット・PIPE同様にバッファのサイズに注意が必要です。一般的に4096バイトを越える場合に、読みだし側が随時読みだしてくれれば良いのですが、読みだしてくれない場合はwrite()がブロックされます(待ち無しの場合はブロックされずに返る)。したがって、データを別のプログラムに渡し、その回答を得たい場合などに、送ることも出来ず、自分でも受け取らないと相手も送れず、デッドロック状態に陥ります。したがって、大きなデータを処理する場合は送信しながらもこまめに受け取りも考える必要が有ります。
また、FIFOはPIPEと異なり、ファイルのようにディレクトリ中に存在します。ということは使え終えてもそのままにしておけばいつまでも存在してしまいます。特にディスクを消費するようなものではありませんが、気分が悪いので、不要になったら削除するようにしましょう。一般のファイルと同じように削除できます。
さらに、PIPE同様に1つのFIFOに対して読み書き兼用で使用することはうまく行きません。両方向が必要であれば2つのFIFOを使用する必要があります。
サンプル
今回は気の効いたサンプルが思い付かなかったので、関数の使用例、という感じで短いサンプルを作成しました。送信プログラムを起動するとFIFOを作成し、標準入力から得たものをFIFOに書き出します。受信プログラムはFIFOから読みだし、標準出力に表示します。送信側でCtrl+D(UNIX)を送ると受信側でFIFOを削除して終了するようにしてみました。
<<送信プログラム>>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/errno.h>
#include <sys/fcntl.h>
void main()
{
int fd;
char buf[256];
if(mkfifo("/tmp/FifoTest",0666)==-1){
perror("mkfifo");
/* 死にはしない */
}
if((fd=open("/tmp/FifoTest",O_WRONLY))==-1){
perror("open");
exit(-1);
}
while(1){
fgets(buf,sizeof(buf)-1,stdin);
if(feof(stdin)){
break;
}
write(fd,buf,strlen(buf));
}
close(fd);
}
<<受信プログラム>>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/errno.h>
#include <sys/fcntl.h>
void main()
{
int fd,len;
char buf[256];
if((fd=open("/tmp/FifoTest",O_RDONLY))==-1){
perror("open");
exit(-1);
}
while(1){
len=read(fd,buf,sizeof(buf)-1);
if(len==0){
break;
}
buf[len]='\0';
fputs(buf,stdout);
}
close(fd);
system("rm -f /tmp/FifoTest");
}
さて、このプログラムで、受信側を複数起動するとどうなるでしょうか?正確には知りませんが、実験した結果では先に起動した受信プログラムが受け取り、あとから起動したものは先に起動したものが殺されるまで何も受け取りませんでした。
逆に送信側を複数起動した場合は送った順に受信側で受け取れます(First
In First Outですから)。同時に送信した場合はシステムコール単位(write)でデータが保証されます(排他的に)。ただし、FIFOのバッファのサイズを越えた場合は一度に送れませんが。また、送信プロセスが全て閉じると受信側がEOFとなるようです。
また、FIFOをNFSマウントしたディレクトリに作成し、異なるマシンで同一のFIFOにアクセスしたくなる方もいるかと思いますが、FIFOはNFSマウント上のディレクトリには動作しません。mknod()は成功しますが、読みだし側ははじめからEOF状態になります。
以上のような特性から、FIFOは1つのマシン上で不特定多数のプロセスから指示を受けるプロセスの実装に向くと思われます。