Cプログラミング
 Computer >> コンピューター >  >> プログラミング >> Cプログラミング

C言語のselect()システムコールでマスターするノンブロッキングI/O

C言語のselect()システムコールでマスターするノンブロッキングI/O

ファイルの読み書きにおいて、I/O操作が果たす役割は非常に重要です。しかし、従来の(ブロッキング型の)I/O操作では、処理が完了するまでプログラムが待ち状態になり、動作全体が遅延するという問題がありました。この課題を解決するのがノンブロッキングI/Oです。ノンブロッキングI/Oを利用すれば、I/O処理の進行中でもプログラムは動作を続けられます。

C言語でノンブロッキングI/Oを実現する際によく使われるのが「select」関数です。select関数を使うと、ソケットやファイルハンドルなど複数のファイルディスクリプタを同時に監視し、読み書きの準備が整っているか、エラーが発生していないかを確認できます。これにより、プログラムの実行を止めることなく複数のI/Oタスクを効率的に管理でき、複数のI/Oリソースの状態を継続的にチェックする仕組みを実現できます。

本記事では、C言語のselect関数を使ってノンブロッキングI/Oを実装する方法を解説します。selectの基本的な使い方に加え、実際のサンプルコードを通じてその応用例を紹介します。

select関数とは?

select関数は、C言語でノンブロッキングI/Oを実装するための強力なツールです。ソケットやファイルハンドルなど、複数のファイルディスクリプタを監視し、それらが読み取り・書き込みの準備状態にあるかどうかを調べることができます。

select関数は、次の3種類のファイルディスクリプタ集合を受け取ります。

  • 読み取り集合(read set):読み取り可能になったかを監視したいディスクリプタ
  • 書き込み集合(write set):書き込み可能になったかを監視したいディスクリプタ
  • 例外集合(exception set):例外状態の発生を監視したいディスクリプタ

これらの集合を使うことで、どのディスクリプタをどの操作に対して監視するかを柔軟に指定できます。さらに、タイムアウト値を渡すことで、イベント発生を待つ最大時間も設定可能です。監視対象のいずれかのディスクリプタでイベントが発生するか、タイムアウト時間が経過すると、selectは制御を返し、準備ができているディスクリプタに関する情報を提供します。この仕組みにより、プログラムの実行をブロックすることなくI/O操作を効率的に行えるため、複数のI/O処理を扱うのに適しています。

select関数のメリット

  • 接続ごとにスレッドを用意する必要がなく、リソース消費を抑えながら複数のI/O操作を効率的に処理できる
  • 単一のプロセス内で複数のI/Oリソースの状態を一元的に監視できる

select関数のデメリット

  • 監視できるファイルディスクリプタの最大数がOSによって制限されることが多い(多くの環境ではFD_SETSIZE=1024)
  • 監視対象のディスクリプタ数が増えるほど、パフォーマンスが低下しやすい

サンプルコード

以下は、select関数を使って2つのファイルを監視するC言語のサンプルプログラムです。

#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h> /* strlen を使うために必要 */

int main()
{
    /* 監視対象のファイルディスクリプタ */
    int fd1, fd2;
    /* ファイルを開き、非ブロッキングモードに設定 */
    fd1 = open("file1.txt", O_RDONLY | O_NONBLOCK);
    fd2 = open("file2.txt", O_WRONLY | O_NONBLOCK);
    fd_set read_fds, write_fds;   /* ファイルディスクリプタ集合 */
    struct timeval timeout;       /* select のタイムアウト */

    while (1)
    {
        FD_ZERO(&read_fds);   /* 読み取り集合をクリア */
        FD_ZERO(&write_fds);  /* 書き込み集合をクリア */

        FD_SET(fd1, &read_fds);   /* fd1 を読み取り集合に追加 */
        FD_SET(fd2, &write_fds);  /* fd2 を書き込み集合に追加 */
        timeout.tv_sec = 4;       /* タイムアウトを4秒に設定 */
        timeout.tv_usec = 0;
        int ready_fds = select(fd2 + 1, &read_fds, &write_fds, NULL, &timeout);
        if (ready_fds == -1) {
            perror("select");
            exit(EXIT_FAILURE);
        }
        else if (ready_fds == 0) {
            printf("Timeout occurred\n");
        }
        else {
            if (FD_ISSET(fd1, &read_fds)) {
                /* fd1 が読み取り可能 */
                char buffer[100];
                ssize_t bytesRead = read(fd1, buffer, sizeof(buffer) - 1);
                if (bytesRead > 0) {
                    buffer[bytesRead] = '\0';
                    printf("Read from file1.txt: %s \n", buffer);
                }
            }

            if (FD_ISSET(fd2, &write_fds)) {
                /* fd2 が書き込み可能 */
                const char* message = "Good morning";
                ssize_t bytesWritten = write(fd2, message, strlen(message));
                if (bytesWritten > 0) {
                    printf("Wrote to file2.txt: %s \n", message);
                }
            }
        }
    }
    /* ファイルディスクリプタをクローズ */
    close(fd1);
    close(fd2);
    return 0;
}

