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で再度通知される
}
}
}
}
レベルトリガーの特徴:
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;
}
}
}
}
}
}
エッジトリガーの特徴:
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の基礎を学習しました:
次のExplanationでは、epollのより深い詳細と最適化テクニックを学習します。