Explanation 2: Epoll Deep Diveの深層解説

イントロダクション

このドキュメントでは、Chapter 2で学んだepollの概念をさらに深く掘り下げます。カーネル実装の詳細、パフォーマンス特性、実世界のユースケース、そしてクロスプラットフォームの比較について詳しく解説します。

1. epollのカーネル実装詳細

1.1 eventpoll構造体の完全な定義

// fs/eventpoll.c (Linuxカーネル)
struct eventpoll {
    /* epollインスタンスを保護するスピンロック */
    spinlock_t lock;

    /* epollのミューテックス(長時間の操作用) */
    struct mutex mtx;

    /* epoll_wait()でブロックされているプロセスの待機キュー */
    wait_queue_head_t wq;

    /* epollファイル自体のポーラー用待機キュー */
    wait_queue_head_t poll_wait;

    /* 準備完了ファイルのリスト */
    struct list_head rdllist;

    /* Red-Black Tree のルート(すべての監視FD) */
    struct rb_root rbr;

    /* ovflist: オーバーフローリスト
       epoll_wait実行中にイベントが発生した場合に使用 */
    struct epitem *ovflist;

    /* ユーザー構造体(リソース制限用) */
    struct user_struct *user;

    /* epollファイル自体のfile構造体 */
    struct file *file;

    /* ネストされたepoll検出用 */
    int visited;
    struct list_head visited_list_link;
};

1.2 epitem構造体(個々の監視FD)

struct epitem {
    /* Red-Black Treeノード */
    struct rb_node rbn;

    /* Ready Listノード */
    struct list_head rdllink;

    /* 次のepitem(ovflist用) */
    struct epitem *next;

    /* 監視対象のファイルディスクリプタ */
    struct epoll_filefd ffd;

    /* このepitemが所属するeventpoll */
    struct eventpoll *ep;

    /* イベントマスクとユーザーデータ */
    struct epoll_event event;

    /* ネストレベル(epoll of epoll) */
    int nwait;

    /* ポーリングテーブル */
    struct list_head pwqlist;
};

1.3 epoll_ctl の内部実装

// 疑似コード: epoll_ctlの主要な処理フロー
SYSCALL_DEFINE4(epoll_ctl, int, epfd, int, op, int, fd,
                struct epoll_event __user *, event)
{
    struct eventpoll *ep;
    struct epitem *epi;
    struct file *file, *tfile;
    int error;

    // 1. epollファイルディスクリプタの検証
    file = fget(epfd);
    if (!is_epoll_file(file)) {
        error = -EINVAL;
        goto error_return;
    }

    // 2. 監視対象ファイルの検証
    tfile = fget(fd);

    // 3. eventpoll構造体の取得
    ep = file->private_data;

    // 4. ミューテックスでロック
    mutex_lock(&ep->mtx);

    // 5. 操作に応じた処理
    switch (op) {
    case EPOLL_CTL_ADD:
        if (!epi) {
            // Red-Black Treeに新規追加
            epitem_insert(ep, &event, tfile, fd);
            error = 0;
        } else {
            error = -EEXIST;  // 既に登録済み
        }
        break;

    case EPOLL_CTL_DEL:
        if (epi) {
            // Red-Black Treeから削除
            error = ep_remove(ep, epi);
        } else {
            error = -ENOENT;  // 未登録
        }
        break;

    case EPOLL_CTL_MOD:
        if (epi) {
            // イベントマスクを更新
            event.events |= POLLERR | POLLHUP;
            error = ep_modify(ep, epi, &event);
        } else {
            error = -ENOENT;
        }
        break;
    }

    mutex_unlock(&ep->mtx);
    return error;
}

1.4 epoll_wait の内部実装

// 疑似コード: epoll_waitの主要な処理フロー
SYSCALL_DEFINE4(epoll_wait, int, epfd,
                struct epoll_event __user *, events,
                int, maxevents, int, timeout)
{
    struct eventpoll *ep;
    struct file *file;
    int error;

    // 1. epollファイルディスクリプタの検証
    file = fget(epfd);
    ep = file->private_data;

    // 2. イベント収集
    error = ep_poll(ep, events, maxevents, timeout);

    return error;
}

