ブロッキング・ノンブロッキングFIFO(名前付きパイプ)の使い方

最近システムプログラミングを始めた初心者です。知識定着のために記事を書いているので、もし間違いやアドバイスなどがありましたらコメントで指摘してくださると非常に助かります。

# 概要
本記事はFIFOの説明、使い方をコード例を交えながら解説します。
この記事で登場するプログラムは以下の環境で動作を確認しています。

```
Ubuntu 18.04.4 LTS (Bionic Beaver)
gcc version 9.2.1
```

また、本記事で紹介するAPIなどの特に詳しい説明(詳細なエラーやその対処など)は対象のドキュメントに譲り、自分がFIFOを使用するために最低限必要だった点やはまってしまった点を重点的に説明します。

# FIFOとは
**FIFO**とは名前付きパイプとも呼ばれる**プロセス間通信の一種**で、共通祖先を持つプロセス同士でしか使えなかった無名パイプを異なるプロセス間同士の通信手段として使えるように拡張したものです。

FIFOはUnix系OSではファイルの一種であり、**普通のファイルのように操作をすることが可能**です。

## FIFOの作り方
FIFOは基本的に `mkfifo` または `mkfifoat` でパスを指定することで作成します。

```cpp:mkfifo
  int ret = mkfifo("test", 0666);
  int (ret == -1) {
    perror("mkfifo");
  }
```
`mkfifoat` は指定したファイルディスクリプタを基準にしてFIFOを作成します。

```cpp:mkfifoat
  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します。

```cpp: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を開くことができます。

```cpp:nonblocking
  #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の先頭からデータを読むことができます。

```cpp:read(Blocking)
  // 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` を返します。

```cpp:read(NonBlocking)
  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に書き込むことができます。

```cpp:write(Blocking)
  // 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は明示的に削除される必要があります。

```cpp:remove
  int ret = remove("test");
  if (ret == -1) {
    perror("remove");
  }
```

自分は削除に `remove` を使用しているのですが他に良い方法があれば教えてください。

## まとめ
* FIFOはファイルシステムを使用するプロセス間通信の1つ。
* FIFOを使用できるのはそれが作成されたコンピュータ内に存在するプロセス。例えばマウントしたNFS共有上にFIFOを作成しても基本的にはデータを書き込むことはできない。
* FIFOは `open`, `read`, `write`, `remove` などの通常のファイルI/Oを使用して操作する。

## 参考文献・サイト
[Wikipedia 名前付きパイプ](https://ja.wikipedia.org/wiki/%E3%83%91%E3%82%A4%E3%83%97_(%E3%82%B3%E3%83%B3%E3%83%94%E3%83%A5%E3%83%BC%E3%82%BF))
[FIFO](https://linuxjm.osdn.jp/html/LDP_man-pages/man7/fifo.7.html) (FIFO7の日本語翻訳)
[FIFO(7)](https://www.man7.org/linux/man-pages/man7/fifo.7.html) 
[詳解UNIXプログラミング 第3版](https://www.amazon.co.jp/dp/B00KRB9U8K/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1)