libevent事件处理框架分析
这两天大致看了看 libevent的代码,简单做一个分析.
libevent最大的特点就是封装了对以下三种事件的响应:IO事件,定时器事件,信号事件.这里就分析libevent如果做到这一点的,在libevent中还包括一些其他的功能(如缓冲区),但是我这里就重点讲解这一部分了.
事件原型,简单看一看用于封装事件的结构体定义:
1)IO事件:再简单不过了,对select/epoll/poll等之类的调用进行封装即可,所提供的接口无非这几种:
2)定时器事件:libevent采用堆数据结构存放所要定时的事件的时间,大家知道堆可以用来实现优先队列,在这里,所有的定时器就放在这样的一个数据结构中了.
3)信号事件:所有的信号都注册回调函数为evsignal_handler(在signal.c中),这个函数的功能就是在某信号被触发的时候将该信号被触发的计数器加1,同时置一个标志位表示有信号被触发.
现在,把所有这些结合起来,看看libevent框架的主循环是如何工作的,用简单的伪码表示:
上面就是libevent处理这三种事件的大体框架.
说一说我认为这个框架存在的缺点:
1) callback函数只能有一个,假设这样一个场景,我需要对某个连接socket同时监控它的可读/可写/超时事件,那么我需要针对同一个socket fd生成三个event对象.
2) 在主循环中,每次都要去查询存放时间的堆看看有没有定时器事件可以被触发,问题在于,很多时候,一个主循环很快就到了下一次,而时间过去的并不多,这次去检查时间是冗余的操作,当然了,由于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就是回调函数,也就是说当所关注的事件发生时所要触发的函数是注册到这个函数指针中的.
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;
};
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)中都有类似的封装,不是什么新鲜的东西.
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;
};
2)定时器事件:libevent采用堆数据结构存放所要定时的事件的时间,大家知道堆可以用来实现优先队列,在这里,所有的定时器就放在这样的一个数据结构中了.
3)信号事件:所有的信号都注册回调函数为evsignal_handler(在signal.c中),这个函数的功能就是在某信号被触发的时候将该信号被触发的计数器加1,同时置一个标志位表示有信号被触发.
现在,把所有这些结合起来,看看libevent框架的主循环是如何工作的,用简单的伪码表示:
主循环
更新当前时间
将当前时间与存放时间的堆中的时间依次进行比较,由于是采用堆实现的,这里查找相当的快,于是所有可以被触发的定时器事件都从堆中被取出,同时取下的事件被放到一个活动事件的队列中
调用封装IO操作的dispatch函数,在其中也将被触发的IO事件加入到那个存放活动事件的队列中
在dispatch的函数中如果信号被触发的标志位被置位,说明有信号被触发,调用evsignal_process函数,这个函数的功能也是把所有被触发的事件放到活动事件的队列中
好了,现在所有可以被触发的事件都在活动事件队列中了,依次遍历取出来调用它们注册的回调函数就成了.
更新当前时间
将当前时间与存放时间的堆中的时间依次进行比较,由于是采用堆实现的,这里查找相当的快,于是所有可以被触发的定时器事件都从堆中被取出,同时取下的事件被放到一个活动事件的队列中
调用封装IO操作的dispatch函数,在其中也将被触发的IO事件加入到那个存放活动事件的队列中
在dispatch的函数中如果信号被触发的标志位被置位,说明有信号被触发,调用evsignal_process函数,这个函数的功能也是把所有被触发的事件放到活动事件的队列中
好了,现在所有可以被触发的事件都在活动事件队列中了,依次遍历取出来调用它们注册的回调函数就成了.
上面就是libevent处理这三种事件的大体框架.
说一说我认为这个框架存在的缺点:
1) callback函数只能有一个,假设这样一个场景,我需要对某个连接socket同时监控它的可读/可写/超时事件,那么我需要针对同一个socket fd生成三个event对象.
2) 在主循环中,每次都要去查询存放时间的堆看看有没有定时器事件可以被触发,问题在于,很多时候,一个主循环很快就到了下一次,而时间过去的并不多,这次去检查时间是冗余的操作,当然了,由于libevent的定时器是精确到毫秒级别的,所以有这么做的必要,但是在一个真正的服务器中,我怀疑有多少需要精确到微秒级别的事件,所以呢,我觉得这个可以做一个改进,每次更新时间之后跟上一次更新的时间做一个比较,如果超过了一秒(或者把这个间隔改成可以由使用者配置的)再去检查堆上面的时间.
posted on 2009-01-03 00:14 那谁 阅读(13512) 评论(4) 编辑 收藏 引用 所属分类: 网络编程 、服务器设计 、libevent
评论
# re: libevent事件处理框架分析 回复 更多评论
libevent的源代码我也看过,不过你说的这两个都不是问题:1) 所关心的不同事件类型可以在注册时指明,不必通过不同的event对象来设置。如:
event_set(&evfifo, socket, EV_READ | EV_WRITE, fifo_read, &evfifo);
而当所关心的事件触发的时候,libevent在调用你的回调函数的时候会指明事件的类型——通过回调函数的第二个参数(是一个|值)。你只要在回调函数判断一下事件类型即可。
2) libevent不会做多余的检查。主循环的timeout_next会取出距离下一个超时时间有多久(最小堆的取最小值操作,是O(1)的复杂度),而这个timeout最终会交给类似于select/poll/epoll_wait这样的系统调用,作为timeout。也就说,在这个timeout时间内,如果没有在select/epoll上注册的事件发生,libevent是阻塞了的。libevent的这种处理方式已经是惯例了的。你那种“改进”不精确,也不高效。
以上如有谬误,欢迎互相交流指出 :-)
# re: libevent事件处理框架分析 回复 更多评论
@Joshua Zhu受教了,谢谢~~
# re: libevent事件处理框架分析 回复 更多评论
callbal函数确实可以设定多个事件。第二个问题还没看到。学习libevent中。
# re: libevent事件处理框架分析 回复 更多评论
@Joshua Zhunginx 呵呵
我记得你的ppt里边有nginx关于超时的处理哈,就是你说的第二点。
膜拜