Linux I/O多路復用におけるpoll関数の詳細とサーバー実装

Linux環境におけるI/O多路復用技術の一つであるpollは、複数のファイル記述子(ファイルディスクリプタ)の状態変化を効率的に監視するためのシステムコールです。selectの制限を克服し、より柔軟なネットワークプログラミングを可能にします。

pollシステムの概要

pollは、アプリケーションが多数の入出力チャネル(ソケット、パイプ、デバイスファイルなど)を同時に待ち受ける際に使用されます。各チャネルに対して「読み取り可能」「書き込み可能」「エラー発生」といった特定のイベントを指定し、それらが準備完了状態になるまでプロセスを効率的に待機させます。

#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

引数の詳細は以下の通りです:

  • fds: struct pollfd型の配列へのポインタ。各要素に監視対象のFDと関心のあるイベントを設定します。
  • nfds: fds配列の要素数(監視するディスクリプタの数)。
  • timeout: 待機時間をミリ秒単位で指定。-1は無限待機、0は即時復帰(ポーリング)を意味します。

pollfd構造体とイベント分離

pollの最大の特徴は、ユーザーが要求するイベントと、カーネルから返される結果が別々のフィールドに分かれている点です。

struct pollfd {
    int   fd;         /* 監視対象のファイル記述子 */
    short events;     /* アプリケーションが要求するイベント(ビットマスク) */
    short revents;    /* 実際に発生したイベント(カーネルからの返却値) */
};

selectでは呼び出しのたびに関心のあるディスクリプタ集合をリセットする必要がありますが、pollではeventsフィールドを維持したまま、カーネルがreventsのみを書き換えるため、構造の管理が容易になります。

pollとselectの比較

pollselectはどちらも複数のI/Oを監視しますが、いくつかの決定的な違いがあります。

  • ファイル記述子の制限: selectは通常1024(FD_SETSIZE)というハードリミットがありますが、pollは配列のサイズを任意に設定できるため、システムリソースが許す限り数に制限はありません。
  • データ構造: selectはビットマップ(fd_set)を使用しますが、pollは構造体配列を使用します。
  • 効率性: 大規模な接続環境では両者とも線形走査(O(N))が必要ですが、pollの方がAPIとしての使い勝手が良く、ビット操作のオーバーヘッドが少ない傾向にあります。

pollを用いたマルチクライアントエコーサーバーの実装

以下に、pollを利用した基本的なマルチクライアント対応エコーサーバーの実装例を示します。

1. ネットワークソケットのラップ(Socket.hpp)

#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

class TcpSocket {
public:
    TcpSocket() : _listen_fd(-1) {}
    
    void Create() {
        _listen_fd = socket(AF_INET, SOCK_STREAM, 0);
        if (_listen_fd < 0) exit(1);
    }

    void Bind(uint16_t port) {
        struct sockaddr_in addr;
        memset(&addr, 0, sizeof(addr));
        addr.sin_family = AF_INET;
        addr.sin_port = htons(port);
        addr.sin_addr.s_addr = INADDR_ANY;
        if (bind(_listen_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) exit(2);
    }

    void Listen() {
        if (listen(_listen_fd, 10) < 0) exit(3);
    }

    int Accept(std::string* ip, uint16_t* port) {
        struct sockaddr_in client_addr;
        socklen_t len = sizeof(client_addr);
        int client_fd = accept(_listen_fd, (struct sockaddr*)&client_addr, &len);
        if (client_fd >= 0) {
            *ip = inet_ntoa(client_addr.sin_addr);
            *port = ntohs(client_addr.sin_port);
        }
        return client_fd;
    }

    int GetFd() const { return _listen_fd; }
    void Close() { if (_listen_fd != -1) close(_listen_fd); }

private:
    int _listen_fd;
};

2. サーバー本体の実装(PollServer.hpp)

#pragma once
#include "Socket.hpp"
#include <vector>
#include <poll.h>

#define MAX_CLIENTS 128
#define UNUSED_FD -1

class PollServer {
public:
    PollServer(uint16_t port) : _port(port) {
        for (int i = 0; i < MAX_CLIENTS; ++i) {
            _poll_set[i].fd = UNUSED_FD;
            _poll_set[i].events = 0;
        }
    }

    void Init() {
        _socket.Create();
        _socket.Bind(_port);
        _socket.Listen();
        
        // 最初の要素にListen用ソケットを登録
        _poll_set[0].fd = _socket.GetFd();
        _poll_set[0].events = POLLIN;
    }

    void Start() {
        while (true) {
            int active_count = poll(_poll_set, MAX_CLIENTS, 5000);
            if (active_count < 0) break;
            if (active_count == 0) continue;

            HandleEvents();
        }
    }

private:
    void HandleEvents() {
        for (int i = 0; i < MAX_CLIENTS; ++i) {
            if (_poll_set[i].fd == UNUSED_FD) continue;

            if (_poll_set[i].revents & POLLIN) {
                if (_poll_set[i].fd == _socket.GetFd()) {
                    AddNewClient();
                } else {
                    ProcessMessage(i);
                }
            }
        }
    }

    void AddNewClient() {
        std::string ip;
        uint16_t port;
        int new_fd = _socket.Accept(&ip, &port);
        if (new_fd < 0) return;

        bool full = true;
        for (int i = 1; i < MAX_CLIENTS; ++i) {
            if (_poll_set[i].fd == UNUSED_FD) {
                _poll_set[i].fd = new_fd;
                _poll_set[i].events = POLLIN;
                std::cout << "New connection: " << ip << ":" << port << std::endl;
                full = false;
                break;
            }
        }
        if (full) close(new_fd);
    }

    void ProcessMessage(int index) {
        char buf[1024];
        ssize_t s = read(_poll_set[index].fd, buf, sizeof(buf)-1);
        if (s > 0) {
            buf[s] = 0;
            std::cout << "Received: " << buf << std::endl;
            write(_poll_set[index].fd, buf, s); // Echo back
        } else {
            std::cout << "Client disconnected" << std::endl;
            close(_poll_set[index].fd);
            _poll_set[index].fd = UNUSED_FD;
        }
    }

    TcpSocket _socket;
    uint16_t _port;
    struct pollfd _poll_set[MAX_CLIENTS];
};

パフォーマンス上の留意点

pollselectのFD上限問題を解決しますが、監視するディスクリプタが増えるにつれて、カーネルとユーザー空間の間でのデータ転送量が増加します。また、返却された後にどのFDが準備完了したかを特定するために配列全体をスキャンする必要があるため、数千単位の超大規模な同時接続を処理する場合には、epollなどのより高度な通知機構が推奨されます。

タグ: linux C++ Network-Programming poll IO-Multiplexing

5月17日 13:03 投稿