Chapter 2: Epoll Deep Dive

はじめに

前章で学んだselect/pollの制限を克服するため、Linuxは2002年にepoll(event poll)を導入しました。epollは、大規模な接続を効率的に処理するために設計された、強力で柔軟なI/O多重化メカニズムです。

このチャプターでは、epollの基本から高度な使用方法まで、包括的に学習します。

学習目標

このチャプターを完了すると、以下のことができるようになります:

  • epollの基本的な3つのシステムコールを理解する
  • エッジトリガーとレベルトリガーの違いを説明できる
  • epollの内部データ構造を理解する
  • 高性能なepollベースのサーバーを実装できる
  • epollの落とし穴を回避できる
  • 他のプラットフォームのI/O多重化と比較できる
  • 1. epollの概要

    1.1 epollとは

    epoll(event poll)は、Linuxカーネル2.5.44(2002年)で導入されたI/O多重化APIです。select/pollの性能問題を解決するために設計されました。

    epollの主な特徴:

    select/pollとの違い:
    
    select/poll:
    - 毎回全FDをスキャン: O(n)
    - カーネル/ユーザー間のコピー
    - FDセットの再構築が必要
    
    epoll:
    - イベントベースの通知: O(1)*
    - ステートフル(カーネルが状態を保持)
    - 準備完了したFDのみ返す
    
    *: アクティブな接続に対して
    

    1.2 なぜepollが必要なのか

    パフォーマンス比較(10,000接続、100がアクティブ):
    
    select/poll:
      1. 10,000個のFDをカーネルに渡す
      2. カーネルが10,000個すべてをチェック
      3. 100個の準備完了FDを見つける
      4. 10,000個のFDをユーザーに返す
      5. ユーザーが10,000個をスキャンして100個を見つける
      → 合計: 30,000回以上の操作
    
    epoll:
      1. 一度だけ10,000個のFDを登録(初期化時)
      2. カーネルが100個の準備完了FDを直接返す
      3. ユーザーが100個のイベントを処理
      → 合計: 約100回の操作
    
    パフォーマンス向上: 約300倍
    

    2. epollの3つの基本システムコール

    epollは3つの主要なシステムコールで構成されています:

    2.1 epoll_create / epoll_create1

    epollインスタンスを作成します。

    const std = @import("std");
    const os = std.os;
    const linux = os.linux;
    
    pub fn createEpoll() !i32 {
        // epoll_create1を使用(推奨)
        // EPOLL_CLOEXECフラグで自動的にclose-on-execを設定
        const epfd = linux.epoll_create1(linux.EPOLL_CLOEXEC);
    
        if (epfd < 0) {
            return error.EpollCreateFailed;
        }
    
        std.debug.print("Epollインスタンス作成: fd={}\n", .{epfd});
        return epfd;
    }
    
    // 古いAPI(非推奨だが互換性のため残されている)
    pub fn createEpollOld() !i32 {
        // sizeパラメータは無視される(歴史的な理由で残存)
        const epfd = linux.epoll_create(1);
        return epfd;
    }
    

    epoll_create1のフラグ:

    フラグ 説明
    `EPOLL_CLOEXEC` exec時に自動的にクローズ(推奨)
    0 フラグなし

    2.2 epoll_ctl

    epollインスタンスにファイルディスクリプタを登録、変更、削除します。

    const std = @import("std");
    const os = std.os;
    const linux = os.linux;
    
    /// ファイルディスクリプタをepollに追加
    pub fn epollAdd(epfd: i32, fd: i32, events: u32) !void {
        var event = linux.epoll_event{
            .events = events,
            .data = linux.epoll_data{ .fd = fd },
        };
    
        const result = linux.epoll_ctl(
            epfd,
            linux.EPOLL_CTL_ADD,
            fd,
            &event
        );
    
        if (result < 0) {
            return error.EpollCtlAddFailed;
        }
    
        std.debug.print("epoll追加: fd={}, events=0x{x}\n", .{fd, events});
    }
    
    /// ファイルディスクリプタのイベントを変更
    pub fn epollModify(epfd: i32, fd: i32, events: u32) !void {
        var event = linux.epoll_event{
            .events = events,
            .data = linux.epoll_data{ .fd = fd },
        };
    
        const result = linux.epoll_ctl(
            epfd,
            linux.EPOLL_CTL_MOD,
            fd,
            &event
        );
    
        if (result < 0) {
            return error.EpollCtlModFailed;
        }
    
        std.debug.print("epoll変更: fd={}, events=0x{x}\n", .{fd, events});
    }
    
    /// ファイルディスクリプタをepollから削除
    pub fn epollDelete(epfd: i32, fd: i32) !void {
        const result = linux.epoll_ctl(
            epfd,
            linux.EPOLL_CTL_DEL,
            fd,
            null  // eventパラメータは無視される
        );
    
        if (result < 0) {
            return error.EpollCtlDelFailed;
        }
    
        std.debug.print("epoll削除: fd={}\n", .{fd});
    }
    

    epoll_ctlの操作:

    操作 説明
    `EPOLL_CTL_ADD` 新しいFDを追加
    `EPOLL_CTL_MOD` 既存のFDのイベントを変更
    `EPOLL_CTL_DEL` FDを削除

    epollイベントフラグ:

    イベント 説明
    `EPOLLIN` 読み込み可能
    `EPOLLOUT` 書き込み可能
    `EPOLLERR` エラー発生
    `EPOLLHUP` ハングアップ(切断)
    `EPOLLET` エッジトリガーモード
    `EPOLLONESHOT` ワンショットモード
    `EPOLLRDHUP` ピアが書き込み側をシャットダウン

    2.3 epoll_wait

    イベントを待機します。

    const std = @import("std");
    const os = std.os;
    const linux = os.linux;
    
    pub fn epollWait(epfd: i32, max_events: usize, timeout_ms: i32) ![]linux.epoll_event {
        const allocator = std.heap.page_allocator;
        var events = try allocator.alloc(linux.epoll_event, max_events);
    
        const nfds = linux.epoll_wait(
            epfd,
            events.ptr,
            @intCast(max_events),
            timeout_ms
        );
    
        if (nfds < 0) {
            allocator.free(events);
            return error.EpollWaitFailed;
        }
    
        if (nfds == 0) {
            // タイムアウト
            std.debug.print("epoll_wait: タイムアウト\n", .{});
            allocator.free(events);
            return &[_]linux.epoll_event{};
        }
    
        std.debug.print("epoll_wait: {d} イベント\n", .{nfds});
        return events[0..@intCast(nfds)];
    }
    
    /// イベントループの例
    pub fn eventLoop(epfd: i32) !void {
        const max_events = 64;
        const timeout_ms = 5000;  // 5秒
    
        while (true) {
            const events = try epollWait(epfd, max_events, timeout_ms);
            defer std.heap.page_allocator.free(events);
    
            for (events) |event| {
                const fd = event.data.fd;
    
                if (event.events & linux.EPOLLIN != 0) {
                    std.debug.print("読み込み可能: fd={}\n", .{fd});
                    try handleRead(fd);
                }
    
                if (event.events & linux.EPOLLOUT != 0) {
                    std.debug.print("書き込み可能: fd={}\n", .{fd});
                    try handleWrite(fd);
                }
    
                if (event.events & linux.EPOLLERR != 0) {
                    std.debug.print("エラー: fd={}\n", .{fd});
                    try handleError(fd);
                }
    
                if (event.events & linux.EPOLLHUP != 0) {
                    std.debug.print("切断: fd={}\n", .{fd});
                    try handleHangup(fd);
                }
            }
        }
    }
    
    fn handleRead(fd: i32) !void {
        // 実装は後述
    }
    
    fn handleWrite(fd: i32) !void {
        // 実装は後述
    }
    
    fn handleError(fd: i32) !void {
        // 実装は後述
    }
    
    fn handleHangup(fd: i32) !void {
        // 実装は後述
    }
    

    タイムアウト値:

    動作
    `-1` 無限に待機(イベントが来るまでブロック)
    `0` ノンブロッキング(即座に戻る)
    `> 0` 指定されたミリ秒待機

    3. エッジトリガー vs レベルトリガー

    epollの最も重要な概念の1つが、トリガーモードです。

    3.1 レベルトリガー(Level Triggered, LT)

    デフォルトモード。条件が満たされている限り、epoll_waitは繰り返しイベントを返します。

    レベルトリガーの動作:
    
    時刻    データ状態    epoll_wait    結果
    ----    ----------    ----------    ----
    T0      データなし    待機中...
    T1      100バイト→    戻る         EPOLLIN
    T2      100バイト     待機中...
    T3      (まだ100)     戻る         EPOLLIN (再通知!)
    T4      50バイト読む  待機中...
    T5      (残り50)      戻る         EPOLLIN (まだデータあり)
    T6      50バイト読む  待機中...
    T7      データなし    待機中...    (ブロック)
    

    // レベルトリガーの例
    pub fn levelTriggeredExample(epfd: i32, fd: i32) !void {
        // レベルトリガーで登録(デフォルト)
        try epollAdd(epfd, fd, linux.EPOLLIN);
    
        var buffer: [1024]u8 = undefined;
    
        while (true) {
            const events = try epollWait(epfd, 1, -1);
            defer std.heap.page_allocator.free(events);
    
            for (events) |event| {
                if (event.events & linux.EPOLLIN != 0) {
                    // 少しだけ読む(全部読まない)
                    const bytes_read = try os.read(event.data.fd, buffer[0..10]);
    
                    std.debug.print("読み込み: {d} バイト\n", .{bytes_read});
    
                    // まだデータが残っていれば、次のepoll_waitで再度通知される
                }
            }
        }
    }
    

    レベルトリガーの特徴:

  • 安全: データを読み忘れてもイベントが再通知される
  • シンプル: 従来のselect/pollと同じ動作
  • パフォーマンス: 一部読むと何度も通知される可能性
  • 3.2 エッジトリガー(Edge Triggered, ET)

    状態が変化したときのみイベントを通知します。

    エッジトリガーの動作:
    
    時刻    データ状態    epoll_wait    結果
    ----    ----------    ----------    ----
    T0      データなし    待機中...
    T1      100バイト→    戻る         EPOLLIN (変化!)
    T2      100バイト     待機中...    (ブロック - 変化なし)
    T3      (まだ100)     待機中...    (ブロック)
    T4      50バイト読む  待機中...    (ブロック)
    T5      (残り50)      待機中...    (ブロック - 未読でも通知なし!)
    T6      50バイト読む  待機中...
    T7      データなし    待機中...
    T8      新データ→     戻る         EPOLLIN (変化!)
    

    // エッジトリガーの例
    pub fn edgeTriggeredExample(epfd: i32, fd: i32) !void {
        // エッジトリガーで登録
        try epollAdd(epfd, fd, linux.EPOLLIN | linux.EPOLLET);
    
        // FDをノンブロッキングに設定(必須!)
        const flags = try os.fcntl(fd, os.F.GETFL, 0);
        _ = try os.fcntl(fd, os.F.SETFL, flags | os.O.NONBLOCK);
    
        var buffer: [1024]u8 = undefined;
    
        while (true) {
            const events = try epollWait(epfd, 1, -1);
            defer std.heap.page_allocator.free(events);
    
            for (events) |event| {
                if (event.events & linux.EPOLLIN != 0) {
                    // エッジトリガーでは全データを読み切る必要がある
                    while (true) {
                        const result = os.read(event.data.fd, &buffer);
    
                        if (result) |bytes_read| {
                            if (bytes_read == 0) {
                                // EOF
                                break;
                            }
                            std.debug.print("読み込み: {d} バイト\n", .{bytes_read});
                            // データ処理...
                        } else |err| {
                            if (err == error.WouldBlock) {
                                // 全データ読み切った
                                break;
                            }
                            return err;
                        }
                    }
                }
            }
        }
    }
    

    エッジトリガーの特徴:

  • 効率的: イベント通知が最小限
  • 複雑: すべてのデータを読み切る必要がある
  • ノンブロッキング必須: WouldBlockまで読む必要がある
  • リスク: データを読み残すと永遠に通知されない

