Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

ブロッキング・ノンブロッキング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 でパスを指定することで作成します。

mkfifo
  int ret = mkfifo("test", 0666);
  int (ret == -1) {
    perror("mkfifo");
  }

mkfifoat は指定したファイルディスクリプタを基準にしてFIFOを作成します。

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が存在する場合 errnoEEXIST が設定されます。

FIFOの使い方

FIFOはファイルシステムを使用するのでファイルを操作するように open, write, read を使用することができます。
(といってもデータが書き込まれる場所はカーネル内のバッファです。)

open

ファイルのopenと同様にFIFOにパスを指定してopenします。

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 できず不便です。
そこで openO_NONBLOCK を指定することでノンブロッキングでFIFOを開くことができます。

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

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 によってブロックされる場合、errnoEAGAIN が設定され read-1 を返します。

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に書き込むことができます。

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は明示的に削除される必要があります。

remove
  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版

seriru13
競技プログラミングとかwebとかゲームとか。 C++が好きなC++初心者です。たまにTSも書きます。最近はよくC#を書いてます。 ライブラリとかゲームとか作って見たい。
https://ryo-s1126.hatenablog.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away