这两天大致看了看
libevent的代码,简单做一个分析.
libevent最大的特点就是封装了对以下三种事件的响应:IO事件,定时器事件,信号事件.这里就分析libevent如果做到这一点的,在libevent中还包括一些其他的功能(如缓冲区),但是我这里就重点讲解这一部分了.
事件原型,简单看一看用于封装事件的结构体定义:
struct event {
TAILQ_ENTRY (event) ev_next;
TAILQ_ENTRY (event) ev_active_next;
TAILQ_ENTRY (event) ev_signal_next;
unsigned int min_heap_idx; /* for managing timeouts */
struct event_base *ev_base;
int ev_fd;
short ev_events;
short ev_ncalls;
short *ev_pncalls; /* Allows deletes in callback */
struct timeval ev_timeout;
int ev_pri; /* smaller numbers are higher priority */
void (*ev_callback)(int, short, void *arg);
void *ev_arg;
int ev_res; /* result passed to event callback */
int ev_flags;
};
其中的ev_callback就是回调函数,也就是说当所关注的事件发生时所要触发的函数是注册到这个函数指针中的.
1)IO事件:再简单不过了,对select/epoll/poll等之类的调用进行封装即可,所提供的接口无非这几种:
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;
};
在我看过的很多开源服务器源码(如lighttpd)中都有类似的封装,不是什么新鲜的东西.
2)定时器事件:libevent采用堆数据结构存放所要定时的事件的时间,大家知道堆可以用来实现优先队列,在这里,所有的定时器就放在这样的一个数据结构中了.
3)信号事件:所有的信号都注册回调函数为evsignal_handler(在signal.c中),这个函数的功能就是在某信号被触发的时候将该信号被触发的计数器加1,同时置一个标志位表示有信号被触发.
现在,把所有这些结合起来,看看libevent框架的主循环是如何工作的,用简单的伪码表示:
主循环
更新当前时间
将当前时间与存放时间的堆中的时间依次进行比较,由于是采用堆实现的,这里查找相当的快,于是所有可以被触发的定时器事件都从堆中被取出,同时取下的事件被放到一个活动事件的队列中
调用封装IO操作的dispatch函数,在其中也将被触发的IO事件加入到那个存放活动事件的队列中
在dispatch的函数中如果信号被触发的标志位被置位,说明有信号被触发,调用evsignal_process函数,这个函数的功能也是把所有被触发的事件放到活动事件的队列中
好了,现在所有可以被触发的事件都在活动事件队列中了,依次遍历取出来调用它们注册的回调函数就成了.
上面就是libevent处理这三种事件的大体框架.
说一说我认为这个框架存在的缺点:
1) callback函数只能有一个,假设这样一个场景,我需要对某个连接socket同时监控它的可读/可写/超时事件,那么我需要针对同一个socket fd生成三个event对象.
2) 在主循环中,每次都要去查询存放时间的堆看看有没有定时器事件可以被触发,问题在于,很多时候,一个主循环很快就到了下一次,而时间过去的并不多,这次去检查时间是冗余的操作,当然了,由于libevent的定时器是精确到毫秒级别的,所以有这么做的必要,但是在一个真正的服务器中,我怀疑有多少需要精确到微秒级别的事件,所以呢,我觉得这个可以做一个改进,每次更新时间之后跟上一次更新的时间做一个比较,如果超过了一秒(或者把这个间隔改成可以由使用者配置的)再去检查堆上面的时间.