3.3 トリガーモードの比較

レベルトリガー(LT):
┌─────────────────────────────────┐
│ 利点:                            │
│ - 実装が簡単                      │
│ - データを読み忘れても安全         │
│ - ブロッキングI/Oでも動作         │
│                                  │
│ 欠点:                            │
│ - 無駄な通知が多い可能性          │
│ - パフォーマンスがETより劣る      │
└─────────────────────────────────┘

エッジトリガー(ET):
┌─────────────────────────────────┐
│ 利点:                            │
│ - 最高のパフォーマンス            │
│ - イベント通知が最小限            │
│ - 大規模システムに適している      │
│                                  │
│ 欠点:                            │
│ - 実装が複雑                      │
│ - ノンブロッキングI/O必須         │
│ - バグが入りやすい                │
└─────────────────────────────────┘

4. 特殊なepollフラグ

4.1 EPOLLONESHOT

イベント後、自動的にFDを無効化します。マルチスレッド環境で便利です。

pub fn oneshotExample(epfd: i32, fd: i32) !void {
    // ONESHOTモードで登録
    try epollAdd(epfd, fd, linux.EPOLLIN | linux.EPOLLONESHOT);

    while (true) {
        const events = try epollWait(epfd, 1, -1);
        defer std.heap.page_allocator.free(events);

        for (events) |event| {
            if (event.events & linux.EPOLLIN != 0) {
                try handleRead(event.data.fd);

                // 処理完了後、再度有効化する必要がある
                try epollModify(epfd, event.data.fd, linux.EPOLLIN | linux.EPOLLONESHOT);
            }
        }
    }
}

