Анализ исходного кода Libevent — цикл событий

задняя часть
Анализ исходного кода Libevent — цикл событий

Недавно я прочитал исходный код libevent и написал статью, в которой подытожил то, что узнал. При использовании libevent следует отдавать предпочтение последней стабильной версии, и чтобы уменьшить сложность чтения исходного кода, я выбрал версию 1.4, которая является веткой patches-1.4. Чтение этой статьи требует основ сетевого программирования Unix и режима реактора.Если у вас есть какие-либо вопросы по этому поводу, вы можете прочитать мою статью.Принцип анализ и практика типичного режима сервера

файловая структура libevent

Эта статья о файловой структуре libeventThe Libevent Reference Manual: PreliminariesОписание относительно понятное, вот краткое описание.

событие и event_base

Event и event_base — это ядро ​​libevent, и мы хотим его обсудить. Они в основном сосредоточены на двух типах структур, event и event_base. Event определяет структуру события, а event_base — это структура цикла событий. Эти две структуры определены в файлах event.h и event-internal.h. API-интерфейсы, такие как инициализация событий, регистрация событий и удаление событий, определены в event.c, а также API-интерфейсы, связанные с базой событий платформы цикла событий.

evbuffer и буферевент

evbuffer и bufferevent решают проблему буферизации чтения и записи в libevent.Эти две структуры также определены в заголовочном файле event.h, а соответствующие API определены в файлах buffer.c и evbuffer.c соответственно. API. bufferevent — это структура управления буфером, которая содержит два указателя evbuffer, один — на область буфера чтения, а другой — на область буфера записи. evbuffer имеет дело с базовым вводом-выводом. Кроме того, я должен упомянуть, что высокий уровень воды и низкий уровень воды устанавливаются как для буфера чтения, так и для буфера записи в буферном событии.Высокий уровень воды предназначен для предотвращения того, чтобы один буфер занимал слишком много памяти, а низкий уровень воды, чтобы уменьшить количество вызовов функции обратного вызова, раз для повышения эффективности.

Система мультиплексирования ввода-вывода

Libevent — кроссплатформенная сетевая библиотека.Разные платформы реализуют мультиплексирование ввода-вывода по-разному.Даже на одной платформе может быть несколько реализаций.Libevent поддерживает методы select, poll, epoll, kqueue и другие.

util

Модуль util — это некоторые общедоступные методы, такие как функции журнала, функции обработки времени и т. д.

мероприятие

libevent дополнительно абстрагирует события, в дополнение к событиям чтения и записи, он также включает синхронизированные события и даже преобразует сигналы в события для обработки. Сначала посмотрите на структуру события.

  1. libevent использует связанный список для хранения зарегистрированных и активированных событий, ev_next — связанный список всех зарегистрированных событий, ev_active_next — связанный список активных событий, а ev_signal_next — связанный список событий сигнала. События времени управляются минимальной кучей. Использование минимальной кучи является очень эффективным способом. Каждый раз необходимо оценивать только события в верхней части кучи. Если временные события в верхней части кучи не готовы, то следующее время не должно быть готово.

  2. У каждого цикла событий есть event_base, который используется для планирования событий, а ev_base указывает на цикл событий, в котором находится событие.

  3. ev_events указывает тип событий, о которых заботится событие, что может быть в следующих ситуациях:

// 时间事件
#define EV_TIMEOUT  0x01
// 可读事件
#define EV_READ     0x02
// 可写事件
#define EV_WRITE    0x04
// 信号
#define EV_SIGNAL   0x08
// 标识是否为永久事件。非永久事件激活一次后,就会从注册队列中删除,如果想继续监听该事件,需要再次加入事件队列。而永久事件则激活后不会从注册事件中删除,除非自己手动删除。
#define EV_PERSIST  0x10    /* Persistant event */
  1. Если событие является событием времени, то ev_timeout является тайм-аутом для этого события.

  2. События в libevent могут использовать приоритет, события с более высоким приоритетом всегда обрабатываются первыми, а ev_pri является приоритетом события.

  3. ev_callback — функция обратного вызова, соответствующая событию.При срабатывании события функция обратного вызова будет вызвана для обработки.

