Перейти к содержанию

Последнее обновление:2026-10-12

Принцип событийного цикла libev

План

libev — кроссплатформенная эффективная библиотека событийных уведомлений, используемая в основном для реализации механизмов мультиплексирования событий (например, select, poll, epoll и т.д.). Её цель — предоставить простой и быстрый механизм событийного цикла, особенно подходящий сценариям с высокой конкурентностью и низкой задержкой: высокопроизводительные веб-серверы, сетевые прокси и другие приложения с интенсивным 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;
При реальном использовании задействованы две ключевые функции, например: регистрация события читаемости стандартного ввода

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 в основном изменяет массивы anfs и fdchanges.

  • для уже active watcher — сразу возвращаемся
  • если ещё не активен — устанавливаем active и priority
  • вставляем в начало связного списка соответствующего fd в anfds (вставка в голову) и делаем текущий watcher головой

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 callback-функции:

void (*backend_modify)(struct ev_loop *loop, int fd, int oev, int nev);
void (*backend_poll)(struct ev_loop *loop, ev_tstamp timeout);
Возьмём для примера epool: 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, помещающий их в массив pendings цикла 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

Внутренняя реализация 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: неистёкшие события не обрабатываются, а для истёкших выполняется следующая логика:

  • timer без установленного repeat

    • вызываем ev_timer_stop для очистки
  • timer с установленным repeat

    Например: `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. Домашнее задание

Напоследок оставлю задание: сможете ли вы написать с помощью libev такой пример?

Когда мы вводим данные, событие stdio возвращает ready; если ввод задерживается — возвращается timeout; демонстрация эффекта:

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

Похожие темы

Комментарии