static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events,
                   int maxevents, long timeout)
{
    int res = 0;
    wait_queue_t wait;
    ktime_t expires;

fetch_events:
    spin_lock_irqsave(&ep->lock, flags);

    // Ready Listにイベントがあるかチェック
    if (!list_empty(&ep->rdllist)) {
        // イベントをユーザースペースに送信
        res = ep_send_events(ep, events, maxevents);
        if (res)
            return res;
    }

    // イベントなし、かつタイムアウト0なら即座に戻る
    if (timeout == 0) {
        spin_unlock_irqrestore(&ep->lock, flags);
        return 0;
    }

    // 待機キューに追加
    init_waitqueue_entry(&wait, current);
    __add_wait_queue_exclusive(&ep->wq, &wait);

    for (;;) {
        set_current_state(TASK_INTERRUPTIBLE);

        // イベント到着またはシグナル受信で起床
        if (!list_empty(&ep->rdllist) || !timeout)
            break;

        // スケジューラに制御を渡す
        timeout = schedule_timeout(timeout);
    }

    __remove_wait_queue(&ep->wq, &wait);
    set_current_state(TASK_RUNNING);

    // イベント収集を再試行
    goto fetch_events;
}

1.5 イベント発生時のコールバック

// デバイスドライバがwake_up()を呼ぶと、
// このコールバックが実行される
static int ep_poll_callback(wait_queue_t *wait, unsigned mode,
                             int sync, void *key)
{
    struct epitem *epi = ep_item_from_wait(wait);
    struct eventpoll *ep = epi->ep;

    spin_lock_irqsave(&ep->lock, flags);

    // イベントマスクのチェック
    if (key && !((unsigned long) key & epi->event.events))
        goto out_unlock;

    // Ready Listに未追加なら追加
    if (list_empty(&epi->rdllink)) {
        list_add_tail(&epi->rdllink, &ep->rdllist);
    }

    // epoll_wait()をブロックしているプロセスを起床
    if (waitqueue_active(&ep->wq))
        wake_up_locked(&ep->wq);

out_unlock:
    spin_unlock_irqrestore(&ep->lock, flags);
    return 1;
}

2. エッジトリガー vs レベルトリガーの詳細分析

2.1 カーネルでの実装の違い

// Ready Listへの追加処理(簡略化)
static int ep_poll_callback(...) {
    struct epitem *epi;
    struct eventpoll *ep;

    // イベント発生時に常に呼ばれる

    if (epi->event.events & EPOLLET) {
        // エッジトリガー: Ready Listに未追加の場合のみ追加
        if (list_empty(&epi->rdllink)) {
            list_add_tail(&epi->rdllink, &ep->rdllist);
        }
    } else {
        // レベルトリガー: 常にReady Listに追加
        // (既に追加済みでも問題ない)
        if (list_empty(&epi->rdllink)) {
            list_add_tail(&epi->rdllink, &ep->rdllist);
        }
    }

    wake_up(&ep->wq);
}

// イベント送信後の処理
static int ep_send_events(...) {
    // イベントをユーザーに送信

    for_each_ready_item(epi) {
        if (epi->event.events & EPOLLET) {
            // エッジトリガー: Ready Listから削除
            list_del_init(&epi->rdllink);
        } else {
            // レベルトリガー: 条件が満たされていればReady Listに残す
            if (still_has_data(epi)) {
                // Ready Listに残す(次回も通知)
            } else {
                list_del_init(&epi->rdllink);
            }
        }
    }
}

2.2 実際のデータフローの違い

シナリオ: 1000バイトのデータ到着

