Skip to content

Last updated:2026-10-12

libev Event Loop Internals

Outline

libev is a cross-platform, efficient event notification library, mainly used to implement multiplexed event mechanisms (such as select, poll, epoll, etc.). Its design goal is to provide a simple, fast event loop mechanism, especially suitable for scenarios that require high concurrency and low latency, such as high-performance web servers, network proxies, and other IO-intensive applications. Its code base is very small and it is used in some production environments; it is highly recommended reading. This section covers its implementation principles and usage.

1. Basic framework

The core of libev is the event loop, which is the interface between the application and the operating system, used to monitor and dispatch different types of events. libev's architecture is very clean and mainly consists of the following parts:

1.1 Event watchers (ev_watcher)

libev manages and monitors various events through event watchers. Each event type (IO events, timer events, etc.) has a corresponding watcher type. Common event watchers include:

  • ev_io: used to monitor readability/writability events on a file descriptor.
  • ev_timer: used for timer events, supporting both one-shot and repeating timers.
  • ev_signal: used to catch signal events (such as SIGINT, SIGTERM, etc.).

All of the above can be regarded as subclasses of 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;

In actual use, two key functions are involved, for example: registering a readable standard-input event

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 is a macro; it expands to the following code, which mainly initializes the watcher, for example: setting the callback, fd, events, and so on.

  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 mainly modifies the anfds and fdchanges arrays.

  • For a watcher that is already active, return directly
  • If it is not active yet, set active and priority
  • Insert it with the head-insertion method into the linked list for the corresponding fd in anfds, and make the current watcher the head
wlist_add(&((loop)->anfds)[fd].head, (WL)w);

static __inline__ void
wlist_add(WL *head, WL elem)
{
  elem->next = *head;
  *head = elem;
}
  • Append the fd to the end of the fdchanges array
++((loop)->fdchangecnt);
((loop)->fdchanges)[((loop)->fdchangecnt) - 1] = fd;
  • Set the flag
w->events &= ~EV__IOFDSET;

1.2 IO multiplexing

libev supports epoll, poll, select, etc.; there are 2 callback functions in the loop:

void (*backend_modify)(struct ev_loop *loop, int fd, int oev, int nev);
void (*backend_poll)(struct ev_loop *loop, ev_tstamp timeout);

Taking epoll as the example, backend_modify is epoll_modify, and backend_poll is epoll_poll.

epoll_modify is the commonly used epoll_ctl: when the events have changed it uses EPOLL_CTL_MOD, otherwise it uses EPOLL_CTL_ADD.

epoll_ctl (backend_fd, oev && oldmask != nev ? EPOLL_CTL_MOD : EPOLL_CTL_ADD, fd, &ev)

epoll_poll executes the following logic:

  • epoll_wait fetches the list of ready events (loop->epoll_events)
  • For ready events, execute fd_event to put them into the pendings array of the corresponding priority in the loop.
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

The Timer implementation internally uses a binary heap and a 4-ary heap; the reason for using a 4-ary heap is that it has better cache efficiency. Its implementation is as follows:

#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))

Note that HEAP0 is used above as the first element, i.e. the offset. The internal implementation of the heap is upheap and downheap, which is simple and will not be elaborated on. Its structure is:

typedef ev_watcher_time *WT;

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

The heap is ordered by comparing at, where at is the timer's expiry time; heap ordering sorts by the value of at from small to large.

1.4 Event loop (ev_loop)

It is usually used as follows:

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

EV_DEFAULT calls the ev_default_loop function, which initializes an ev_loop structure; the loop structure is initialized mainly through loop_init, for example: choosing epoll, or poll/select.

The most crucial point: epoll_init happens when the loop above is created.

ev_run is fairly complex and mainly does a few things:

  • fd_reify

Iterate over the fdchanges array mentioned before; for each fd, take the linked list out of anfds, traverse all the event lists to gather all the events, and check whether the current newest events match the previous old events (events); if they differ, call epoll_modify (or add the event for the first time).

typedef struct
{
  WL head;      // 事件链表
  unsigned char events; // 前面事件
    // other
} ANFD;
  • backend_poll

See the implementation of backend_poll above: it fetches the ready events and puts them into the pendings array, i.e. appends them to the end of the corresponding priority queue.

  • timer_reify: unexpired events are left alone; expired ones execute the following logic:

  • A timer without repeat set

    • Calls ev_timer_stop to clear it
  • A timer with repeat set

    For example: `ev_timer_init(&timer_watcher, timer_cb, 2.0, 3.0);``

    `ev_timer sets at to the current time plus 2 seconds (i.e. the timer will fire after 2 seconds), and sets repeat to 3 seconds; the timer then fires for the first time after 2 seconds and subsequently every 3 seconds.

    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 is by default ev_invoke_pending: it takes events out of the corresponding priority queues and invokes the callback functions to process them.

ANPENDING *p = ((loop)->pendings)[((loop)->pendingpri)] + --((loop)->pendingcnt)[((loop)->pendingpri)];
(p->w)->cb(loop, (p->w), (p->events));

2. How to use it?

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

Building the project:

gcc main.c -lev

3. Homework

Finally, here is an exercise for you: can you write an example like this with libev?

When we input data, the stdio event returns ready; if the input times out, it returns timeout. Demo effect:

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

Comments