Última actualización:2026-10-12
Principios del bucle de eventos de libev¶
Esquema
libev es una biblioteca de notificación de eventos multiplataforma y eficiente, usada sobre todo para implementar mecanismos de eventos multiplexados (por ejemplo select, poll, epoll, etc.). Su objetivo de diseño es ofrecer un mecanismo de bucle de eventos simple y rápido, especialmente adecuado para escenarios que exigen alta concurrencia y baja latencia, como servidores web de alto rendimiento, proxies de red y otras aplicaciones intensivas en IO. Su cantidad de código es muy reducida y se usa en entornos de producción; recomiendo encarecidamente su lectura. En esta sección hablaremos de sus principios de implementación y de su uso.
1. Marco básico¶
El núcleo de libev es el bucle de eventos (event loop), que es la interfaz entre la aplicación y el sistema operativo, encargada de monitorizar y distribuir los distintos tipos de eventos. La arquitectura de libev es muy sencilla y comprende principalmente las siguientes partes:
1.1 Identificadores de eventos (ev_watcher)¶
libev gestiona y monitoriza los diversos eventos mediante identificadores de eventos (watchers). Cada tipo de evento (eventos IO, eventos de temporizador, etc.) tiene su tipo de watcher correspondiente. Los identificadores de eventos más comunes incluyen:
- ev_io: monitoriza eventos de lectura/escritura de descriptores de archivo.
- ev_timer: para eventos de temporizador, admite temporizadores de una sola vez y repetitivos.
- ev_signal: para capturar eventos de señal (como
SIGINT,SIGTERM, etc.).
Todos los anteriores pueden considerarse subclases de 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;
En el uso real intervienen dos funciones clave; por ejemplo, para registrar un evento de lectura de la entrada estándar:
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 es una macro cuya expansión final es el código siguiente; básicamente inicializa el watcher, por ejemplo asignando el callback, el fd, los events, etc.
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 se ocupa básicamente de modificar los arrays anfds y fdchanges.
- Si el watcher ya está active, regresa directamente
- Si aún no está activo, asigna active y priority
- Inserta por la cabeza en la lista enlazada del fd correspondiente de anfds y pone el watcher actual como head
wlist_add(&((loop)->anfds)[fd].head, (WL)w);
static __inline__ void
wlist_add(WL *head, WL elem)
{
elem->next = *head;
*head = elem;
}
- Inserta el fd al final del array fdchanges
- Asigna el marcador
1.2 Multiplexado de IO¶
libev admite epoll, poll, select, etc.; dentro del loop hay 2 funciones de retorno:
void (*backend_modify)(struct ev_loop *loop, int fd, int oev, int nev);
void (*backend_poll)(struct ev_loop *loop, ev_tstamp timeout);
Tomemos epoll como ejemplo: backend_modify es epoll_modify y backend_poll es epoll_poll.
epoll_modify es el epoll_ctl que todo el mundo suele usar: cuando el evento cambia usa EPOLL_CTL_MOD, y en caso contrario EPOLL_CTL_ADD.
La lógica de ejecución de epoll_poll es la siguiente:
- epoll_wait obtiene la lista de eventos listos (loop->epoll_events)
- Para los eventos listos, ejecuta fd_event y los coloca en el array de pendings del loop con la prioridad correspondiente.
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¶
La implementación interna de Timer usa un montón binario y un montón cuaternario; se usa el cuaternario porque su eficiencia de caché es mayor. Su implementación es la siguiente:
#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))
Nótese que arriba se usa HEAP0 como primer elemento, es decir, como offset; la implementación interna del montón es upheap y downheap, tan sencilla que no entraremos en detalles. Su estructura es:
El montón se ordena comparando at, que es el tiempo de vencimiento del temporizador; la ordenación del montón va del valor de at menor al mayor.
1.4 Bucle de eventos (ev_loop)¶
Normalmente se usa así:
EV_DEFAULT llama a la función ev_default_loop, que inicializa una estructura ev_loop; dentro, loop_init se encarga sobre todo de inicializar la estructura del loop, por ejemplo eligiendo entre epoll, poll o select.
El punto más esencial: epoll_init ocurre al crear el loop anterior.
ev_run es bastante complejo y hace principalmente varias cosas:
- fd_reify
Recorre el array fdchanges mencionado antes, saca de anfds la lista enlazada según el fd, recorre todas las listas de eventos para obtenerlos todos y comprueba si el evento más reciente coincide con el evento anterior (events); si no coincide, llama a epoll_modify (o añade el evento por primera vez).
- backend_poll
Véase antes la implementación de backend_poll: obtiene los eventos listos y los coloca en el array pendings, es decir, al final de la cola de prioridad correspondiente.
-
timer_reify: los eventos no vencidos no se procesan; los vencidos ejecutan la lógica siguiente:
-
Timer sin repeat asignado
- Llama a ev_timer_stop para eliminarlo
-
Timer con repeat asignado
Por ejemplo: `ev_timer_init(&timer_watcher, timer_cb, 2.0, 3.0);``
`ev_timertiene suatestablecido en el tiempo actual más 2 segundos (es decir, el temporizador se disparará pasados 2 segundos) y surepeaten 3 segundos; entonces el temporizador se disparará primero a los 2 segundos y después cada 3 segundos. -
Invoke_cb
Invoke_cb por defecto es ev_invoke_pending: saca los eventos de la cola de prioridad correspondiente y llama a la función callback para procesarlos.
ANPENDING *p = ((loop)->pendings)[((loop)->pendingpri)] + --((loop)->pendingcnt)[((loop)->pendingpri)];
(p->w)->cb(loop, (p->w), (p->events));
2. ¿Cómo usarlo?¶
Compilación del proyecto:
3. Tarea final¶
Para terminar, dejo un ejercicio: ¿serías capaz de escribir con libev un ejemplo así?
Cuando introducimos datos, el evento de stdio devuelve ready; si la entrada expira, devuelve timeout. Efecto de la demostración:
Temas relacionados¶
- Guía de lectura de C++ Concurrency in Action | C++那些事
- Localización de interbloqueos en C++ | C++那些事
- Vectorización SIMD en C++ | C++那些事
- STL paralelo de C++17 | C++那些事
- False Sharing y líneas de caché en C++ | C++那些事
- Depuración multihilo All-Stop con GDB | C++那些事
- Detección de errores de concurrencia con Helgrind | C++那些事
- Implementación interna de std::thread en C++ | C++那些事
- std::thread y jthread en C++ | C++那些事
- Introducción al multihilo en C++ | C++那些事