レベルトリガー:
─────────────────────────────────────────
時刻  カーネル   Ready List    アプリ動作
─────────────────────────────────────────
T0    0バイト   []            epoll_wait() [ブロック]
T1    1000↓     [fd]          戻る: EPOLLIN
T2    1000      [fd]          read(100) → 100バイト読む
T3    900       [fd]          epoll_wait()
T4    900       [fd]          戻る: EPOLLIN (まだデータあり!)
T5    900       [fd]          read(100) → 100バイト読む
...   ...       ...           ...
T12   100       [fd]          epoll_wait()
T13   100       [fd]          戻る: EPOLLIN
T14   100       [fd]          read(1000) → 100バイト読む
T15   0         []            epoll_wait() [ブロック]
─────────────────────────────────────────
epoll_wait呼び出し回数: 10回

エッジトリガー:
─────────────────────────────────────────
時刻  カーネル   Ready List    アプリ動作
─────────────────────────────────────────
T0    0バイト   []            epoll_wait() [ブロック]
T1    1000↓     [fd]          戻る: EPOLLIN
T2    1000      []            read() → 1000バイト (全部!)
T3    0         []            epoll_wait() [ブロック]
T4    新データ↓  [fd]          戻る: EPOLLIN
─────────────────────────────────────────
epoll_wait呼び出し回数: 1回(大幅削減)

注意: エッジトリガーでは、T2で全データを読み切らないと
      データが残っていてもT3以降でイベントが来ない!

2.3 エッジトリガーの正しい実装パターン

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

/// エッジトリガーの完全な実装例
pub fn edgeTriggeredReadLoop(fd: i32) !void {
    // バッファサイズは大きめに
    var buffer: [16384]u8 = undefined;
    var total_read: usize = 0;

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

        if (result) |bytes_read| {
            if (bytes_read == 0) {
                // EOF: 接続が閉じられた
                std.debug.print("EOF検出: 合計 {d} バイト読み込み\n", .{total_read});
                return;
            }

            total_read += bytes_read;

            // データ処理
            try processData(buffer[0..bytes_read]);

            // バッファが満杯だった場合、まだデータがある可能性
            if (bytes_read == buffer.len) {
                // 続けて読み込む
                continue;
            } else {
                // バッファに余裕があれば、データを読み切った
                std.debug.print("データ読み切り: {d} バイト\n", .{total_read});
                return;
            }
        } else |err| {
            if (err == error.WouldBlock) {
                // これが正常な終了条件
                std.debug.print("全データ読み切り: {d} バイト\n", .{total_read});
                return;
            }

            // その他のエラー
            std.debug.print("読み込みエラー: {}\n", .{err});
            return err;
        }
    }
}

fn processData(data: []const u8) !void {
    // データ処理ロジック
    std.debug.print("処理: {d} バイト\n", .{data.len});
}

2.4 よくある間違い

// 間違い1: エッジトリガーで全データを読まない
pub fn wrongEdgeTriggered1(fd: i32) !void {
    var buffer: [1024]u8 = undefined;

    // 一度だけ読む → 残りのデータは永遠に通知されない!
    const bytes_read = try os.read(fd, &buffer);
    try processData(buffer[0..bytes_read]);
    // ❌ 間違い: WouldBlockまで読むべき
}

// 間違い2: エッジトリガーでブロッキングI/O
pub fn wrongEdgeTriggered2(fd: i32) !void {
    // FDがブロッキングモードのまま
    var buffer: [1024]u8 = undefined;

    while (true) {
        const bytes_read = try os.read(fd, &buffer);  // ブロック!
        // ❌ 間違い: ノンブロッキングに設定すべき
    }
}

// 間違い3: レベルトリガーの誤解
pub fn wrongLevelTriggered(fd: i32) !void {
    // レベルトリガーでも、できるだけ多く読むべき
    var buffer: [1024]u8 = undefined;

    const bytes_read = try os.read(fd, &buffer);
    // 少しだけ読んで戻る → 無駄なepoll_wait呼び出し
    // ✓ 正解: できる限り読むべき(ブロッキングの範囲で)
}

3. パフォーマンス特性とスケーラビリティ

3.1 計算量の詳細分析

操作ごとの計算量:

epoll_create1():
  時間: O(1)
  メモリ: 約1KB(eventpoll構造体)

epoll_ctl(ADD):
  時間: O(log n)  - Red-Black Treeへの挿入
  メモリ: 約200バイト/FD(epitem構造体)

