Ir para o conteúdo

Última atualização:2026-10-12

Princípio do event loop do libev

Outline

O libev é uma biblioteca de notificação de eventos eficiente e multiplataforma, usada principalmente para implementar mecanismos de eventos multiplexados (como select, poll, epoll etc.). Seu objetivo de projeto é fornecer um mecanismo de laço de eventos simples e rápido, especialmente adequado para cenários que exigem alta concorrência e baixa latência, como servidores Web de alta performance, proxies de rede e outras aplicações intensivas de IO. A quantidade de código é bem pequena e ela é usada em ambientes de produção; a leitura é muito recomendada. Nesta seção vamos falar dos seus princípios de implementação e do seu uso.

1.Estrutura básica

O núcleo do libev é o laço de eventos (event loop), que serve de interface entre a aplicação e o sistema operacional, usado para monitorar e despachar diferentes tipos de eventos. A arquitetura do libev é bastante enxuta e contém principalmente as seguintes partes:

1.1 Handles de evento (ev_watcher)

O libev gerencia e monitora os vários eventos por meio de handles de evento (watchers). Cada tipo de evento (eventos de IO, eventos de timer etc.) tem um tipo de watcher correspondente. Os handles de evento mais comuns incluem:

  • ev_io: usado para monitorar eventos de leitura/escrita em descritores de arquivo.
  • ev_timer: usado para eventos de timer, com suporte a timers de uma vez e repetitivos.
  • ev_signal: usado para capturar eventos de sinal (como SIGINT, SIGTERM etc.).

Todos os acima podem ser chamados de subclasses 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;
No uso real, há duas funções-chave envolvidas; por exemplo: registrar um evento de leitura na entrada padrão

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 é uma macro cuja expansão final é o código abaixo; ela serve basicamente para inicializar o watcher, por exemplo: definir callback, fd, 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 modifica principalmente os arrays anfs e fdchanges.

  • Para um watcher já active, retorna diretamente
  • Se ainda não estiver ativo, define active e priority
  • Usa inserção no início para adicioná-lo à lista ligada do fd correspondente em anfds, e define o watcher atual como head

wlist_add(&((loop)->anfds)[fd].head, (WL)w);

static __inline__ void
wlist_add(WL *head, WL elem)
{
  elem->next = *head;
  *head = elem;
}
- Insere o fd no final do array fdchanges

++((loop)->fdchangecnt);
((loop)->fdchanges)[((loop)->fdchangecnt) - 1] = fd;
- Define a flag

w->events &= ~EV__IOFDSET;

1.2 Multiplexação de IO

O libev dá suporte a epoll, poll, select etc.; dentro do loop há 2 funções callback:

void (*backend_modify)(struct ev_loop *loop, int fd, int oev, int nev);
void (*backend_poll)(struct ev_loop *loop, ev_tstamp timeout);
Tomando o epoll como exemplo, backend_modify é epoll_modify e backend_poll é epoll_poll.

epoll_modify é o epoll_ctl que todos costumam usar: quando o evento muda, usa EPOLL_CTL_MOD; caso contrário, usa EPOLL_CTL_ADD.

epoll_ctl (backend_fd, oev && oldmask != nev ? EPOLL_CTL_MOD : EPOLL_CTL_ADD, fd, &ev)
A lógica de execução de epoll_poll é a seguinte:

  • epoll_wait obtém a lista de eventos prontos (loop->epoll_events)
  • Para os eventos prontos, executa fd_event, colocando-os no array pendings do loop, no nível de prioridade correspondente.
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

A implementação interna do Timer usa heap binário e heap quaternário; o heap quaternário é usado porque tem melhor eficiência de cache. Sua implementação é a seguinte:

#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 que acima HEAP0 é usado como primeiro elemento, ou seja, o offset; a implementação interna do heap é upheap e downheap, que é simples e não detalharemos. Sua estrutura é:

typedef ev_watcher_time *WT;

typedef struct
{
  ev_tstamp at;
  WT w;
} ANHE;
A ordenação do heap é feita comparando at, onde at é o tempo de expiração do timer; a ordenação do heap é crescente em relação ao valor de at.

1.4 Laço de eventos (ev_loop)

O uso típico é o seguinte:

struct ev_loop *loop = EV_DEFAULT;
ev_run(loop, 0);
EV_DEFAULT chama a função ev_default_loop, que inicializa uma estrutura ev_loop; dentro dela, loop_init inicializa a estrutura do loop, por exemplo: escolher entre epoll, poll ou select.

O ponto mais essencial: o epoll_init acontece na criação do loop acima.

O ev_run é bem complexo e faz principalmente algumas coisas:

  • fd_reify

Percorre o array fdchanges mencionado antes, retira do anfds a lista ligada correspondente ao fd, percorre todas as listas ligadas de eventos para obter todos os eventos e verifica se o evento mais recente é igual ao evento antigo anterior (events); se for diferente, chama epoll_modify (ou adiciona o evento pela primeira vez).

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

Veja a implementação de backend_poll anterior: ele obtém os eventos prontos e os coloca no array pendings, ou seja, no final da fila de prioridade correspondente.

  • timer_reify: se o evento não expirou, nada é feito; se expirou, executa a lógica abaixo:

  • timer sem repeat definido

    • chama ev_timer_stop para removê-lo
  • timer com repeat definido

    Por exemplo: `ev_timer_init(&timer_watcher, timer_cb, 2.0, 3.0);``

    `ev_timer define at como o tempo atual mais 2 segundos (ou seja, o timer disparará em 2 segundos) e repeat é definido como 3 segundos; assim, o timer dispara primeiro após 2 segundos e, a partir daí, a 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 padrão ev_invoke_pending: retira o evento da fila de prioridade correspondente e chama a função callback para tratá-lo.

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

2.Como usar?

git clone git@github.com:enki/libev.git
./configure
make && make install
Compilação do projeto:

gcc main.c -lev

3.Exercício

Por fim, deixo um exercício: você consegue escrever com o libev um exemplo como este?

Quando inserimos dados, o evento de stdio retorna ready; se a entrada ultrapassar o tempo limite, retorna timeout. Efeito da demonstração:

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

Tópicos relacionados

Comentários