libevent中最重要的两个数据结构莫过于event 跟event_base。弄明白这两个数据结构基本上也就弄明白libevent了。
libevent中,不论是IO事件,timeout事件,还是signal事件,都是用struct event来描述。具体的实现代码如下。
struct event {
TAILQ_ENTRY(event) ev_active_next;
TAILQ_ENTRY(event) ev_next;
/* 用来描述timeout事件 */
union {
TAILQ_ENTRY(event) ev_next_with_common_timeout;
int min_heap_idx;
} ev_timeout_pos;
evutil_socket_t ev_fd; //对IO事件而言,为文件描述符。对信号事件而言,为绑定的信号。
struct event_base *ev_base;//绑定的event_base
union {
/* 用来描述IO事件 */
struct {
TAILQ_ENTRY(event) ev_io_next;
struct timeval ev_timeout;
} ev_io;
/* 用来描述signal事件 */
struct {
TAILQ_ENTRY(event) ev_signal_next;
short ev_ncalls;
short *ev_pncalls;
} ev_signal;
} _ev;
short ev_events;//事件类型
short ev_res; //传递给回调函数的结果
short ev_flags; //事件的状态标志
ev_uint8_t ev_pri; //事件的优先级。值越小优先级越高
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; //事件回调函数参数
};
我用的代码是2.0.22-stable版。这个版本中的代码比之前比较旧的版本实现得更清晰。
事件类型由ev_events成员描述,总共有三种:IO事件、timeout事件、signal事件.EV_READ和EV_WRITE都是用来描述IO事件。
#define EV_TIMEOUT 0x01
#define EV_READ 0x02
#define EV_WRITE 0x04
#define EV_SIGNAL 0x08
#define EVLIST_TIMEOUT 0x01 // event在time堆中
#define EVLIST_INSERTED 0x02 // event在已注册事件链表中
#define EVLIST_SIGNAL 0x04 // 未见使用
#define EVLIST_ACTIVE 0x08 // event在激活链表中
#define EVLIST_INTERNAL 0x10 // 内部使用标记
#define EVLIST_INIT 0x80 // event已被初始化
我们可以看到描述事件的结构体中有两个尾队列:ev_active_next,ev_next。ev_next用来描述已经注册的事件链表。ev_active_next用来描述激活链表。
//
如果事件类型是IO事件,那么事件还会处于ev_io_next队列。如果事件类型是signal事件,那么事件还会处于ev_signal_next队列。如果是timeout事件,则有两种可能:要不处于ev_next_with_common_timeout队列,要不处于最小堆。libevent默认将timeout事件放在最小堆中。如果timeout事件数量很多,那么可以将事件放在ev_next_with_common_timeout队列,那样处理起来速度会更快。
Reactor模式大致由如下几个部分组成:handle/EventHandler事件源/事件处理接口、Synchrounous Event Demultiplexer事件多路复用器、Reactor反应器、Concrete Event handler特定事件处理接口。盗用一下别人的图。
handle即事件源。libevent中的信号、文件描述符都是handle。
同步事件多路复用器实际上是内核实现的一个函数。这个函数会阻塞等待已经注册事件集合。当有注册事件发生时,事件进入就绪状态,复用器会通知反应器。反应器收到通知后就可以调用事件的回调函数处理事件。linux系统下的select, poll,epoll都是Demultiplexer。libevent把这些复用器叫做backend,并用struct eventop来描述。
反应器的功能如下:
struct event_base {
const struct eventop *evsel;
void *evbase;
struct common_timeout_list **common_timeout_queues;
struct min_heap timeheap;//管理timeout事件的最小堆
struct event_list *activequeues;//就绪队列。
struct event_list eventqueue;//所有调用event_add接口的事件都会被加入到这个队列,调用后event状态为EVLIST_INSERTED.
struct event_signal_map sigmap;
struct event_io_map io;
struct timeval event_tv;
struct timeval tv_cache;
};
struct event_signal_map {
void **entries;
int nentries;
};
entries是一个数组。如果事件类型为信号事件,则数组成员类型为struct evmap_signal。如果事件类型为IO事件,则数组成员为struct evmap_io。这两个成员的数据结构如下:
struct evmap_io {
struct event_list events;
ev_uint16_t nread;
ev_uint16_t nwrite;
};
struct evmap_signal {
struct event_list events;
};
Q: event_base中为什么会存在这两个数据成员?
A: 因为libevent允许将同一个fd/signal映射到不同的事件,即fd/signal—->struct event*的映射。struct event_signal_map中的数组entries的索引就是fd/signal的值,对应的事件struct event则存放在evmap_io/evmap_signal中的尾队列events中。在windows中,文件描述符是一个比较大的值,不适合放到event_signal_map结构的数组中,libevent会先将fd值做一个hash然后再用链表定址存放在struct event_map_entry的尾队列中。linux下由于遵循POSIX标准的文件描述符是从0开始递增的,一般都不会太大,可以用event_signal_map中的数组来映射fd的值。windows下fd映射到struct event的具体实现分析可以参考这篇:Libevent源码分析—–event_io_map哈希表
每个event_base就相当于一个Reactor框架。
上面我们介绍了Reactor模式中reactor反应器的三个主要功能:注册/注销事件、运行事件主循环、等待事件就绪然后调用事件处理函数。在libevent中实现这些功能的对应接口如下: