高性能ネットワークプログラミングにおける epoll の役割
大規模なネットワークアプリケーションを開発する際、プラットフォームによって推奨される IO 多路复用メカニズムが異なります。Windows 環境では iocp が主流ですが、Linux システムにおいては epoll が事実上の標準となっています。従来の select や poll に比べて、数百万という socket 接続を扱う際の性能差は歴然としており、なぜこれほど高速に動作するのかを理解することは、システム設計において不可欠です。
まず、ユーザ空間で利用される主要な三つのシステコールを確認しておきましょう。
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
基本的なフローはシンプルです。最初に epoll_create を用いてエポックレディ(監視オブジェクト)を作成し、指定したサイズを超える場合はカーネル側での保証が難しくなる場合があります。
次に、epoll_ctl を用いて作成された監視オブジェクトに対して、特定のファイル記述子(fd)を追加・変更・削除する操作を行います。最後に、epoll_wait 呼び出しにより、指定した時間内にイベントが発生した fd 一覧を受け取ります。
従来手法との根本的な違い
select や poll と比較した場合の決定的な利点はデータ転送の最小化にあります。従来の手法では、各呼び出しごとに監視対象リスト全体をユーザ領域からカーネル領域へコピーする必要があり、万単位の fd では膨大なメモリアクセスが発生していました。一方、epoll では監視対象の設定はepoll_ctlで一度行えば良く、epoll_wait呼び出し時に監視対象を再度渡す必要がないため、このオーバーヘッドが存在しません。
カーネル内部のデータ構造実装
Linux カーネル内では、epoll は独自の仮想ファイルシステムを介して管理されます。epoll_create で生成されたノードには、監視すべき socket を格納するための「赤黒木」と、準備完了したイベントを待ち受ける「就绪链表(Ready List)」が紐付けられます。さらに、メモリ割り当ての効率化のために、専用スラブキャッシュエリアが確保されています。
以下のコードは、内部構造である epitem および記憶領域の割り当てロジックを模倣した例示です。実際のカーネル実装と同様に、オブジェクトごとのメモリブロックを効率的に再利用するアルゴリズムを採用しています。
struct epitem {
struct rb_node rbn; /* 赤黒木のノード */
struct list_head rdllink; /* 就绪链表へのリンク */
struct file *file; /* 関連付けられたファイル情報 */
struct epq *head; /* イベントキュー */
};
/* スラブキャッシュからのオブジェクト確保イメージ */
struct epitem *alloc_item(void) {
struct epitem *obj;
// メモリプールから空きオブジェクトを取得
obj = get_slab_object("epitem_pool");
if (!obj) return NULL;
/* リスト初期化と初期設定 */
INIT_LIST_HEAD(&obj->rdllink);
RB_CLEAR_NODE(&obj->rbn);
return obj;
}
イベント通知と割込み処理
epoll が高速である理由の多くは、epoll_wait が常に大量のデータをチェックしない点にあります。epoll_ctlで登録された時、カーネルは該当 socket に対するハードウェア割込みのコールバック関数を登録します。ネットワークカードからデータを受信すると、割込みハンドラが即座に対応する socket を「就绪链表」に追加します。そのため、epoll_wait呼び出し時には、この小さなリストの内容をユーザ空間へコピーするだけで済み、監視中の総数が何百万個あってもパフォーマンス低下は生じません。
トリガリングモード:LT と ET
epoll の挙動を制御するために提供される二つのモードがあります。どちらも上記の仕組みに基づきますが、イベント通知の頻度が異なります。
- レベルトリガ(LT): デフォルト動作。socket に未処理データがある限り、
epoll_waitを呼ぶ度に必ずその fd を通知します。データを全て読み切るまで繰り返しが発生します。 - エッジトリガ(ET): 状態が変化した瞬間(新規データ到着など)のみ通知します。一度戻ってきても、新しいイベントが来るまで再通知されないため、非ブロッキング I/O で完全にデータを読み切る必要があります。
ET モードでは、カーネル内部で就绪リストをクリアした後、同じ socket に未処理イベントが残っているか確認し、ET ではなく LT モードであれば再度リストに戻します。これに対し ET モードでは新しい割込みが入るまでリストに復帰せず、CPU サイクルの節約を実現しています。