EPOLLONESHOTの用途:

マルチスレッドサーバー:

Thread 1        Thread 2
   |               |
   v               v
epoll_wait()   epoll_wait()
   |               |
   |-- Event(fd=5) |
   |               |
process(fd=5)      |
   |               |
   |               |-- Event(fd=5) を受け取らない (ONESHOT)
   |               |
   |            待機中...
   |
epoll_ctl(MOD) で再有効化

4.2 EPOLLRDHUP

ピアが書き込み側をシャットダウンしたことを検出します。

pub fn rdhupExample(epfd: i32, fd: i32) !void {
    // RDHUPを有効化
    try epollAdd(epfd, fd, linux.EPOLLIN | linux.EPOLLRDHUP);

    while (true) {
        const events = try epollWait(epfd, 1, -1);
        defer std.heap.page_allocator.free(events);

        for (events) |event| {
            if (event.events & linux.EPOLLRDHUP != 0) {
                std.debug.print("ピアが書き込み側をクローズ\n", .{});
                // クリーンアップ処理
                try epollDelete(epfd, event.data.fd);
                os.close(event.data.fd);
            } else if (event.events & linux.EPOLLIN != 0) {
                try handleRead(event.data.fd);
            }
        }
    }
}

5. 完全なepollサーバーの例