epoll_ctl(MOD):
  時間: O(log n)  - Red-Black Tree検索
  メモリ: 変化なし

epoll_ctl(DEL):
  時間: O(log n)  - Red-Black Treeからの削除
  メモリ: -200バイト/FD

epoll_wait():
  時間: O(1) + O(m)
        - O(1): Ready Listアクセス
        - O(m): m個のイベントをユーザーにコピー
  メモリ: 変化なし

n: 登録されているFDの総数
m: 準備完了したFDの数(通常 m << n)

3.2 実測パフォーマンス比較

ベンチマーク条件:
- 総接続数: 10,000
- アクティブ接続: 100
- イベント/秒: 10,000

select:
  CPU使用率: 90%
  メモリ: 約10MB
  レイテンシ: 10-50ms
  スループット: ~5,000 events/sec
  時間複雑度の影響: 大きい(O(n)のスキャン)

poll:
  CPU使用率: 85%
  メモリ: 約12MB
  レイテンシ: 8-40ms
  スループット: ~6,000 events/sec
  時間複雑度の影響: 大きい(O(n)のスキャン)

epoll (レベルトリガー):
  CPU使用率: 15%
  メモリ: 約8MB
  レイテンシ: 0.5-2ms
  スループット: ~80,000 events/sec
  時間複雑度の影響: 小さい(O(1)の取得)

epoll (エッジトリガー):
  CPU使用率: 10%
  メモリ: 約8MB
  レイテンシ: 0.3-1ms
  スループット: ~100,000 events/sec
  時間複雑度の影響: 最小(イベント通知最適)

3.3 メモリ使用量の詳細

epollのメモリフットプリント:

基本構造:
  eventpoll構造体:        約1KB
  Red-Black Tree オーバーヘッド: 約2KB

登録FDごと:
  epitem構造体:           200バイト
  wait_queue_entry:       64バイト
  合計:                   約264バイト/FD

10,000接続の場合:
  基本: 3KB
  FD: 264バイト × 10,000 = 2.6MB
  合計: 約2.6MB

比較:
  select: 128バイト × 3 = 384バイト (固定)
         ただし、1024制限あり
  poll:   16バイト × n (可変)
         10,000接続 = 160KB
  epoll:  264バイト × n (可変)
         10,000接続 = 2.6MB

結論: epollは少しメモリを多く使うが、
      パフォーマンスの向上を考えれば妥当

4. 実世界のケーススタディ

4.1 Nginxのepoll使用

// Nginx のイベント処理(簡略化)
static ngx_int_t
ngx_epoll_process_events(ngx_cycle_t *cycle, ngx_msec_t timer,
                         ngx_uint_t flags)
{
    int events;
    ngx_event_t *rev, *wev;
    struct epoll_event ee[NGX_EPOLL_EVENTS];

    // エッジトリガーで待機
    events = epoll_wait(ep, ee, NGX_EPOLL_EVENTS, timer);

    if (events == -1) {
        return NGX_ERROR;
    }

    for (i = 0; i < events; i++) {
        c = ee[i].data.ptr;

        if (ee[i].events & (EPOLLERR|EPOLLHUP)) {
            // エラー処理
            ngx_log_error(...);
        }

        rev = c->read;
        if (ee[i].events & EPOLLIN) {
            rev->ready = 1;
            rev->handler(rev);  // 読み込みハンドラ実行
        }

        wev = c->write;
        if (ee[i].events & EPOLLOUT) {
            wev->ready = 1;
            wev->handler(wev);  // 書き込みハンドラ実行
        }
    }

    return NGX_OK;
}

Nginxの設定パラメータ:

events {
    use epoll;                   # epollを明示的に指定
    worker_connections 10000;    # 各ワーカーの最大接続数
    multi_accept on;             # 一度に複数のacceptを許可
    accept_mutex off;            # accept競合の制御(新しいNginxではoff推奨)
}

4.2 Redisのepoll使用

