fifo-nonwait - FIFO をプロセス間の同期目的で使いたい

目的

Linux上のプロセスの同期待合せ処理として、処理がシンプルなFIFO(名前付きパイプ)を使いたい。
やりたいことは、μuITRONのイベントフラグ相当の処理。
外部仕様を書くと、

  1. 書込み側はいつでも書き込め、読み出し側のopenを待たずに次の処理を続ける
  2. 読み出し側(待つ側)は書き込み側よりも前にopenしても良い
  3. 書き込みデータがロストしない

これが実現できるか、Linux 上での FIFO の動作仕様を確認する。

結果

  1. FIFO作成後、一旦ダミーでO_NONBLOCKフラグを付けてopen()しておく(openしっぱなし)
  2. 書き込み側は O_RDWR|O_NONBLOCK の flag でopen()してwrite()する
  3. 読み込みは、普通に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.

これによると、以下の困ったことがありそう:

  1. FIFO の両端(writeとread)ともがclose状態だとデータが破棄される? つまり、write側がcloseしたら、read側がopenしてもデータが無くなってる?
  2. 相手側がopenするまで待たされる? つまり、read側がopenするまではwrite側も待たされてしまう?

仮説の検証

FIFO プログラムで、実仕様を確認する。
(長くなるので、プログラムは記事末尾に記載)

前準備

mkfifo fifo
gcc -o fifo-test -O0 -Wall -Wextra fifo-test.c

確認結果

以下の三つの登場人物を使う:

  1. writer: 書き込み側(通知する側)
  2. reader: 読み込み側(待ち受ける側)
  3. 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

つまり、opener を導入することで、FIFOを使ってμITRONのイベントフラグ相当の処理が実現できた。

確認に使ったコード

fifo-test.c
https://sssvn.jp/svn/spikelet/linux/fifo/fifo-test.c
/*
 * 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;
}