fifo-nonwait - FIFO をプロセス間の同期目的で使いたい
目的
Linux上のプロセスの同期待合せ処理として、処理がシンプルなFIFO(名前付きパイプ)を使いたい。
やりたいことは、μuITRONのイベントフラグ相当の処理。
外部仕様を書くと、
- 書込み側はいつでも書き込め、読み出し側のopenを待たずに次の処理を続ける
- 読み出し側(待つ側)は書き込み側よりも前にopenしても良い
- 書き込みデータがロストしない
結果
- FIFO作成後、一旦ダミーでO_NONBLOCKフラグを付けてopen()しておく(openしっぱなし)
- 書き込み側は O_RDWR|O_NONBLOCK の flag でopen()してwrite()する
- 読み込みは、普通にopen()してread()する
という役割分担にすれば良い。
以下、詳細。
FIFO の man page
fifo(4) には以下の記述がある:
The kernel maintains exactly one pipe object for each FIFO special file that is opened by at least one process. The FIFO must be opened on both ends (reading and writing) before data can be passed. Normally, opening the FIFO blocks until the other end is opened also.
これによると、以下の困ったことがありそう:
- FIFO の両端(writeとread)ともがclose状態だとデータが破棄される? つまり、write側がcloseしたら、read側がopenしてもデータが無くなってる?
- 相手側がopenするまで待たされる? つまり、read側がopenするまではwrite側も待たされてしまう?
仮説の検証
FIFO プログラムで、実仕様を確認する。
(長くなるので、プログラムは記事末尾に記載)
確認結果
以下の三つの登場人物を使う:
- writer: 書き込み側(通知する側)
- reader: 読み込み側(待ち受ける側)
- opener: FIFOをopenだけしておく人
また、open() の 引数に O_NONBLOCK を付けるか付けないかで挙動が変わるので、そこに注目する。
writer が先に open するケース (1)
以下の動作を期待:
→時間 writer: open -> write -> close reader: open -> read -> close
実際は:
% ./fifo-test w b fifo & ; sleep 1; ./fifo-test r b fifo [writer] <#8721> before open (sleep) [reader] <#8723> before open [reader] <#8723> before read [writer] <#8721> before write [reader] <#8723> after read [writer] <#8721> after write
readerがアクセスするまで、writerが待たされている。
これでは、要件の「読み出し側のopenを待たずに次の処理を続ける」が満たせていないので困る。
writer が先にopenするケース (2)
writerのopenをO_NONBLOCKで「待ち無し」にしてみる。
ちなみに、fifo(4) にも記載されているが、O_WRONLY|O_NONBLOCK を指定するとENXIOエラーが返るので、
open の引数を O_RDWR|O_NONBLOCK に変更する。
参考:
A process can open a FIFO in non-blocking mode. In this case, opening for read only will succeed even if noone has opened on the write side yet; opening for write only will fail with ENXIO (no such device or address) unless the other end has already been opened.
トライアルの結果:
% ./fifo-test w n fifo & ; sleep 1; ./fifo-test r b fifo [writer] <#8851> before open [writer] <#8851> before write [writer] <#8851> after write [reader] <#8853> before open
writerが書いたデータをreaderが読めない。
これは、fifoの両端が閉じられると、内部のデータも破棄されてしまうため。
ということは、fifoを誰かが常にopenし続けておく必要がある。
writer が先に open するケース (3)
第三の登場人物 opener を導入して、ダミーで fifo を open させてみる。
期待する動作:
→時間 opener: open writer: open -> write -> close reader: open -> read -> close
トライアルの結果:
% ./fifo-test o b fifo & ; sleep 1; ./fifo-test w b fifo & ; sleep 1; ./fifo-test r b fifo [1] 8882 [opener] <#8882> before open [opener] <#8882> after open (sleep) [writer] <#8884> before open [writer] <#8884> before write [writer] <#8884> after write (sleep) [reader] <#8886> before open [reader] <#8886> before read [reader] <#8886> after read
writer は reader が来なくても処理が終了できて、
reader は writer が書いたデータをきちんと読めている。
これで、writer が先に open するケースは期待通りに動作した。
reader が先に open するケース (1)
opener を導入した状態でテスト。
期待する動作:
→時間 opener: open reader: open -> read -> close writer: open -> write -> close
実際は:
% ./fifo-test o b fifo & ; sleep 1; ./fifo-test r b fifo & ; sleep 1; ./fifo-test w b fifo [opener] <#8890> before open [opener] <#8890> after open (sleep) [reader] <#8892> before open [reader] <#8892> before read (sleep) [writer] <#8894> before open [writer] <#8894> before write [writer] <#8894> after write [reader] <#8892> after read
ちゃんとreaderがwriterを待っており、正しく動作している。
まとめ
テスト結果を整理すると、以下になる(比較のために他の条件もいくつかテストした):
条件 | writer open フラグ | opener | 結果 | |
---|---|---|---|---|
writerが先にopen | ブロック | 無し | NG (writerが待たされる) | |
writerが先にopen | O_NONBLOCK | 無し | NG (readerがデータを読めない) | |
writerが先にopen | どちらでも | あり | OK | |
readerが先にopen | ブロック | 無し | OK | |
readerが先にopen | O_NONBLOCK | 無し | NG (後のwriterが待ちにはいる) | |
readerが先にopen | どちらでも | あり | OK |
確認に使ったコード
/* * Linux FIFO の動作仕様を確認するサンプル */ #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> static void die(char *msg) { perror(msg); exit(1); } /* * open for reading */ int fifo_reader(char *path, int nonblock_flag) { int fd, rc; char buf[10]; printf("[reader] <#%d> before open\n", getpid()); fd = open(path, O_RDONLY | (nonblock_flag ? O_NONBLOCK: 0)); if (fd < 0) die("cannot open fifo for reading"); printf("[reader] <#%d> before read\n", getpid()); rc = read(fd, buf, sizeof(buf)); if (rc < 0) die("failed to read"); printf("[reader] <#%d> after read\n", getpid()); close(fd); return 0; } /* * open for writing */ int fifo_writer(char *path, int nonblock_flag) { int fd, rc; char buf[10] = "123456789"; printf("[writer] <#%d> before open\n", getpid()); fd = open(path, (nonblock_flag ? O_RDWR|O_NONBLOCK: O_WRONLY)); if (fd < 0) die("cannot open fifo for writing"); printf("[writer] <#%d> before write\n", getpid()); rc = write(fd, buf, sizeof(buf)); if (rc < 0) die("failed to write"); printf("[writer] <#%d> after write\n", getpid()); close(fd); return 0; } /* * just open */ int fifo_opener(char *path, int nonblock_flag) { int fd; printf("[opener] <#%d> before open\n", getpid()); fd = open(path, O_RDWR | (nonblock_flag ? O_NONBLOCK: 0)); if (fd < 0) die("cannot open fifo for writing"); printf("[opener] <#%d> after open\n", getpid()); sleep(0x7fffffff); return 0; } int main(int argc, char *argv[]) { char *mode_str; char *flag_str; char *fifo_name; int nonblock_flag = 0; /* BLOCK by default */ if (argc != 4) { fprintf(stderr, "usage: %s <mode> <flag> <fifo-name>\n", argv[0]); fprintf(stderr, " mode: r|w|o for read|write|open\n"); fprintf(stderr, " flag: b|n for BLOCK/NONBLOCK\n"); exit(1); } mode_str = argv[1]; flag_str = argv[2]; fifo_name = argv[3]; switch (*flag_str) { case 'b': nonblock_flag = 0; break; case 'n': nonblock_flag = 1; break; default: fprintf(stderr, "invalid flag: '%s'\n", flag_str); break; } switch (*mode_str) { case 'r': fifo_reader(fifo_name, nonblock_flag); break; case 'w': fifo_writer(fifo_name, nonblock_flag); break; case 'o': fifo_opener(fifo_name, nonblock_flag); break; default: fprintf(stderr, "invalid mode: '%s'\n", mode_str); break; } return 0; }