// Redis のイベントループ(ae_epoll.c)
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
    aeApiState *state = eventLoop->apidata;
    int retval, numevents = 0;

    // epoll_wait呼び出し
    retval = epoll_wait(state->epfd, state->events,
                        eventLoop->setsize,
                        tvp ? (tvp->tv_sec*1000 + tvp->tv_usec/1000) : -1);

    if (retval > 0) {
        numevents = retval;
        for (j = 0; j < numevents; j++) {
            int mask = 0;
            struct epoll_event *e = state->events+j;

            if (e->events & EPOLLIN) mask |= AE_READABLE;
            if (e->events & EPOLLOUT) mask |= AE_WRITABLE;
            if (e->events & EPOLLERR) mask |= AE_WRITABLE;
            if (e->events & EPOLLHUP) mask |= AE_WRITABLE;

            eventLoop->fired[j].fd = e->data.fd;
            eventLoop->fired[j].mask = mask;
        }
    }
    return numevents;
}

4.3 Node.js (libuv) のepoll使用

// libuv のLinux epoll バックエンド(簡略化)
void uv__io_poll(uv_loop_t* loop, int timeout) {
    struct epoll_event events[1024];
    struct epoll_event* pe;
    int fd, nfds;

    // イベント待機
    nfds = epoll_wait(loop->backend_fd, events, 1024, timeout);

    if (nfds == -1) {
        if (errno != EINTR)
            abort();
        return;
    }

    // 各イベントを処理
    for (i = 0; i < nfds; i++) {
        pe = events + i;
        fd = pe->data.fd;

        // タイマー、シグナル、I/Oイベントを振り分け
        if (fd == -1)
            continue;

        w = loop->watchers[fd];
        if (w == NULL)
            continue;

        // コールバック実行
        w->cb(loop, w, pe->events);
    }
}

5. クロスプラットフォーム比較

5.1 kqueue (BSD/macOS)

// kqueueの例(比較用)
int kq = kqueue();

struct kevent change;
EV_SET(&change, fd, EVFILT_READ, EV_ADD | EV_ENABLE, 0, 0, NULL);
kevent(kq, &change, 1, NULL, 0, NULL);

struct kevent event;
int nev = kevent(kq, NULL, 0, &event, 1, NULL);

if (nev > 0) {
    if (event.filter == EVFILT_READ) {
        // 読み込み処理
        ssize_t n = read(event.ident, buffer, size);
    }
}

epoll vs kqueue:

特性 epoll (Linux) kqueue (BSD/macOS)
**API** 3つのシステムコール 1つのシステムコール
**登録** epoll_ctl (個別) kevent (バッチ可)
**待機** epoll_wait kevent (同じ関数)
**データ** epoll_data (union) udata (void*)
**フィルタ** イベントフラグ EVFILT_* (より豊富)
**タイマー** timerfd必要 EVFILT_TIMER統合
**シグナル** signalfd必要 EVFILT_SIGNAL統合
**パフォーマンス** 非常に高速 同等に高速

5.2 IOCP (Windows)

// Windows IOCP の例
HANDLE iocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);

// ソケットを関連付け
CreateIoCompletionPort((HANDLE)socket, iocp, (ULONG_PTR)context, 0);

// 非同期読み込み開始
WSABUF buf;
DWORD flags = 0;
WSARecv(socket, &buf, 1, NULL, &flags, &overlapped, NULL);

// 完了待機
DWORD bytes;
ULONG_PTR key;
OVERLAPPED* pOverlapped;
GetQueuedCompletionStatus(iocp, &bytes, &key, &pOverlapped, INFINITE);

// データは既に読み込まれている(Proactorパターン)
process_data(buffer, bytes);

epoll vs IOCP:

特性 epoll (Linux) IOCP (Windows)
**パターン** Reactor Proactor
**通知** 「読み込み可能」 「読み込み完了」
**I/O開始** アプリが実行 カーネルが実行
**バッファ** アプリが管理 カーネルが使用
**複雑さ** 中程度 高い
**パフォーマンス** 非常に高速 最高速(真の非同期)

5.3 Zigでのクロスプラットフォーム抽象化

const std = @import("std");
const builtin = @import("builtin");

