📚

select関数を用いた標準入力の監視【Linux / C言語】

2021/06/19に公開

はじめに

野暮用でローカル環境で動作する簡単なソケット通信プログラムを作成することになったのですが、その際にselect関数の使い方でつまづいたのでその備忘録です。

最初は作成したソケット通信プログラムをそのまま解説するような形で記事を書こうと思ったのですが、試しに項目を書き出してみたらトンデモナイ量になったのでちょっとダウンサイジング。

標準入力の監視という方向でselect関数を使ったサンプルプログラムを示しながら軽く解説的なものを書いてあります。

気力が湧いたらソケットの制御をselect関数で行うバージョンについても記事を書こうと思います。

使用環境

プログラムの動作確認にはcygwinを利用しました。何をどうやってインストールしたのかは記憶の彼方へ飛んでしまったのですが、どうやらmintty 3.0.6を使用している模様。ともかくLinux環境があれば動くと思います(ラズパイとか)。

  • OS: windows 10 home
  • エディター: visual studio code(ver.1.57.1)
  • コンパイル, 実行: cygwin(mintty 3.0.6)

サンプルプログラム

select関数を使った標準入力を監視するプログラムはこんな感じ。

#include <stdio.h>
#include <stdlib.h>
#include <sys/select.h>

int main(void){
    int fd = 0;
    fd_set rfds;
    struct timeval tv;
    char* inputval;
    int retval;

    FD_ZERO(&rfds);
    FD_SET(fd, &rfds);
    tv.tv_sec = 5;
    tv.tv_usec = 500000;

    retval = select(fd + 1, &rfds, NULL, NULL, &tv);

    if(retval < 0){
        perror("select()");
    }
    else if(retval > 0){
        scanf("%s", inputval);
        printf("input: %s\n", inputval);
    }
    else{
        printf("timeout\n");
    }
    return (EXIT_SUCCESS);
}

解説

最初にselect関数の仕様(引数など)と大まかな動作について私なりの解釈を説明した後、サンプルプログラムについてその動作を説明していきます。

select関数の仕様と動作

select関数は以下のような引数と返り値を持っています。また、FD(ファイルディスクリプタ)の集合を操作するためのマクロがあります。

#include <sys/select.h>

int select(int nfds, fd_set *readfds, fd_set *writefds,
            fd_set *exceptfds, struct timeval *timeout);

void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);

select関数の引数についてざっくりと説明していきます。

  • int nfds
    監視するFDのうち、最大の値+1を指定する
    つまり、readfds, writefds, exceptfdsに含まれるFDの中の最大値+1
  • fd_set *readfds
    読み込み可能かどうかを監視したいFDの集合
  • fd_set *writefds
    書き込み可能かどうかを監視したいFDの集合
  • fd_set *exceptfds
    例外が発生したかどうかを監視したいFDの集合
  • struct timeval *timeout
    タイムアウトの時間

次はマクロについてざっくりと説明していきます。

  • void FD_CLR(int fd, fd_set *set);
    指定したFDの集合から特定のFDを削除する
  • int FD_ISSET(int fd, fd_set *set);
    指定したFDの集合に特定のFDが存在するかどうかを判別する
  • void FD_SET(int fd, fd_set *set);
    指定したFDの集合に特定のFDをセットする
  • void FD_ZERO(fd_set *set);
    指定したFDの集合を空にする

次はselect関数の動作について説明していきます。

select関数はFDの集合を監視(ここでブロッキングが発生)、集合内の要素にアクション(変化)があった場合にブロッキングを解いて動作を終了する...というような動作をする関数です。

例えば、FDの集合にソケットを数個入れた状態でselect関数を実行すると、いずれかのソケットに対してwrite関数等でデータ受信が発生するまで待機(ブロッキング)する...という動作ができるということです。

ただ、アクションがあるまでずっとブロッキングするというのは少々不便な場合があるので、select関数にはタイムアウト機能が付いています。一定時間が経過すると、FDのアクションがあるなしに関わらずselect関数の動作を終了させることができます。

よって、select関数は

  • FDにアクションが発生する
  • 一定時間が経過する(タイムアウト)
  • エラーが発生する

のいずれかが起こるまでFDを監視(ブロッキング)する動作を行う関数、ということになります。

画像で説明するとこんな感じ。

次にselect関数と合わせて使うマクロについて説明していきます。

先程から説明で「FDの集合」という言葉が多く登場していると思うのですが、マクロは主にこの「FDの集合」に対して操作を行うためのものになります。

それぞれのマクロがどのような役割を持っているのかは先述した通りなので、ここではマクロをどのように組み合わせて使うのかについて説明していきます。

マクロの組み合わせについて説明をする前に、マクロの動作についておさらいをすると

  • FD_ZERO(): FDの集合内の要素を全て削除する
  • FD_SET(): FDの集合にFDを要素として追加する
  • FD_ISSET(): FDの集合に特定のFDが要素として存在するかを判別する

