Saltar a contenido

Ú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
++((loop)->fdchangecnt);
((loop)->fdchanges)[((loop)->fdchangecnt) - 1] = fd;
  • Asigna el marcador
w->events &= ~EV__IOFDSET;

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.

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

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:

typedef ev_watcher_time *WT;

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

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í:

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

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

typedef struct
{
  WL head;      // 事件链表
  unsigned char events; // 前面事件
    // other
} ANFD;
  • 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_timer tiene su at establecido en el tiempo actual más 2 segundos (es decir, el temporizador se disparará pasados 2 segundos) y su repeat en 3 segundos; entonces el temporizador se disparará primero a los 2 segundos y después cada 3 segundos.

    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 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?

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

Compilación del proyecto:

gcc main.c -lev

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:

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

Temas relacionados

Comentarios