/// プラットフォーム固有のイベント多重化を統一インターフェースで提供
pub const EventLoop = switch (builtin.os.tag) {
    .linux => EpollEventLoop,
    .macos, .freebsd, .netbsd, .openbsd => KqueueEventLoop,
    .windows => IocpEventLoop,
    else => SelectEventLoop,  // フォールバック
};

pub const EpollEventLoop = struct {
    epfd: i32,
    events: []linux.epoll_event,
    allocator: std.mem.Allocator,

    pub fn init(allocator: std.mem.Allocator) !EpollEventLoop {
        const epfd = linux.epoll_create1(linux.EPOLL_CLOEXEC);
        if (epfd < 0) return error.CreateFailed;

        return EpollEventLoop{
            .epfd = epfd,
            .events = try allocator.alloc(linux.epoll_event, 1024),
            .allocator = allocator,
        };
    }

    pub fn add(self: *EpollEventLoop, fd: i32, events: u32) !void {
        var event = linux.epoll_event{
            .events = events,
            .data = linux.epoll_data{ .fd = fd },
        };
        const result = linux.epoll_ctl(self.epfd, linux.EPOLL_CTL_ADD, fd, &event);
        if (result < 0) return error.AddFailed;
    }

    pub fn wait(self: *EpollEventLoop, timeout_ms: i32) ![]linux.epoll_event {
        const nfds = linux.epoll_wait(
            self.epfd,
            self.events.ptr,
            @intCast(self.events.len),
            timeout_ms
        );
        if (nfds < 0) return error.WaitFailed;
        return self.events[0..@intCast(nfds)];
    }

    pub fn deinit(self: *EpollEventLoop) void {
        std.os.close(self.epfd);
        self.allocator.free(self.events);
    }
};

// 同様にKqueue, IOCP, Selectの実装...

6. epollの高度な使用パターン

6.1 epoll of epoll(ネスト)

/// 複数のepollインスタンスを1つで管理
pub fn nestedEpollExample() !void {
    // マスターepoll
    const master_epfd = try createEpoll();
    defer os.close(master_epfd);

    // ワーカーepoll群
    var worker_epfds: [4]i32 = undefined;
    for (&worker_epfds) |*epfd| {
        epfd.* = try createEpoll();
    }
    defer for (worker_epfds) |epfd| os.close(epfd);

    // 各ワーカーepollをマスターに登録
    for (worker_epfds) |epfd| {
        try epollAdd(master_epfd, epfd, linux.EPOLLIN);
    }

    // マスターepollで全ワーカーを監視
    while (true) {
        const events = try epollWait(master_epfd, 64, -1);
        defer std.heap.page_allocator.free(events);

        for (events) |event| {
            const worker_epfd = event.data.fd;
            // このワーカーにイベントがある
            try processWorkerEvents(worker_epfd);
        }
    }
}

fn processWorkerEvents(worker_epfd: i32) !void {
    const events = try epollWait(worker_epfd, 64, 0);
    defer std.heap.page_allocator.free(events);

    for (events) |event| {
        // 実際のI/Oイベント処理
        try handleIoEvent(event);
    }
}

6.2 タイマー統合(timerfd)

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

pub fn epollWithTimer(epfd: i32) !void {
    // タイマーFDの作成
    const timer_fd = linux.timerfd_create(
        linux.CLOCK.MONOTONIC,
        linux.TFD_NONBLOCK | linux.TFD_CLOEXEC
    );
    if (timer_fd < 0) return error.TimerCreateFailed;
    defer os.close(timer_fd);

    // 1秒ごとに発火するタイマー設定
    var new_value = linux.itimerspec{
        .it_value = .{ .tv_sec = 1, .tv_nsec = 0 },
        .it_interval = .{ .tv_sec = 1, .tv_nsec = 0 },
    };
    _ = linux.timerfd_settime(timer_fd, 0, &new_value, null);

    // epollに追加
    try epollAdd(epfd, timer_fd, linux.EPOLLIN);

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

        for (events) |event| {
            if (event.data.fd == timer_fd) {
                // タイマー発火
                var expirations: u64 = undefined;
                _ = try os.read(timer_fd, std.mem.asBytes(&expirations));
                std.debug.print("タイマー発火: {} 回\n", .{expirations});

                try performPeriodicTask();
            } else {
                // 通常のI/Oイベント
                try handleIoEvent(event);
            }
        }
    }
}