実行結果:

Wrote to file2.txt: Good morning
Wrote to file2.txt: Good morning
Wrote to file2.txt: Good morning
Wrote to file2.txt: Good morning
Timeout occurred

コードの解説

このプログラムでは、C言語のselect関数を使ってノンブロッキングI/Oを実装し、「file1.txt」と「file2.txt」の2つのファイルを監視しています。両ファイルともO_NONBLOCKフラグ付きで開かれているため、読み書きの完了を待つことなくプログラムは実行を続けられます。

メインループでは、select関数を使って指定したタイムアウト期間(4秒)内にファイルへ何らかのアクティビティがあるかどうかを確認します。タイムアウト期間内にアクティビティがなければ「Timeout occurred」と表示するだけです。アクティビティがあった場合は、FD_ISSETマクロでどちらのファイルに変化があったかを判定します。「file1.txt」が読み取り可能になっていれば内容を読み込んで表示し、「file2.txt」が書き込み可能になっていれば「Good morning」というメッセージを書き込みます。

プログラムはwhile(1)による無限ループで構成されているため、外部から終了させられるまでファイルの監視を続けます。なお、close()によるファイルディスクリプタのクローズはループを抜けた後に実行されるよう記述されていますが、このサンプルではループが無限に続くため、実際には終了シグナルなどで中断される点に注意してください。

まとめ

C言語のselect関数は、ノンブロッキングI/O操作を実装するための優れたソリューションを提供します。複数のファイルディスクリプタを一度に監視できるため、プログラムの実行をブロックすることなく複数のI/Oタスクを効率的に処理できます。ただし、監視できるディスクリプタ数の上限や、大量のディスクリプタを扱う際のパフォーマンス低下といった弱点も理解しておく必要があります。こうした制約はあるものの、select関数はCプログラムにおけるノンブロッキングI/O管理の基本として、今なお有効な選択肢と言えるでしょう。

著者について

C言語のselect()システムコールでマスターするノンブロッキングI/O

Bamdeb Ghosh

Bamdeb Ghoshは、ワイヤレスネットワーク分野で豊富な実務経験を持つエンジニアです。Wiresharkを活用したワイヤレス/有線ネットワークのキャプチャ解析の専門家であり、Android、Bluetooth、Linuxコマンド、Pythonにも精通しています。サイト:wifisharks.com

  1. C言語で行列をジグザグ(波状)形式に出力する方法

    行数と列数を持つ行列 mat[row][col] が与えられたとき、その行列を下の図のようにジグザグ(波状)形式で出力する必要があります。 期待される出力は以下のとおりです。 Output: 10 20 40 70 50 30 60 80 90 この問題に対しては、行列を対角線に沿って走査し、1本の対角線の要素を出力し終えるたびに走査方向を反転させるというシンプルなアプローチを採用しています。 アルゴリズム 開始 ステップ1:k = 3、l = 3 を宣言して設定する ステップ2:行列 mat[][3] を宣言する ステップ3:row = 0、col = 0、flag = false を宣言

  2. C言語で実装するFCFSスケジューリング:平均待ち時間とターンアラウンド時間の計算方法

    n個のプロセスP1、P2、P3、…、Pnとそれぞれのバーストタイム(CPU実行時間)が与えられたとき、FCFS(First Come, First Served)CPUスケジューリングアルゴリズムを用いて平均待ち時間と平均ターンアラウンド時間を求めるのが本記事のテーマです。 待ち時間とターンアラウンド時間とは? ターンアラウンド時間:プロセスの投入から完了までの時間間隔です。ターンアラウンド時間 = プロセスの完了時刻 − プロセスの投入時刻 待ち時間:ターンアラウンド時間からバーストタイムを差し引いた時間です。待ち時間 = ターンアラウンド時間 − バーストタイム FCFSスケジューリン