struct event {
    /*
    ** libevent 用双向链表来保存注册的所有事件,包括IO事件,信号事件。
    ** ev_next 存储了该事件在事件链表中的位置
    ** 另外,libevent 还用另一个链表来存储激活的事件,通过遍历激活的事件链表来分发任务
    ** ev_active_next 存储了该事件在激活事件链表中的位置
    ** 类似,ev_signal_next 就是该事件在信号事件链表中的位置
    */
	TAILQ_ENTRY (event) ev_next;
	TAILQ_ENTRY (event) ev_active_next;
	TAILQ_ENTRY (event) ev_signal_next;
    /* libevent 用最小堆来管理超时时间,min_heap_idx 保存堆顶的 index */
	unsigned int min_heap_idx;	/* for managing timeouts */

    /* event_base 是整个事件循环的核心,每个 event 都处在一个 event_base 中,ev_base 保存这个结构体的指针 */
	struct event_base *ev_base;
    /* 对于 IO 事件,ev_fd 是绑定的文件描述符,对于 signal 事件,ev_fd 是绑定的信号 */
	int ev_fd;
    /* 要处理的事件类型, */
	short ev_events;
    /* 事件就绪执行时,调用ev_callback的次数,通常为1 */
	short ev_ncalls;
	short *ev_pncalls;	/* Allows deletes in callback */
    /* 事件超时的时间长度 */
	struct timeval ev_timeout;
    /* 优先级 */
	int ev_pri;		/* smaller numbers are higher priority */
    /* 响应事件时调用的callback函数 */
	void (*ev_callback)(int, short, void *arg);
	void *ev_arg;

	int ev_res;		/* result passed to event callback */
    /* 表示事件所处的状态 */
	int ev_flags;
};

Существует три основных API для обработки событий: event_set, event_add, event_del.

event_setevent_set используется для инициализации объекта события

void event_set(struct event *ev, int fd, short events,
      void (*callback)(int, short, void *), void *arg)
{
    /* Take the current base - caller needs to set the real base later */
    /* current_base 是一个全局变量,ev_base 会默认指向这个变量,
    ** 之后 ev_base 也可以通过 event_base_set 设置指向指定的 event_base 
    ** 特别是对于一个进程中有多个 event_base 的情况下,需要绑定到指定的 event_base 上*/
    ev->ev_base = current_base;

    ev->ev_callback = callback;
    ev->ev_arg = arg;
    ev->ev_fd = fd;
    ev->ev_events = events;
    ev->ev_res = 0;
    ev->ev_flags = EVLIST_INIT;
    ev->ev_ncalls = 0;
    ev->ev_pncalls = NULL;

    min_heap_elem_init(ev);

    /* by default, we put new events into the middle priority */
    /* 设定默认优先级为最大优先级的一半 */
    if(current_base)
        ev->ev_pri = current_base->nactivequeues/2;
}

event_addevent_add похож на добавление зарегистрированного события в очередь событий.Если событие прослушивает событие чтения, запись или сигнал, оно будет сначала добавлено в систему мультиплексирования ввода-вывода, а затем добавлено в список зарегистрированных событий. Если параметр tv не равен NULL, событие также будет зарегистрировано в минимальной куче временных событий.

int event_add(struct event *ev, const struct timeval *tv)
{
    // 要注册的evbase
    struct event_base *base = ev->ev_base;
    const struct eventop *evsel = base->evsel;
    void *evbase = base->evbase;
    int res = 0;

    event_debug((
         "event_add: event: %p, %s%s%scall %p",
         ev,
         ev->ev_events & EV_READ ? "EV_READ " : " ",
         ev->ev_events & EV_WRITE ? "EV_WRITE " : " ",
         tv ? "EV_TIMEOUT " : " ",
         ev->ev_callback));

    // 校验没有设其他的标志位
    assert(!(ev->ev_flags & ~EVLIST_ALL));

    /*
     * prepare for timeout insertion further below, if we get a
     * failure on any step, we should not change any state.
     */
    // 分配最小堆插入一个元素的内存,先分配内存是为了保证时间事件
    if (tv != NULL && !(ev->ev_flags & EVLIST_TIMEOUT)) {
        if (min_heap_reserve(&base->timeheap,
            1 + min_heap_size(&base->timeheap)) == -1)
            return (-1);  /* ENOMEM == errno */
    }

    /* ev_events 监听的事件类型为读写或者信号 而且 该事件没有被注册过,也不在激活队列里 */
    if ((ev->ev_events & (EV_READ|EV_WRITE|EV_SIGNAL)) &&
        !(ev->ev_flags & (EVLIST_INSERTED|EVLIST_ACTIVE))) {
        /* 将事件注册到 IO 多路复用中 */
        res = evsel->add(evbase, ev);
        if (res != -1)
            /* 注册成功后将事件加入到 event_base 的事件链表中 */
            event_queue_insert(base, ev, EVLIST_INSERTED);
    }

    /* 
     * we should change the timout state only if the previous event
     * addition succeeded.
     */
    if (res != -1 && tv != NULL) {
        struct timeval now;

        /* 
         * we already reserved memory above for the case where we
         * are not replacing an exisiting timeout.
         */
        /* 如果事件已经在定时事件中了,则从时间事件链表中删除该事件 */
        if (ev->ev_flags & EVLIST_TIMEOUT)
            event_queue_remove(base, ev, EVLIST_TIMEOUT);

        /* Check if it is active due to a timeout.  Rescheduling
         * this timeout before the callback can be executed
         * removes it from the active list. */
        /* 如果事件已经在激活队列中,则从激活队列中删除该事件 */
        if ((ev->ev_flags & EVLIST_ACTIVE) &&
            (ev->ev_res & EV_TIMEOUT)) {
            /* See if we are just active executing this
             * event in a loop
             */
            if (ev->ev_ncalls && ev->ev_pncalls) {
                /* Abort loop */
                *ev->ev_pncalls = 0;
            }
            
            event_queue_remove(base, ev, EVLIST_ACTIVE);
        }
        // 获取当前时间
        gettime(base, &now);
        // 计算超时时间
        evutil_timeradd(&now, tv, &ev->ev_timeout);

        event_debug((
             "event_add: timeout in %ld seconds, call %p",
             tv->tv_sec, ev->ev_callback));
        // 插入到定时时间事件队列中
        event_queue_insert(base, ev, EVLIST_TIMEOUT);
    }

    return (res);
}

