最終更新:2026-10-12
libev イベントループの原理¶
目次
libev はクロスプラットフォームで効率的なイベント通知ライブラリであり、主に多重化イベント機構(select、poll、epoll など)の実装に使われます。その設計目標は、シンプルかつ高速なイベントループ機構を提供することで、高並列・低レイテンシが求められる場面、例えば高性能Webサーバー、ネットワークプロキシ、その他のIO集約型アプリケーションに特に適しています。コード量が非常に少なく、一部の本番環境でも使用されており、読むことを強くおすすめします。本節ではその実装原理と使い方を解説します。
1.基本フレームワーク¶
libevの中心となるのはイベントループ(event loop)です。これはアプリケーションとオペレーティングシステムの間のインターフェースであり、様々なタイプのイベントを監視・配信するために使われます。libevのアーキテクチャは非常に簡潔で、主に以下の部分で構成されます:
1.1 イベントハンドル(ev_watcher)¶
libevはイベントハンドル(watcher)を通じて様々なイベントを管理・監視します。それぞれのイベントタイプ(IOイベント、タイマーイベントなど)には、対応するwatcherタイプがあります。よく使われるイベントハンドルには以下があります:
- ev_io:ファイルディスクリプタの読み込み可能/書き込み可能イベントの監視に使います。
- ev_timer:タイマーイベント用で、一回限りのタイマーと繰り返しタイマーをサポートします。
- ev_signal:シグナルイベント(
SIGINT、SIGTERMなど)の捕捉に使います。
上記はすべてev_watcherのサブクラスと呼ぶことができます:
typedef struct ev_watcher
{
int active;
int pending;
int priority;
void *data;
void (*cb)(struct ev_loop *loop, struct ev_watcher *w, int revents);
} ev_watcher;
実際に使用するときには、2つの重要な関数が関係します。例えば:読み込み可能な標準入力イベントの登録
ev_io stdin_watcher;
ev_io_init(&stdin_watcher, stdin_cb, /*STDIN_FILENO*/ 0, EV_READ);
ev_io_start(loop, &stdin_watcher);
ev_io_initはマクロであり、最終的に展開されると次のコードになります。主にwatcherの初期化を行います。例えば:callback、fd、eventsなどの設定です。
do
{
do
{
((ev_watcher *)(void *)((&stdin_watcher)))->active = ((ev_watcher *)(void *)((&stdin_watcher)))->pending = 0;
((ev_watcher *)(void *)(((&stdin_watcher))))->priority = (0);
((((&stdin_watcher)))->cb = ((stdin_cb)), memmove(&((ev_watcher *)(((&stdin_watcher))))->cb, &(((&stdin_watcher)))->cb, sizeof((((&stdin_watcher)))->cb)));
} while (0);
do
{
((&stdin_watcher))->fd = ((0));
((&stdin_watcher))->events = ((EV_READ)) | EV__IOFDSET;
} while (0);
} while (0);
ev_io_startは主にanfdsとfdchanges配列を変更します。
- すでにactiveなwatcherはそのまま返ります
- まだアクティブでない場合は、activeとpriorityを設定します
- 先頭挿入法でanfdsの対応するfdのリンクリストに挿入し、現在のwatcherをheadに設定します
wlist_add(&((loop)->anfds)[fd].head, (WL)w);
static __inline__ void
wlist_add(WL *head, WL elem)
{
elem->next = *head;
*head = elem;
}
- fdをfdchanges配列の末尾に挿入します
- フラグを設定します
1.2 IO多重化¶
libevではepoll、poll、selectなどがサポートされており、その中でloopには2つのコールバック関数があります:
void (*backend_modify)(struct ev_loop *loop, int fd, int oev, int nev);
void (*backend_poll)(struct ev_loop *loop, ev_tstamp timeout);
ここではepollを例にとると、backend_modifyはepoll_modify、backend_pollはepoll_pollです。
epoll_modifyは皆さんが普段使っているepoll_ctlのことで、イベントに変化があった場合はEPOLL_CTL_MODを、そうでなければEPOLL_CTL_ADDを使用します。
epoll_pollの実行ロジックは次のとおりです:
- epoll_waitでレディイベントのリストを取得します(loop->epoll_events)
- レディイベントに対してはfd_eventを実行し、loopのpendingsの対応する優先度の配列に入れます。
static __inline__ void
fd_event(struct ev_loop *loop, int fd, int revents)
{
ANFD *anfd = ((loop)->anfds) + fd;
if (__builtin_expect((!!(!anfd->reify)), (1)))
fd_event_nocheck(loop, fd, revents);
}
static __inline__ void
fd_event_nocheck(struct ev_loop *loop, int fd, int revents)
{
ANFD *anfd = ((loop)->anfds) + fd;
ev_io *w;
for (w = (ev_io *)anfd->head; w; w = (ev_io *)((WL)w)->next)
{
int ev = w->events & revents;
if (ev)
ev_feed_event(loop, (W)w, ev);
}
}
// 省略部分代码
void __attribute__((__noinline__))
ev_feed_event(struct ev_loop *loop, void *w, int revents)
{
w_->pending = ++((loop)->pendingcnt)[pri];
((loop)->pendings)[pri][w_->pending - 1].w = w_;
((loop)->pendings)[pri][w_->pending - 1].events = revents;
}
1.3 Timer¶
Timerの内部実装には二分ヒープと四分ヒープが使われています。ここで四分ヒープを採用しているのは、四分ヒープの方がキャッシュ効率が高いためです。その実装は次のとおりです:
#define DHEAP 4
#define HEAP0 (DHEAP - 1) /* index of first element in heap */
#define HPARENT(k) ((((k) - HEAP0 - 1) / DHEAP) + HEAP0)
#define UPHEAP_DONE(p,k) ((p) == (k))
上記ではHEAP0が最初の要素、すなわちoffsetとして使われていることに注意してください。ヒープ内部の実装はupheap、downheapであり、比較的単純なのでここでは省略します。その構造は次のとおりです:
heapのソートはatに基づいて比較されます。atはタイマーの満了時間であり、ヒープソートではatの値の小さい順に並べ替えられます。
1.4 イベントループ(ev_loop)¶
通常は次のように使用します:
EV_DEFAULTはev_default_loop関数を呼び出し、ev_loop構造体を初期化します。その中では主にloop_initによってloop構造体を初期化します。例えば:epollにするか、poll、selectにするかの選択です。
最も核心的なポイント:epoll_initは上記のloopの作成時に発生します。
ev_runは比較的複雑で、主に次のいくつかのことを行います:
- fd_reify
前に述べたfdchanges配列を走査し、fdに基づいてanfdsからリンクリストを取り出し、すべてのイベントリンクリストを走査してすべてのイベントを取得し、現在の最新のイベントと以前の古いイベント(events)が一致するかどうかを判断します。一致しない場合はepoll_modifyを呼び出します(初めてイベントを追加する場合も同様です)。
- backend_poll
前述のbackend_pollの実装を参照してください。レディイベントを取得し、そのイベントをpendings配列に入れます。つまり、対応する優先度キューの末尾に置かれます。
-
timer_reify。イベントが満了していなければ何も処理せず、満了した場合は次のロジックを実行します:
-
repeatが設定されていないtimer
- ev_timer_stopを呼び出してクリアします
-
repeatが設定されているtimer
例:
ev_timer_init(&timer_watcher, timer_cb, 2.0, 3.0);ev_timerのatに現在時刻+2秒を設定し(つまりタイマーは2秒後に発火します)、さらにrepeatを3秒に設定した場合、タイマーはまず2秒後に発火し、その後は3秒ごとに発火します。 -
Invoke_cb
Invoke_cbのデフォルトはev_invoke_pendingで、対応する優先度キューからイベントを取り出し、callback関数を呼び出してイベント処理を行います。
ANPENDING *p = ((loop)->pendings)[((loop)->pendingpri)] + --((loop)->pendingcnt)[((loop)->pendingpri)];
(p->w)->cb(loop, (p->w), (p->events));
2.使い方¶
プロジェクトのコンパイル:
3.宿題¶
最後に宿題を1つ残します。libevを使って次のような例を書けるでしょうか?
データを入力するとstdioイベントがreadyを返し、入力がタイムアウトするとtimeoutを返す、というものです。実行結果は次のとおりです:
関連トピック¶
- C++ Concurrency in Action 読書ガイド | C++那些事
- C++ デッドロック調査 | C++那些事
- C++ SIMD ベクトル化 | C++那些事
- C++17 並列 STL | C++那些事
- C++ False Sharing とキャッシュライン | C++那些事
- GDB マルチスレッド All-Stop デバッグ | C++那些事
- Helgrind 並行エラー検出 | C++那些事
- C++ std::thread の内部実装 | C++那些事
- C++ std::thread と jthread | C++那些事
- C++ マルチスレッド入門 | C++那些事