このページの目次

epoll とイベント駆動

カバー範囲: epoll_create1/epoll_ctl/epoll_wait → LT vs ET → timerfd/signalfd/eventfd → select/pollとの比較 → epoll内部実装のポイント 対象: Linux 2.6+

概要

epoll は、Linux における高性能なイベント駆動プログラミングの標準インターフェースです。nginx、HAProxy、Redis はすべて epoll を基盤として構築されています。select/poll との根本的な違いは、epoll が O(1) の待機(アクティブな fd のみを返す)であるのに対し、select/poll は毎回 fd 集合全体を O(n) でスキャンする必要がある点です。

epoll API

int epfd = epoll_create1(EPOLL_CLOEXEC);  // epoll インスタンスの作成

struct epoll_event ev = {
    .events   = EPOLLIN | EPOLLET,       // 読み込み可能を監視、エッジトリガー
    .data.fd  = listen_fd,               // ユーザーデータ (ptr でも可)
};
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);  // fd の登録

struct epoll_event events[MAX_EVENTS];
int nfds = epoll_wait(epfd, events, MAX_EVENTS, timeout_ms);

for (int i = 0; i < nfds; i++) {
    if (events[i].events & EPOLLIN)
        handle_read(events[i].data.fd);
}

epoll_event.events

EPOLLIN:       読み込み可能 (データ到着または接続確立)
EPOLLOUT:      書き込み可能 (送信バッファに空きがある)
EPOLLERR:      エラー (自動検出、設定不要)
EPOLLHUP:      ハングアップ (相手が切断)
EPOLLRDHUP:    相手の書き込み側接続が閉じた (4.17+)
EPOLLET:       エッジトリガー (以下参照)
EPOLLONESHOT:  一度限り (トリガー発生後に自動で削除され、EPOLL_CTL_MOD で再登録するまで有効にならない)
EPOLLEXCLUSIVE: 排他的ウェイクアップ (スラッシングを軽減、4.5+)

LT vs ET: 重要な違い

LT (Level-Triggered、デフォルト):
  fd が読み込み可能/書き込み可能な状態である限り → 毎回の epoll_wait で返される
  → 単純: データの一部だけを読み取り、残りは次回に読むことができる
  → リスク: 読み込みが完了していない場合、次の epoll_wait でも返される → ループが必要

ET (Edge-Triggered):
  状態遷移時のみ通知される (読み込み不可→読み込み可、書き込み不可→書き込み可)
  → 効率的: 重複通知がない
  → 要件: EAGAIN (すべて処理し終わった) になるまで読み書きを行う必要がある
  → O_NONBLOCK を必須とする (しないと read がブロックする可能性がある)

ET モードの正しい実装

// ET モードでは必ずループして読み込む必要がある:
while (1) {
    ssize_t n = read(fd, buf, sizeof(buf));
    if (n > 0) {
        process(buf, n);
    } else if (n == 0) {
        close(fd);  // EOF
        break;
    } else {
        if (errno == EAGAIN || errno == EWOULDBLOCK)
            break;  // すべて読み終わった
        // エラー処理
        break;
    }
}

timerfd / signalfd / eventfd: epoll の相棒

// 統一されたイベントループ: すべての非同期イベントを fd に変換し、epoll で一元管理する

int epfd = epoll_create1(0);

// 1. ネットワークソケット
epoll_add(epfd, listen_fd, EPOLLIN);

// 2. timerfd: タイマー
int tfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK);
epoll_add(epfd, tfd, EPOLLIN);

// 3. signalfd: シグナル
sigset_t mask; sigaddset(&mask, SIGINT); sigaddset(&mask, SIGTERM);
sigprocmask(SIG_BLOCK, &mask, NULL);
int sfd = signalfd(-1, &mask, SFD_NONBLOCK);
epoll_add(epfd, sfd, EPOLLIN);

// 4. eventfd: スレッド間通知
int efd = eventfd(0, EFD_NONBLOCK);
epoll_add(epfd, efd, EPOLLIN);

// 単一スレッド: epoll_wait → すべてのイベントを統一して処理
while (1) {
    int nfds = epoll_wait(epfd, events, MAX, -1);
    for (int i = 0; i < nfds; i++) {
        if      (events[i].data.fd == listen_fd) accept();
        else if (events[i].data.fd == tfd)       handle_timer();
        else if (events[i].data.fd == sfd)       handle_signal();
        else if (events[i].data.fd == efd)       handle_notification();
        else                                      handle_client(events[i].data.fd);
    }
}

select/poll との比較

selectpollepoll
fd の数FD_SETSIZE (1024)上限なし上限なし
待機O(n) スキャンO(n) スキャンO(1) アクティブ数
状態の保存毎回渡す (スタック)毎回渡す (スタック)カーネルが管理 (RB木)
エッジトリガーなしなしあり (ET)
1万コネクション (アイドル)~100μs~100μs~1μs

epoll の内部実装

// fs/eventpoll.c
// epoll インスタンスは以下の要素を含む:
//   1. 赤黒木 (rbr): 登録された fd のリスト (epoll_ctl ADD → 挿入)
//   2. 準備済みリスト (rdllist): アクティブな fd (epoll_wait で返される)
//
// イベント到着 → ドライバ/プロトコルスタックが ep_poll_callback を呼び出す:
//   → イベントが一致するか確認? → rdllist に追加
//   → 待機者がいる場合 (epoll_wait) → ウェイクアップ

参考

  • man: epoll(7), epoll_create1(2), epoll_ctl(2), epoll_wait(2)
  • ソースコード⁠: fs/eventpoll.c
  • LWN: "The epoll API", "Edge triggered epoll"

キーワード: epoll, EPOLLET, EPOLLONESHOT, edge-triggered, timerfd, signalfd, eventfd, event loop