Последнее обновление: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);
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);
- для уже 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;
}
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);
epoll_modify — это привычный всем epoll_ctl: когда событие изменилось, используется EPOLL_CTL_MOD, иначе EPOLL_CTL_ADD.
Логика выполнения 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))
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: неистёкшие события не обрабатываются, а для истёкших выполняется следующая логика:
-
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 секунды. -
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. Домашнее задание¶
Напоследок оставлю задание: сможете ли вы написать с помощью libev такой пример?
Когда мы вводим данные, событие stdio возвращает ready; если ввод задерживается — возвращается timeout; демонстрация эффекта:
Похожие темы¶
- Путеводитель по C++ Concurrency in Action | C++那些事
- Поиск взаимоблокировок в C++ | C++那些事
- Векторизация C++ SIMD | C++那些事
- Параллельный STL в C++17 | C++那些事
- C++ False Sharing и строка кэша | C++那些事
- Отладка многопоточности GDB в режиме All-Stop | C++那些事
- Обнаружение ошибок конкурентности с помощью Helgrind | C++那些事
- Внутренняя реализация C++ std::thread | C++那些事
- C++ std::thread и jthread | C++那些事
- Введение в многопоточность C++ | C++那些事