という動作を行うと説明したと思います。

よって例えば、FD_ZERO()FD_SET()select()FD_ISSET()というような組み合わせで処理をループさせると、あるFD(の集合)に対して継続的にそのアクションを確認するような処理を作成できるということになります。

具体的な使い方としては、サーバと接続しているクライアントのソケットをFD_ZERO()FD_SET()select()FD_ISSET()で繰り返し処理することにより、サーバから届いたデータを繰り返し受け取るような処理が作成できます。

画像で説明するとこんな感じ。

サンプルプログラムの動作

大まかな区切りごとに説明していきます。

#include <stdio.h>
#include <stdlib.h>
#include <sys/select.h>

今回使用するライブラリは3つ。

毎度おなじみstdio.hEXIT_SUCCESSを使うためのstdlib.h(正直return 0;でもokなのであってもなくてもいい)。

今回重要なのはsys/select.h。ここにselect関数やマクロが詰まっています。環境によっては別のライブラリを読み込まなきゃいけないそうなので、必要な人は記事最後にある参考資料を漁ってみてください。

int fd = 0;
fd_set rfds;
struct timeval tv;
char* inputval;
int retval;

ここでは主に変数や構造体の作成をしています。

int fdは標準入力に割り当てられたFDを代入するための変数。一般的に標準入力には0, 標準出力には1, 標準エラー出力には2がそれぞれFDとして割り当てられています(もしかしたら環境によっては変わるかも?)。

fd_set rfdsはFDの集合を表す構造体。この構造体にFD_SET()を使ってfdを格納することになります。

struct timeval tvはselect関数で使うタイムアウト値を設定するための構造体。秒とミリ秒を指定することができます。

char* inputvalは標準入力に入力された文字列を格納するための変数。

int retvalはselect関数の返り値を格納するための変数。retvalの値をif文で判別することにより、select関数がエラーを吐いたのかタイムアウトを起こしたのかなどがわかります。

FD_ZERO(&rfds);
FD_SET(fd, &rfds);
tv.tv_sec = 5;
tv.tv_usec = 500000;

ここでは変数や構造体に値を格納しています。

FD_ZERO(&rfds)FD_SET(fd, &rfds)でFDの集合を初期化、標準入力のFD(fd)を集合にセットしています。

tv.tv_sec = 5はselect関数のタイムアウト値(秒)です。今回はタイムアウトを5.5秒にしたいので5を格納しています。

tv.tv_usec = 500000はselect関数のタイムアウト値(マイクロ秒)です。今回はタイムアウトを5.5秒にしたいので500000を格納しています(マイクロは10^(-6)なので0.5秒*10^(-6) = 500000)。

retval = select(fd + 1, &rfds, NULL, NULL, &tv);

ここでselect関数を実行します。

nfdsにはfd + 1を渡していますが、もし他にもFDを扱う場合にはこれよりも前にFDの最大値を求めるような処理を行い、その結果を渡す必要があります(例えば複数のソケットを扱う場合)。

今回writefdsexceptfdsは使用しないのでNULLを渡しておきます。

&を忘れないように注意!(引数がfd_set *readfdsのように定義されているのをお忘れなく)

if(retval < 0){
    perror("select()");
}
else if(retval > 0){
    scanf("%s", inputval);
    printf("input: %s\n", inputval);
}
else{
    printf("timeout\n");
}
return (EXIT_SUCCESS);

select関数が動作を終了したときの処理です。

select関数の返り値がretval < 0になった場合(つまり、-1が返ってきた場合)は、エラーが発生したことを表しているのでperror()で詳細を表示します。

select関数の返り値がretval > 0になった場合は、標準入力に入力が発生したことを表しているのでscanf()でバッファの中身を取得しprintf()で表示します。もし複数のFDを監視している場合はこのelse ifのブロックがFD_ISSET()の使い所です。

select関数の返り値がretval < 0でもretval > 0でもない場合(つまり、retval == 0)は、タイムアウトしたことを表しているのでその旨をprintf()で表示します。

最後はreturn (EXIT_SUCCESS)で正常終了。

おわりに

以上、select関数の使い方でした。

私と似たようなところでつまずいている人の参考になったなら幸いです。

蛇足なのですが、select関数の他にもFDを同時に扱えるような関数があるそうなので、余裕がある人はselect関数を使う前に色々と調べてみてください(epoll関数とか)。

ここまで読んでくださりありがとうございました。

参考資料

この記事を書くにあたって参考にさせて頂いたサイトの一覧です。

select関数とマクロについて書かれている日本語サイトです。

select関数とマクロについて書かれている英語サイトです。
英語がある程度読める人は日本語サイトと合わせて読むと理解が深まると思います(日本語サイトには載っていない内容がチラホラあります)。

select関数とrecv関数を使ったデータ受信(ソケット通信プログラム)について書かれているサイトです。
こっち読んだ方が早い。

Discussion