цикл событий

После того, как событие введено, вводится основная база event_base планирования событий, которая определена в заголовочном файле event-internal.h.

Во-первых, в event_base есть член evsel, который сохраняет указатель функции ресурсов мультиплексирования ввода-вывода Структура eventop следующая:

struct eventop {
    const char *name;
    void *(*init)(struct event_base *);
    int (*add)(void *, struct event *);
    int (*del)(void *, struct event *);
    int (*dispatch)(struct event_base *, void *, struct timeval *);
    void (*dealloc)(struct event_base *, void *);
    /* set if we need to reinitialize the event base */
    int need_reinit;
};

Для каждого IO-мультиплексирования реализовано несколько методов init, add, del и dispatch: init — инициализация, add — добавление события, del — удаление события, diapatch — ожидание активации события и обработка активированных событий отдельно , событие. В event_base также есть элемент evbase, который содержит ресурсы для мультиплексирования ввода-вывода. Например, первый параметр функции добавления — void*, и этот void* нужно передать в evbase. На самом деле этот подход заключается в достижении полиморфизма с помощью указателей на функции C. Если это объектно-ориентированный язык, он не должен быть таким сложным. Тем не менее, этот способ реализации полиморфизма на C все еще стоит изучения.

В event_base есть элемент activequeues, который необходимо объяснить, и который является указателем на указатель. Как упоминалось ранее, очереди активации имеют приоритеты, а события активации с одинаковым приоритетом помещаются в связанный список, поэтому головные узлы нескольких очередей активации с разными приоритетами образуют очередь. Итак, вот указатель на указатель.

struct event_base {
    /* eventop 对象指针,决定了使用哪种IO多路复用资源 
    ** 但是 eventop 实际上只保存了函数指针,最后资源的句柄是保存在 evbase 中。
    ** 比如要使用 epoll,那么就应该有一个 epoll 的文件描述符,eventop 中只保存了epoll相关的add,del等函数
    ** epoll 的文件描述符是保存在 evbase 中的,因此调用的形式就是 evsel->add(evbase, ev);
    */
    const struct eventop *evsel;
    void *evbase;
    /* event base 上所有事件的数量包括注册事件和激活事件
    ** 在 event_queue_insert 函数中加 1 */
    int event_count;        /* counts number of total events */
    /* event base 上被激活的事件的数量 */
    int event_count_active; /* counts number of active events */

    int event_gotterm;      /* Set to terminate loop */
    int event_break;        /* Set to terminate loop immediately */

    /* active event management */
    /* libevent 支持事件的优先级,对于激活的事件,不同优先级的事件存储在不同的链表中 
    ** 然后再用一个链表把这些链表串起来
    */
    struct event_list **activequeues;
    /* 事件可以设定的最大优先级 */
    int nactivequeues;

