最近システムプログラミングを始めた初心者です。知識定着のために記事を書いているので、もし間違いやアドバイスなどがありましたらコメントで指摘してくださると非常に助かります。
概要
本記事はFIFOの説明、使い方をコード例を交えながら解説します。
この記事で登場するプログラムは以下の環境で動作を確認しています。
Ubuntu 18.04.4 LTS (Bionic Beaver)
gcc version 9.2.1
また、本記事で紹介するAPIなどの特に詳しい説明(詳細なエラーやその対処など)は対象のドキュメントに譲り、自分がFIFOを使用するために最低限必要だった点やはまってしまった点を重点的に説明します。
FIFOとは
FIFOとは名前付きパイプとも呼ばれるプロセス間通信の一種で、共通祖先を持つプロセス同士でしか使えなかった無名パイプを異なるプロセス間同士の通信手段として使えるように拡張したものです。
FIFOはUnix系OSではファイルの一種であり、普通のファイルのように操作をすることが可能です。
FIFOの作り方
FIFOは基本的に mkfifo
または mkfifoat
でパスを指定することで作成します。
int ret = mkfifo("test", 0666);
int (ret == -1) {
perror("mkfifo");
}
mkfifoat
は指定したファイルディスクリプタを基準にしてFIFOを作成します。
int fd, ret;
const char* home_path = getenv("HOME"); // 環境変数からHOMEのパスを取得
fd = open(home_path, O_RDONLY); // HOMEDirのfdを取得
if (fd == -1) {
perror("open");
exit(0);
}
ret = mkfifoat(fd, "test", 0666); // HOMEを基準にFIFOを作成
if (ret == -1) {
perror("mkfifo");
}
FIFOはファイルの一種なのでlsでも存在を確認できます。
また、すでに同名のFIFOが存在する場合 errno
に EEXIST
が設定されます。
FIFOの使い方
FIFOはファイルシステムを使用するのでファイルを操作するように open
, write
, read
を使用することができます。
(といってもデータが書き込まれる場所はカーネル内のバッファです。)
open
ファイルのopenと同様にFIFOにパスを指定してopenします。
#include <sys/fcntl.h>
int fd1 = open("test", O_RDONLY); // 読み出しOnly
int fd2 = open("test", O_WRONLY); // 書き込みOnly
int fd3 = open("test", O_RDWR); // 読み書きどちらも可能
FIFOはその両端がオープンされるまでデータを渡すことができません(= openがブロッキングされる)。
この仕様ではFIFOでサーバ・クライアント間の通信を行う際などに、サーバがクライアントから通信を受け付けるまでFIFOを open
できず不便です。
そこで open
の O_NONBLOCK
を指定することでノンブロッキングでFIFOを開くことができます。
#include <sys/fcntl.h>
int fd_nb1 = open("test", O_RDONLY | O_NONBLOCK); // modeは論理和で指定
int fd_nb2 = open("test", O_RDWR | O_NONBLOCK);
NONBLOCK
指定時の注意として、FIFOを write
モードで開く場合は片方が開いていても ENXIO(no such device or address)
を返されます。
そのためここでは O_WRONLY
に代わって O_RDWR
を採用しています(ただし、POSIXではO_RDWRの規程がありません。従ってこれはLinuxでしか使えず移植には注意が必要です)。
read
FIFOを open
したときのファイルディスクリプタを使用して指定バイト分だけFIFOの先頭からデータを読むことができます。
// readの1例
struct Packet {
int data1;
double data2;
};
void* buf = malloc(sizeof(Packet));
int len = read(fd, buf, sizeof(Packet));
Packet p = reinterpret_cast<Packet*>(buf);
またノンブロッキングなFIFOでは、read
によってブロックされる場合、errno
に EAGAIN
が設定され read
は -1
を返します。
int len = read(fd, buf, SIZE);
if (len < 0) {
if (errno == EAGAIN) {
fprintf(stderr, "avoid blocking\n");
} else {
perror("read");
}
} else if (len == 0) {
printf("EOF\n"); // len == 0のときはパイプの中身を全て読んでいる状態。
}
// len > 0 でlen byte分データを読んだ。
また自分でFIFOでreadの戻り値を実験したところ
パイプを書き込みモードで開いたプロセス数 | readの戻り値 |
---|---|
0 | 0 (EOF) |
1 | -1 (EAGAIN) or 読み込んだバイト数 |
という結果になりました。
write
read
と同様に指定したバイト分をFIFOに書き込むことができます。
// writeの1例
struct Packet {
int data1;
double data2;
}
Packet p {10, 2.2};
int len = write(fd, &p, sizeof(Packet));
ノンブロッキングなFIFOでは、これも read
と同様に write
によってブロックされるときに EAGAIN
が設定されます。
remove
FIFOは明示的に削除される必要があります。
int ret = remove("test");
if (ret == -1) {
perror("remove");
}
自分は削除に remove
を使用しているのですが他に良い方法があれば教えてください。
まとめ
- FIFOはファイルシステムを使用するプロセス間通信の1つ。
- FIFOを使用できるのはそれが作成されたコンピュータ内に存在するプロセス。例えばマウントしたNFS共有上にFIFOを作成しても基本的にはデータを書き込むことはできない。
- FIFOは
open
,read
,write
,remove
などの通常のファイルI/Oを使用して操作する。
参考文献・サイト
Wikipedia 名前付きパイプ
FIFO (FIFO7の日本語翻訳)
FIFO(7)
詳解UNIXプログラミング 第3版