5.1 基本的なエコーサーバー

const std = @import("std");
const os = std.os;
const linux = os.linux;
const net = std.net;

pub fn main() !void {
    // epollインスタンスを作成
    const epfd = try createEpoll();
    defer os.close(epfd);

    // リスニングソケットを作成
    const address = try net.Address.parseIp4("127.0.0.1", 8080);
    var server = net.StreamServer.init(.{});
    defer server.deinit();

    try server.listen(address);
    const server_fd = server.sockfd.?;

    // サーバーソケットをepollに追加
    try epollAdd(epfd, server_fd, linux.EPOLLIN);

    std.debug.print("Epollサーバー起動: {}\n", .{address});

    // イベントループ
    try runEventLoop(epfd, &server);
}

fn runEventLoop(epfd: i32, server: *net.StreamServer) !void {
    const max_events = 64;
    var events_buffer: [max_events]linux.epoll_event = undefined;

    while (true) {
        const nfds = linux.epoll_wait(
            epfd,
            &events_buffer,
            max_events,
            -1  // 無限待機
        );

        if (nfds < 0) {
            return error.EpollWaitFailed;
        }

        for (events_buffer[0..@intCast(nfds)]) |event| {
            const fd = event.data.fd;

            // サーバーソケットのイベント
            if (fd == server.sockfd.?) {
                try acceptNewConnection(epfd, server);
                continue;
            }

            // クライアントソケットのイベント
            if (event.events & linux.EPOLLIN != 0) {
                try handleClientRead(epfd, fd);
            }

            if (event.events & (linux.EPOLLERR | linux.EPOLLHUP) != 0) {
                try handleClientClose(epfd, fd);
            }
        }
    }
}

fn acceptNewConnection(epfd: i32, server: *net.StreamServer) !void {
    const connection = try server.accept();
    const client_fd = connection.stream.handle;

    // ノンブロッキングに設定
    const flags = try os.fcntl(client_fd, os.F.GETFL, 0);
    _ = try os.fcntl(client_fd, os.F.SETFL, flags | os.O.NONBLOCK);

    // epollに追加
    try epollAdd(epfd, client_fd, linux.EPOLLIN | linux.EPOLLET);

    std.debug.print("新しい接続: fd={}\n", .{client_fd});
}

