读了一下libevent的部分代码,主要是timer,signal,epoll相关的,学习了网络库如何处理定时器事件和信号。
大多数网络模块的库应该都是这样实现的,很巧妙。
首先说timer,libevent通过一个小根堆结构来保存定时事件,堆顶元素是最近即将超时的时间,例如右5个定时器,分别在2S,1S,4S,7S,8S后超时,那么堆顶的元素就是1S的定时器,这有什么用呢?
因为不管是select/poll/epoll,监控文件描述符的时候都会设置一个超时间隔,我们恰好可以把堆顶元素的超时时间作为这个超时间隔。
还是上面的5个timer,在本次的epoll_wait中我们可以取出堆顶元素1S作为超时间隔,分以下两种情况:
1:如果1S内没有I/O事件,那么epoll_wait将在1S后超时。此时应该触发1S的定时事件。
2:如果1S内发生了I/O事件,那么epoll_wait返回时,1S的定时事件不应该触发。
所以,每次epoll_wait返回时,我们就依次取出堆顶元素,如果超时,就处理超时事件。
伪代码:
while (1)
{
// 从timer_heap中计算本次epoll_wait的超时间隔
int timeout = timer_heap->nearest_timeout(now);
int nfds = epoll_wait(fd, events, events_size, timeout);
....
timer_handler *handler = NULL;
//如果handler != NULL,则<strong>处理</strong>定时事件,可能会有多个<strong>定时器</strong>超时,所以用while循环
while ((hander = timer_heap->check_time_out(now)) != NULL)
handler->process_timeout(now);
}
再说说signal事件,这个也是转化为I/O事件,通过socketpair或者pipe都可以,也就是epoll_wait中监控管道的读端,当接受到信号时,信号处理程序通过管道的写端把信号写进去,从而触发I/O事件,读端把信号读出来。
太巧妙了,把定时器事件和signal事件都转化为I/O事件了。并且定时器也可以精确到ms级别。
我自己写了下,定时器还是相当精确的,误差在1ms以内。
下面的
图就给出了这一基本流程。
1 ) 首先应用程序准备并初始化event,设置好事件类型和回调函数;这对应于前面第步骤
2 和3 ;
2 ) 向libevent 添加该事件 event。对于定时事件,libevent 使用一个小根堆管理,key 为超
时时间;对于 Signal 和I/O 事件,libevent 将其放入到等待链表(wait list)中,这是一
个双向链表结构;
3 ) 程序调用event_base_dispatch() 系列函数进入无限循环,等待事件,以select() 函数为例;
每次循环前libevent 会检查定时事件的最小超时时间 tv ,根据 tv 设置select() 的最大等
待时间,以便于后面及时处理超时事件;
当select() 返回后,首先检查超时事件,然后检查I/O 事件;
11
Libevent 将所有的就绪事件,放入到激活链表中;
然后对激活链表中的事件,调用事件的回调函数执行事件处理;
libevent对event 的管理
从event 结构体中的 3 个链表节点指针和一个堆索引出发,大体上也能窥出 libevent 对
event的管理方法了,可以参见下面的示意图。每次当有事件event转变为就绪状态时,libevent 就会把它移入到active event list[priority]中,其中priority是event的优先级;
接着libevent 会根据自己的调度策略选择就绪事件,调用其cb_callback()函数执行事件
处理;并根据就绪的句柄和事件类型填充cb_callback 函数的参数。