一 event简介
libevent是以event为核心的reactor模型高性能的网络库,事件类型为io时间,定时时间和信号signal事件三种。libevent巧妙的将三种时间统一起来处理极大的简化了网络程序的复杂度。那么libevent是如何管理这三种event呢?
struct event {
TAILQ_ENTRY(event) ev_active_next;
TAILQ_ENTRY(event) ev_next;
/* for managing timeouts */
union {
TAILQ_ENTRY(event) ev_next_with_common_timeout;
int min_heap_idx;
} ev_timeout_pos;
evutil_socket_t ev_fd;
struct event_base *ev_base;
union {
/* used for io events */
struct {
TAILQ_ENTRY(event) ev_io_next;
struct timeval ev_timeout;
} ev_io;
/* used by signal events */
struct {
TAILQ_ENTRY(event) ev_signal_next;
short ev_ncalls;
/* Allows deletes in callback */
short *ev_pncalls;
} ev_signal;
} _ev;
short ev_events;
short ev_res; /* result passed to event callback */
short ev_flags;
ev_uint8_t ev_pri; /* smaller numbers are higher priority */
ev_uint8_t ev_closure;
struct timeval ev_timeout;
/* allows us to adopt for different types of events */
void (*ev_callback)(evutil_socket_t, short, void *arg);
void *ev_arg;
};
libevent使用双向链表保存所有注册的io时间和signal事件,event_next就是该事件在双向链表中的位置指针。除此之外,libevent会把已激活的时间放入一个双向链表,保存所有的已激活事件,ev_active_next就表示了该事件在激活链表中的位置。 min_heap_idx 和 ev_timeout,如果是 timeout 事件,它们是 event 在小根堆中的索引和超时值, libevent 使用小根堆来管理定时事件, ev_base 该事件所属的反应堆实例,这是一个 event_base 结构体,下一节将会详细讲解, ev_fd,对于 I/O 事件,是绑定的文件描述符;对于 signal 事件,是绑定的信号,ev_callback, event 的回调函数,被 ev_base 调用,执行事件处理程序,这是一个函数指针。
从 event 结构体中的 2个链表节点指针和一个堆索引出发,大体上也能窥出 libevent 对event 的管理方法了,可以参见下面的示意图。每次当有事件 event 转变为就绪状态时,libevent 就会把它移入到 active event list[priority]中,其中 priority 是 event 的优先级;接着 libevent 会根据自己的调度策略选择就绪事件,调用其 cb_callback()函数执行事件处理,并根据就绪的句柄和事件类型填充 cb_callback 函数的参数。
二 libevent如何将io,signal和timer时间统一管理
首先我们要了解libevent时间主循环的执行流程:
(1)io和timer时间的统一
Libevent 将 Timer 和 Signal 事件都统一到了系统的 I/O 的 demultiplex 机制中了,首先将 Timer 事件融合到系统 I/O 多路复用机制中,还是相当清晰的,因为系统的 I/O机制像 select()和 epoll_wait()都允许程序制定一个最大等待时间(也称为最大超时时间)timeout,即使没有 I/O 事件发生,它们也保证能在 timeout 时间内返回。那么根据所有 Timer 事件的最小超时时间来设置系统 I/O 的 timeout 时间;当系统 I/O返回时,再激活所有就绪的 Timer 事件就可以了,这样就能将 Timer 事件完美的融合到系统的 I/O 机制中了。这是在 Reactor 和 Proactor 模式(主动器模式,比如 Windows 上的 IOCP)中处理 Timer事件的经典方法了, ACE 采用的也是这种方法。堆是一种经典的数据结构,向堆中插入、删除元素时间复杂度都是 O(lgN), N 为堆中元素的个数,而获取最小 key 值(小根堆)的复杂度为 O(1),因此变成了管理 Timer 事件的绝佳人选(当然是非唯一的), libevent 就是采用的堆结构。
(2) 信号处理
基本方法就是采用“消息机制”。在 libevent 中这是通过 socket pair 完成的,下面就来详细分析一下。Socket pair 就是一个 socket 对,包含两个 socket,一个读 socket,一个写 socket。工作方式如下图所示:
创建一个 socket pair 并不是复杂的操作,可以参见下面的流程图,清晰起见,其中忽略了一些错误处理和检查。
Socket pair 创建好了,可是 libevent 的事件主循环还是不知道 Signal 是否发生了啊,我们还差最后一步,那就是: 为 socket pair 的读 socket 在 libevent 的 event_base 实例上注册一个 persist 的读事件。这样当向写 socket 写入数据时,读 socket 就会得到通知,触发读事件,从而 event_base就能相应的得到通知了,实际上就是在event_base中一个server,然后监听一个连接是否有数据可读。完整的处理框架如下所示: