libevent监听的event有以下几种
- 文件描述符/套接字,没有设定超时时长
- 信号
- 文件描述符/套接字,设定超时时长
对于时间,libevent内部的时间管理是通过最小堆实现的,原因如下
- 既然某些fd有规定的超时时长,那么io多路复用函数就不能永久阻塞,需要设定一个超时时长(最后一个参数)
- 用户在使用event_add设定的时间是相对于event_add调用的相对时间,这就导致所有具有超时时长的event什么时候超时是杂乱无章的,没办法为io函数设定某个时长
针对以上原因,libevent内部将所有的event超时时长全部转化为绝对时间,有以下几点好处
- 可以对所有的超时时间进行排序,获得最早超时的那个event
- 判断是否超时时只需和现有时间比较大小,不需要每次都进行相对时间的判断
- 可以采用最小堆存储所有具有超时时间的event,如果堆顶event未超时,那么所有的event都不会超时,可以选择这个event的超时时长最为io函数的阻塞时间
使用绝对时间带来的麻烦是如果event是一个永久事件,那么当event被激活后仍然需要重新注册到base中,此时因为event的时间是绝对时间的缘故,不能够直接调用event_add_internal添加event,而是需要重新计算超时时间再添加,这就导致仍然需要在event中存储用户提供的超时时长,在重新添加之前计算绝对时间的超时时间
/*
* event_add调用的内部函数,用于将event添加到base的注册队列中
* 同时添加到相应的map中
*
* 注意:这个函数不仅仅只由event_add调用,还有event_persist_closure调用
* 由这个函数调用是因为当具有超时时间的event被激活后,需要先从base中的所有队列中删除
* 然后重新计算超时时间,再重新添加到base中,所以又重新调用了这个函数
*
* 注意:event不仅代表文件描述符,还有可能是信号的event,当是信号时,会递归
* 调用两遍这个函数,第一遍调用时判断是信号则调用evsig_map_add函数,在这个函数中
* 进行两步
* 将信号event添加到base的信号map中
* 调用evsigops的add函数,即调用evsig_add,这个函数中绑定内部信号处理函数,同时将socketpair的event
* 添加到base中,使用event_add,也就是调用event_add_internal
* 不过只会执行两遍,因为在evsig_add中会进行判断,只有第一次添加socketpair的event时才会执行第二次调用
*
* 见evsig_add
*/
static inline int
event_add_internal(struct event *ev, const struct timeval *tv,
int tv_is_absolute)
{
struct event_base *base = ev->ev_base;
int res = 0;
int notify = 0;
EVENT_BASE_ASSERT_LOCKED(base);
/* ... */
/*
* prepare for timeout insertion further below, if we get a
* failure on any step, we should not change any state.
*/
/*
* 这一步主要是用来让最小堆增加一个位置,并没有实际添加到最小堆上
* 判断条件是这是一个具有超时时间的event,同时在最小堆中没有这个event
* 这样就需要在最小堆上留出一个位置来存放这个event
* 因为用户可以对同一个event调用event_add多次,这就可能两次event_add除了超时时间不同
* 其他的都相同,这样就不需要在留出一个位置,直接替换以前的就可以
*
* 如果已经在最小堆中,ev_flags将是EVLIST_TIMEOUT
*/
if (tv != NULL && !(ev->ev_flags & EVLIST_TIMEOUT)) {
if (min_heap_reserve(&base->timeheap,
1 + min_heap_size(&base->timeheap)) == -1)
return (-1); /* ENOMEM == errno */
}
/*
* we should change the timeout state