コンテンツにスキップ

最終更新: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配列の末尾に挿入します
++((loop)->fdchangecnt);
((loop)->fdchanges)[((loop)->fdchangecnt) - 1] = fd;
  • フラグを設定します
w->events &= ~EV__IOFDSET;

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_ctl (backend_fd, oev && oldmask != nev ? EPOLL_CTL_MOD : EPOLL_CTL_ADD, fd, &ev)

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であり、比較的単純なのでここでは省略します。その構造は次のとおりです:

typedef ev_watcher_time *WT;

typedef struct
{
  ev_tstamp at;
  WT w;
} ANHE;

heapのソートはatに基づいて比較されます。atはタイマーの満了時間であり、ヒープソートではatの値の小さい順に並べ替えられます。

1.4 イベントループ(ev_loop)

通常は次のように使用します:

struct ev_loop *loop = EV_DEFAULT;
ev_run(loop, 0);

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を呼び出します(初めてイベントを追加する場合も同様です)。

typedef struct
{
  WL head;      // 事件链表
  unsigned char events; // 前面事件
    // other
} ANFD;
  • 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秒ごとに発火します。

    if (w->repeat)
    {
      // 更新下次触发时间
      ((WT)(w))->at += w->repeat;
      // 过期了
      if (((WT)(w))->at < ((loop)->mn_now))
        ((WT)(w))->at = ((loop)->mn_now);   // 设置当前时间
      // 设置堆顶元素为前面w->at
      (((loop)->timers)[HEAP0]).at = (((loop)->timers)[HEAP0]).w->at;
      // 调整堆
      downheap(((loop)->timers), ((loop)->timercnt), HEAP0);
    }
    
  • 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.使い方

git clone git@github.com:enki/libev.git
./configure
make && make install

プロジェクトのコンパイル:

gcc main.c -lev

3.宿題

最後に宿題を1つ残します。libevを使って次のような例を書けるでしょうか?

データを入力するとstdioイベントがreadyを返し、入力がタイムアウトするとtimeoutを返す、というものです。実行結果は次のとおりです:

[light@i-7dwsk0ty libev]$ ./a.out 
timeout
[light@i-7dwsk0ty libev]$ ./a.out 
21
stdin ready

関連トピック

コメント