Skip to content

多重I O

rsudo edited this page May 2, 2022 · 7 revisions

方法として以下の3つがある

  • マルチプロセス
  • マルチスレッド
  • NON-Blocking I/O

今回はNON-Blocking I/Oを使用する必要がある

select()

select()はaccept()と同様に処理をブロックするが、複数のディスクリプタを同時に見張ることができる。

いずれかのディスクリプタに変化があった場合に処理を先に進める。

#include <sys/select.h>
int select(int nfds,               // 見張るべきfdのうちの最大値+1
           fd_set *readfds,        // 読みが可能になったら教えて欲しいfd群。接続待ちも。
           fd_set *writefds,       // 書きが可能になったら教えて欲しいfd群
           fd_set *exceptfds,      // エラーが起きたら教えて欲しいfd群
           struct timeval *timeout)// 指定した時間経過でブロック解除
返り値 意味
-1 エラー
0 タイムアウト
1以上 準備状態のfdが1つ以上ある

select()と一緒に使う関数

// fdsetに指定されたfd_set型のデータの全ビットを0にする
void FD_ZERO(fd_set *fdset);

// fdをfdsetに追加する
void FD_SET(int fd, fd_set *fdset);

// fdsetにfdが含まれていれば0以外を返し、含まれていなければ0を返す
int FD_ISSET(int fd, fd_set *fdset);

処理の流れ

  1. FD_ZEROでreadfdsを0にする
  2. FD_SETで監視するfdをセットする
  3. select()を呼び出す
  4. FD_ISSETでどのfdが変化したか調べて処理を分岐

実装イメージ

  • accがサーバーのsocket
  • readfdsに含まれるfdが読み込み可能になったら処理を進める
int main() {
  int portno = 1111;
  int maxfds, next_maxfds, acc, com;
  fd_set readfds, readfds_save;

  // サーバーソケット作成
  acc = socket(AF_INET, SOCK_STREAM, 0);
  FD_ZERO(&readfds_save);
  FD_SET(acc, &readfds);
  maxfds = next_maxfds = acc + 1;

  while (1) {
    readfds = readfds_save;
    switch (select(maxfds, &readfds, NULL, NULL, NULL)) {
      // エラーの場合
      case -1:
        perror("select");
        exit(1);

      // タイムアウトの場合
      case 0:
        break;

        // 新規接続または接続済みソケットからの受信
      default:

        // 新規接続の場合
        if (FD_ISSET(acc, &readfds)) {
          if ((com = accept(acc, NULL, NULL)) < 0) {
            perror("accept");
            exit(1);
          }

          // 新しいソケットをreadfdsに追加
          FD_SET(com, &readfds);

          // maxfds更新
          if (com + 1 > maxfds) {
            next_maxfds = com + 1;
          }
        }

        // 接続済みソケットからのデータ受信かチェック
        for (int i = 0; i < maxfds; i++) {
          if (FD_ISSET(i, &readfds)) {
            // recvして色々処理してレスポンス返す

            // maxfds更新
            if (maxfds == i + 1) {
              next_maxfds = find_maxfds(&readfds_save);
            }
          }
        }
        maxfds = next_maxfds;
    }
  }
  return 0;
}

http://www.coins.tsukuba.ac.jp/~syspro/2019/2019-06-12/echo-server-select.html

select()使う時のベストプラクティス

select() を使うときは、タイムアウトは設定すべきでない。 処理するデータが無いときには、 あなたのプログラムには何もすることは無いはずである。 タイムアウトに依存したコードは通常移植性がなく、 デバッグも難しくなる。

上述したように、 効率的なプログラムを書くには nfds の値を適切に計算して与えなければならない。

select() コールの終了後に結果をチェックして、 適切に対応するつもりのないファイルディスクリプタは、 どの集合にも加えてはならない。 次のルールも参照。

select() から返った後には、全ての集合の全てのファイルディスクリプタについて 読み書き可能な状態になっているかをチェックすべきである。

read(2), recv(2), write(2), send(2) といった関数は、こちらが要求した全データを読み書きする必要はない。 もし全データを読み書きするなら、それはトラフィックの負荷が小さく、 ストリームが速い場合だろう。この条件は常に満たされるとは限らない。 これらの関数が頑張っても 1 バイトしか送受信できないような場合も 考慮に入れてやらなければならない。

処理するデータ量が小さいことがはっきりとわかっている場合を除いて、 一度に 1 バイトずつ読み書きするようなことはしてはならない。 バッファの許すかぎりのデータをまとめて読み書きしないと、 非常に効率が悪い。下記の例ではバッファは 1024 バイトにしているが、 このサイズを大きくするのは簡単だろう。

read(2), recv(2), write(2), send(2) などの関数や select() コールは、 errno を EINTR や EAGAIN (EWOULDBLOCK) にして -1 を返すことがある。 このような結果に対して適切に対応してやらなければならない (上記の例ではしていない)。 書いているプログラムがシグナルを受ける予定がなければ、 EINTR が返されることはあまり考えられない。 書いているプログラムで非ブロック I/O をセットしていない場合は、 EAGAIN が返されることはないだろう。

決して、引き数に長さ 0 のバッファを指定して read(2), recv(2), write(2), send(2) を呼び出してはならない。

read(2), recv(2), write(2), send(2) が 7. に示した以外のエラーで失敗した場合や、 入力系の関数の一つがファイル末尾を表す 0 を返した場合は、 そのディスクリプタをもう一度 select に渡してはならない。 下記の例では、そのディスクリプタをただちにクローズし、 そこには -1 をセットして、 それが集合に含まれ続けるのを許さないようにしている。

タイムアウトの値は select() を呼ぶたびに初期化すべきである。 OS によっては timeout 構造体が変更される場合があるからである。 但し、 pselect() は自分の timeout 構造体を変更することはない。

select() はファイルディスクリプタ集合を変更するので、 select() がループの中で使用されている場合には、呼び出しを行う前に毎回 ディスクリプタ集合を初期化し直さなければならない。

https://ja.manpages.org/fd_clr/2

参考記事

https://www.infra.jp/programming/network_programming_3.html

Clone this wiki locally