fn handleClientRead(epfd: i32, fd: i32) !void {
    var buffer: [4096]u8 = undefined;

    while (true) {
        const result = os.read(fd, &buffer);

        if (result) |bytes_read| {
            if (bytes_read == 0) {
                // EOF
                try handleClientClose(epfd, fd);
                return;
            }

            std.debug.print("受信 (fd={}): {d} バイト\n", .{fd, bytes_read});

            // エコーバック
            _ = try os.write(fd, buffer[0..bytes_read]);
        } else |err| {
            if (err == error.WouldBlock) {
                // すべて読み切った
                return;
            }
            return err;
        }
    }
}

fn handleClientClose(epfd: i32, fd: i32) !void {
    std.debug.print("接続終了: fd={}\n", .{fd});
    try epollDelete(epfd, fd);
    os.close(fd);
}

fn createEpoll() !i32 {
    const epfd = linux.epoll_create1(linux.EPOLL_CLOEXEC);
    if (epfd < 0) {
        return error.EpollCreateFailed;
    }
    return epfd;
}

fn epollAdd(epfd: i32, fd: i32, events: u32) !void {
    var event = linux.epoll_event{
        .events = events,
        .data = linux.epoll_data{ .fd = fd },
    };

    const result = linux.epoll_ctl(epfd, linux.EPOLL_CTL_ADD, fd, &event);
    if (result < 0) {
        return error.EpollCtlAddFailed;
    }
}

fn epollDelete(epfd: i32, fd: i32) !void {
    const result = linux.epoll_ctl(epfd, linux.EPOLL_CTL_DEL, fd, null);
    if (result < 0) {
        return error.EpollCtlDelFailed;
    }
}

6. epollの内部構造

6.1 カーネルのデータ構造

epollはカーネル内で以下のデータ構造を使用します:

epollインスタンス:
┌─────────────────────────────────────┐
│ struct eventpoll {                  │
│   spinlock_t lock;                  │
│   struct rb_root rbr;      ←───┐   │  Red-Black Tree
│   struct list_head rdllist;←───┼─┐ │  Ready List
│   ...                           │ │ │
│ }                               │ │ │
└─────────────────────────────────┼─┼─┘
                                  │ │
    Red-Black Tree (登録FD)      │ │
    ┌──────────────────────┐     │ │
    │      fd=3            │◄────┘ │
    │    /      \          │       │
    │ fd=5      fd=10      │       │
    │         /    \       │       │
    │      fd=7   fd=15    │       │
    └──────────────────────┘       │
                                   │
    Ready List (準備完了FD)        │
    ┌──────────────────────┐       │
    │  fd=3 → fd=10 → ...  │◄──────┘
    └──────────────────────┘

Red-Black Tree:

  • 登録されたすべてのFDを管理
  • O(log n)の挿入/削除/検索
  • FDの重複登録を防ぐ

Ready List:

  • 準備完了したFDのリスト
  • epoll_waitはこのリストを返すだけ(O(1))
  • イベント発生時にカーネルが自動更新
  • 6.2 イベント通知の仕組み

    イベント発生時の処理フロー:
    
    1. デバイスドライバがデータ到着を検知
       ↓
    2. ドライバがwait queueをwake_up()
       ↓
    3. epollのコールバック関数が呼ばれる
       ↓
    4. FDをReady Listに追加
       ↓
    5. epoll_waitをブロックしているプロセスを起床
       ↓
    6. epoll_waitがReady Listを返す
    

    まとめ

    このチャプターでは、epollの基礎を学習しました:

  • 3つのシステムコール: epoll_create, epoll_ctl, epoll_wait
  • トリガーモード: レベルトリガー vs エッジトリガー
  • 特殊フラグ: EPOLLONESHOT, EPOLLRDHUP
  • 内部構造: Red-Black TreeとReady List
  • 実装例: 完全なエコーサーバー
  • 次のExplanationでは、epollのより深い詳細と最適化テクニックを学習します。

    参考資料

  • Linux man pages: epoll(7), epoll_create(2), epoll_ctl(2), epoll_wait(2)
  • The Linux Programming Interface (Michael Kerrisk)
  • Linuxカーネルソースコード: fs/eventpoll.c