    /* signal handling info */
    struct evsignal_info sig;
    /* 保存所有注册事件的链表 */
    struct event_list eventqueue;
    /* 上一次进行事件循环的时间 */
    struct timeval event_tv;
    /* 管理时间事件的小顶堆 */
    struct min_heap timeheap;

    struct timeval tv_cache;
};

Следующие функции в основном связаны с event_base: event_base_new, event_base_free, event_base_loop.

event_base_new и event_base_free предназначены для выделения ресурсов event_base и освобождения ресурсов event_base соответственно, что легче понять. Сначала создайте базу событий через event_base_new, затем создайте разные события и зарегистрируйте их в базе событий и, наконец, запустите цикл событий через event_base_loop. Если вы хотите выйти из цикла событий, вы можете вызвать event_base_loopbreak или event_loopexit_cb.

Ядром цикла обработки событий является вызов функции диспетчеризации мультиплексирования ввода-вывода.Следует отметить, что перед вызовом функции диспетчеризации она сначала вычисляет, как давно произошло последнее событие времени, а затем использует эту разницу во времени в качестве времени когда отправка заблокирована, чтобы на событие времени можно было отреагировать своевременно, не ожидая слишком много времени из-за слишком долгой блокировки мультиплексирования ввода-вывода. Это обычная практика, и цикл событий Redis также реализован таким образом.

int
event_base_loop(struct event_base *base, int flags)
{
    const struct eventop *evsel = base->evsel;
    void *evbase = base->evbase;
    struct timeval tv;
    struct timeval *tv_p;
    int res, done;

    /* clear time cache */
    base->tv_cache.tv_sec = 0;

    if (base->sig.ev_signal_added)
        evsignal_base = base;
    done = 0;
    while (!done) {
        /* Terminate the loop if we have been asked to */
        /* 调用 event_loopexit_cb 跳出循环,为什么搞了两个函数? */
        if (base->event_gotterm) {
            base->event_gotterm = 0;
            break;
        }

        /* 调用 event_base_loopbreak 函数跳出循环 */
        if (base->event_break) {
            base->event_break = 0;
            break;
        }

        /* You cannot use this interface for multi-threaded apps */
        while (event_gotsig) {
            event_gotsig = 0;
            if (event_sigcb) {
                res = (*event_sigcb)();
                if (res == -1) {
                    errno = EINTR;
                    return (-1);
                }
            }
        }

        /* 矫正时间 */
        timeout_correct(base, &tv);

        tv_p = &tv;
        /* 如果没有激活事件,且等待方式不是非阻塞,计算当前时间距离最小堆堆顶时间事件的时间差,作为阻塞的时间 */
        if (!base->event_count_active && !(flags & EVLOOP_NONBLOCK)) {
            timeout_next(base, &tv_p);
        } else {
            /* 
             * if we have active events, we just poll new events
             * without waiting.
             */
            /* 如果有激活事件,将阻塞时间设置为 0 */
            evutil_timerclear(&tv);
        }
        
        /* If we have no events, we just exit */
        /* 如果已经没有事件了,则退出循环 */
        if (!event_haveevents(base)) {
            event_debug(("%s: no events registered.", __func__));
            return (1);
        }

        /* update last old time */
        /* 更新事件循环的时间 */
        gettime(base, &base->event_tv);

        /* clear time cache */
        /* 清空时间缓存 */
        base->tv_cache.tv_sec = 0;
        /* 调用 IO 多路复用函数等待事件就绪,就绪的信号事件和IO事件会被插入到激活链表中 */
        res = evsel->dispatch(base, evbase, tv_p);

        if (res == -1)
            return (-1);
        /* 写时间缓存 */
        gettime(base, &base->tv_cache);
        /* 检查heap中的时间事件,将就绪的事件从heap中删除并插入到激活队列中 */
        timeout_process(base);
        /* 如果有激活的信号事件和IO时间,则处理 */
        if (base->event_count_active) {
            event_process_active(base);
            if (!base->event_count_active && (flags & EVLOOP_ONCE))
                done = 1;
        } else if (flags & EVLOOP_NONBLOCK)
            /* 如果采用非阻塞的方式 */
            done = 1;
    }

    /* clear time cache */
    base->tv_cache.tv_sec = 0;

    event_debug(("%s: asked to terminate loop.", __func__));
    return (0);
}

Суммировать

Ядро событийного цикла libevent описано выше, а соответствующие китайские комментарии даны на моем github для деталей реализации. Кроме того, в интернете есть картинка, очень наглядно описывающая событийный цикл libevent, я нарисовал ее еще раз, немного доработал и выложил для связи с вами.

libevent 事件循环