fn performPeriodicTask() !void {
    std.debug.print("定期タスク実行\n", .{});
}

fn handleIoEvent(event: linux.epoll_event) !void {
    std.debug.print("I/Oイベント: fd={}\n", .{event.data.fd});
}

6.3 シグナル統合(signalfd)

pub fn epollWithSignal(epfd: i32) !void {
    // シグナルマスクの設定
    var mask: linux.sigset_t = undefined;
    linux.sigemptyset(&mask);
    linux.sigaddset(&mask, linux.SIG.INT);   // SIGINT
    linux.sigaddset(&mask, linux.SIG.TERM);  // SIGTERM

    // シグナルをブロック(signalfdで受信するため)
    _ = linux.sigprocmask(linux.SIG.BLOCK, &mask, null);

    // signalfdの作成
    const signal_fd = linux.signalfd(-1, &mask, linux.SFD_NONBLOCK | linux.SFD_CLOEXEC);
    if (signal_fd < 0) return error.SignalFdCreateFailed;
    defer os.close(signal_fd);

    // epollに追加
    try epollAdd(epfd, signal_fd, linux.EPOLLIN);

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

        for (events) |event| {
            if (event.data.fd == signal_fd) {
                // シグナル受信
                var siginfo: linux.signalfd_siginfo = undefined;
                const bytes_read = try os.read(signal_fd, std.mem.asBytes(&siginfo));

                if (bytes_read == @sizeOf(linux.signalfd_siginfo)) {
                    std.debug.print("シグナル受信: {}\n", .{siginfo.ssi_signo});

                    if (siginfo.ssi_signo == linux.SIG.INT or
                        siginfo.ssi_signo == linux.SIG.TERM) {
                        std.debug.print("終了シグナル受信、クリーンアップ中...\n", .{});
                        return;
                    }
                }
            } else {
                try handleIoEvent(event);
            }
        }
    }
}

7. パフォーマンスチューニング

7.1 バッファサイズの最適化

// SO_RCVBUF / SO_SNDBUFの調整
pub fn optimizeSocketBuffers(fd: os.socket_t) !void {
    // 受信バッファを64KBに
    const rcvbuf: i32 = 65536;
    try os.setsockopt(
        fd,
        os.SOL.SOCKET,
        os.SO.RCVBUF,
        std.mem.asBytes(&rcvbuf)
    );

    // 送信バッファを64KBに
    const sndbuf: i32 = 65536;
    try os.setsockopt(
        fd,
        os.SOL.SOCKET,
        os.SO.SNDBUF,
        std.mem.asBytes(&sndbuf)
    );
}

7.2 TCP最適化

pub fn optimizeTcp(fd: os.socket_t) !void {
    // TCP_NODELAY: Nagleアルゴリズムを無効化
    const nodelay: i32 = 1;
    try os.setsockopt(
        fd,
        os.IPPROTO.TCP,
        os.TCP.NODELAY,
        std.mem.asBytes(&nodelay)
    );

    // TCP_QUICKACK: 遅延ACKを無効化
    const quickack: i32 = 1;
    try os.setsockopt(
        fd,
        os.IPPROTO.TCP,
        linux.TCP.QUICKACK,
        std.mem.asBytes(&quickack)
    );
}

まとめ

このExplanationでは、epollの深層を探りました:

  • カーネル実装の詳細(eventpoll, epitem構造体)
  • エッジトリガーとレベルトリガーの実装の違い
  • パフォーマンス特性とスケーラビリティ分析
  • 実世界のケーススタディ(Nginx, Redis, Node.js)
  • クロスプラットフォーム比較(kqueue, IOCP)
  • 高度な使用パターン(timerfd, signalfd)
  • パフォーマンスチューニングテクニック

次のExerciseでは、これらの知識を活用して高性能なepollサーバーを実装します。