集成定时器事件

一,定时器事件

1、概述

libevent提供了高性能定时器的功能,方便执行延迟回调逻辑。在添加事件监听的时候,可以不指定fd和监听的事件,指定超时的时间,实现定时器功能。定时器的实现主要依赖下面的数据结构,如下:
1)最小堆:按事件的超时时间构造,超时时间最早的在堆顶。
2)公共超时队列:这个可以个性化设置每个队列的超时时间,添加事件时时间相同的放到同一个队列里,在最小堆里只添加一个队列首部的事件到最小堆。能避免大量事件都放到最小堆,导致最小堆添加、删除时性能变差的问题。

2、实现原理

Libevent实现超时事件的原理是,多路IO复用函数都是有一个超时值。如果用户需要Libevent同时监听多个超时event,那么Libevent就把超时值最小的那个作为多路IO复用函数的超时值。自然,当时间一到,就会从多路IO复用函数返回,此时对超时event进行处理即可。当运行事件循环时,根据timer-heap中的事件最小超时时间,计算系统I/O demultiplexer的最大等待时间,下面是event_base_loop函数对应的代码段,如下:

3、超时事件管理

3.1、小根堆

Libevent内部使用小根堆来管理超时事件,堆的实现采用了典型的数组来维护,当数组元素满时扩容数组调整最小堆。堆中的元素是struct event,按照事件的ev_timeout字段对超时时间进行排序,如下:

集成定时器事件_第1张图片

注意:小根堆按照ev->ev_timeout值(绝对超时时间)进行排序

3.2、往min_heap中添加event

每一个event_base都对应一个min_heap数据结构,event_base中的所有设置了超时的event都会放在event_base的timeheap这一成员中,其定义如下:

struct event_base {
	......
	/** Priority queue of events with timeouts. */
	struct min_heap timeheap;
	......
};

event添加一个超时是通过event_add实现的,而在event_add内部实际上是event_add_internal函数,该函数共有三个传入参数,第一个参数是event指针,第二个参数是一个超时结构体timeval,第三个参数用于指明传入的超时结构体是否为绝对时间。如果传入的timeval非空,说明event是需要设置超时的,通过event_add_internal就可以将该event添加到min_heap中,如下所示:

static inline int
event_add_internal(struct event *ev, const struct timeval *tv,
    int tv_is_absolute)  
{   
	/* 处理超时事件 */
	if (res != -1 && tv != NULL) {
		......
		common_timeout = is_common_timeout(tv, base);
		if (tv_is_absolute) {  //如果是绝对时间 就直接用ev_timeout存储
			ev->ev_timeout = *tv;
		} else if (common_timeout) {  
                ......
		} else {
			evutil_timeradd(&now, tv, &ev->ev_timeout); //如果就只是一个普通的相对时间,就直接用系统时间加上超时时长作为超时时间
		}
		......
		event_queue_insert(base, ev, EVLIST_TIMEOUT); //插入到超时队列中
		......
}

3.3、min_heap中event的激活

在event_base_loop函数中会调用timeout_process函数去处理定时器中超时的event,该函数定义如下:

static void
timeout_process(struct event_base *base)
{
	/* Caller must hold lock. */
	struct timeval now;
	struct event *ev;

	if (min_heap_empty_(&base->timeheap)) {
		return;
	}

	gettime(base, &now);

	while ((ev = min_heap_top_(&base->timeheap))) {
		/* 比较定时器堆顶的event是否超时,如果没有超时说明定时器中没有event超时 */
		if (evutil_timercmp(&ev->ev_timeout, &now, >))
			break;

		/* delete this event from the I/O queues */
		event_del_nolock_(ev, EVENT_DEL_NOBLOCK);

		event_debug(("timeout_process: event: %p, call %p",
			 ev, ev->ev_callback));
		/* 按超时激活类型添加到激活队列中 */
		event_active_nolock_(ev, EV_TIMEOUT, 1);
	}
}

定时器min_heap中event的激活是通过timeout_process函数实现的,在该函数中会把已经超时的event从定时器中删除,并且把该event添加到激活队列中。接着只需要处理激活队列,就可以执行这些超时的event对应的回调函数。

4、公共超时事件

4.1、超时时间相关的概念

1)超时时间:是一个绝对超时时间,即从1970年1月1日到当前的某个时间点经过的时间
2)超时时长:是一个相对超时时间,例如:设置的超时时长是2秒,即在2秒后会超时
3)公共超时时间:带有COMMON_TIMEOUT_MAGIC标志(Libevent定义的一个宏)的一个超时时间

4.2、common_timeout的作用

common_timeout把base中所有拥有共同点的event放在了一起,而这个所谓的“共同点”就是指超时时长相同,这些超时时长相同的event,他们的超时时间是不同的。 拥有相同超时时长的所有event构成一个链表events,并且让它们按照超时时间的先后按升序排列(即相同超时时长中最先超时的那个event放在最前面),而events中设置一个内部使用的timeout_event作为代表,把最先超时的那个event的超时时间添加到timeout_event中,然后把超时事件放到min_heap中,当放到min_heap中的timeout_event超时,就回到events中,从前往后把所有超时的event全部激活。如下:

集成定时器事件_第2张图片

4.3、common_timeout的结构定义

在event_base的结构体中,含有以下定义:

struct event_base
{
    ......
 
    /** An array of common_timeout_list* for all of the common timeout
	 * values we know. */
	struct common_timeout_list **common_timeout_queues;   //common_timeout_list *数组,存放不同超时时长的common_timeout_list的指针
	/** The number of entries used in common_timeout_queues */
	int n_common_timeouts;  //common_timeout_queues中实际的元素个数
	/** The total size of common_timeout_queues. */
	int n_common_timeouts_allocated;  //common_timeout_queues的容量
    ......
}

在event_base中,common_timeout_queues是一个common_timeout_list *类型的指针数组,其中每个元素都指向一个common_timeout_list,common_timeout_list的定义如下:

//event-internal.h
struct common_timeout_list {
	/* List of events currently waiting in the queue. */
	struct event_list events;  //event的双向链表
	/* 'magic' timeval used to indicate the duration of events in this
	 * queue. */
	struct timeval duration;  //该common_timeout_list的超时时长,events双向链表中的所有event都是相同的超时时长
	/* Event that triggers whenever one of the events in the queue is
	 * ready to activate */
	struct event timeout_event;  //“event代表”,最终只有这个event实际插到了min_heap中
	/* The event_base that this timeout list is part of */
	struct event_base *base;  //该common_timeout_list所在的event_base
};
 
//event_struct.h
 
TAILQ_HEAD (event_list, event);  //由event组成的双向链表

4.4、common_timeout与一般timeout的区分

对于一个timeval超时结构体来说,它有两个成员,一个数tv_sec用来指明超时时间中的秒数,一个就是tv_usec用来指明超时时间中的微秒数。由于微秒的数值范围只能是0~999999,而tv_usec的变量类型实际上是32位的,能表示的数值范围远远大于999999,因此用低20位足以来表示timeval中的tv_usec,这样一来,tv_usec的高12位就是没有使用的。而libevent中则是通过这高12位来区分一个timeval超时结构体是common_timeout还是普通的timeout。有如下定义:

//event-internal.h
#define COMMON_TIMEOUT_MICROSECONDS_MASK       0x000fffff   //取低20位掩码
 
//event.c
#define MICROSECONDS_MASK       COMMON_TIMEOUT_MICROSECONDS_MASK   //取低20位,即微秒超时时长
#define COMMON_TIMEOUT_IDX_MASK 0x0ff00000   //20~27bit为该超时时长在common_timeout_queues中的位置
#define COMMON_TIMEOUT_IDX_SHIFT 20          //微秒最大为999999,因此用低20位存储即可,高12位中前4位标识是否为common_timeout 
#define COMMON_TIMEOUT_MASK     0xf0000000   //取高四位掩码
#define COMMON_TIMEOUT_MAGIC    0x50000000   //高四位标志是否为common timeout

1)timeval结构体中的tv_usec由32位表示,而实际上微秒的数值只需低20位即可表示,因此,tv_usec & MICROSECONDS_MASK 即可得到低20位的值,也就是实际的微秒数值。
2)对于高12位来说,tv_usec的高4位用来判断一个这个timeval是否是common_timeout,用 tv_usec & COMMON_TIMEOUT_MASK可以获取高4位的值,如果高4位是COMMON_TIMEOUT_MAGIC(即0x5000000),说明这个timeval是一个common_timeout,否则就表示这个timeval只是一个普通的超时时间。
3)如果是common_timeout,那么这个event是放在base的common_timeout_queues某一项(如common_timeout_queues[i])所对应的common_timeout_list中的,而tv_usec剩下的20~27bit则用来表示这个common_timeout所在的common_timeout_list在common_timeout_queues数组中的索引。例如:如果tv_usec的20-27bit为00000101,则说明这个tv_usec对应的common_timeout_list放在common_timeout_queues[5]下面

4.5、获取common_timeout在common_timeout_queues中的下标

通过下面的宏可以获取common_timeout对应的下标,将tv_usec与COMMON_TIMEOUT_IDX_MASK按位与,屏蔽掉tv_usec除2027bit以外的其他位,得到的结果再右移20位得到的结果就是原来的tv_usec的2027bit的值,这个值就是common_timeout对应的common_timeout_list对应在common_timeout_queues中的下标。宏定义如下:

#define COMMON_TIMEOUT_IDX(tv) \ //获取tv所在的common_timeout_list在common_timeout_queues中的位置
	(((tv)->tv_usec & COMMON_TIMEOUT_IDX_MASK)>>COMMON_TIMEOUT_IDX_SHIFT)//20~27bit右移20位得到下标

4.6、判断一个timeval是否为common_timeout

调用下面的函数可以判断一个超时时间是否是一个common_timeout。 该函数先取出tv_usec的高四位来检查传入的timeval是否为common_timeout,在此基础上还要去判断tv_usec的20~27bit所对应的下标是否合法,因为有可能存在高四位表明为common_timeout,但是20-27bit是非法的情况。

static inline int
is_common_timeout(const struct timeval *tv,
    const struct event_base *base)
{
	int idx;
	if ((tv->tv_usec & COMMON_TIMEOUT_MASK) != COMMON_TIMEOUT_MAGIC)//取高4位,COMMON_TIMEOUT_MAGIC说明它是一个common timeout,如果高四位不等于COMMON_TIMEOUT_MAGIC,那么就不是commontimeout
		return 0;
	idx = COMMON_TIMEOUT_IDX(tv);
	return idx < base->n_common_timeouts; //下标必须小于 base中common_timeout_queues的实际元素个数
}

4.7、创建一个common_timeout

调用event_base_init_common_timeout接口可以创建一个common_timeout,下面是接口定义:

const struct timeval *
event_base_init_common_timeout(struct event_base *base,
    const struct timeval *duration) //查看base中是否有duration相应的common_timeout_list,如果没有就分配一个,并且将新分配中的timeout_event进行设置回调函数。传入的duration既可以是带掩码的也可以是不带掩码的,返回的是相应的common_timeout_list的duration
{
	int i;
	struct timeval tv;
	const struct timeval *result=NULL;
	struct common_timeout_list *new_ctl;
 
	EVBASE_ACQUIRE_LOCK(base, th_base_lock);
	if (duration->tv_usec > 1000000) { //微秒最大值应该是999999,如果超过了1000000,要么它是一个common_timeout,就取出实际的超时时间,否则就把微秒进位到秒上去
		memcpy(&tv, duration, sizeof(struct timeval));
		if (is_common_timeout(duration, base))
			tv.tv_usec &= MICROSECONDS_MASK;
		tv.tv_sec += tv.tv_usec / 1000000;
		tv.tv_usec %= 1000000;
		duration = &tv;   //更新duration的实际时长
	}
	for (i = 0; i < base->n_common_timeouts; ++i) { //遍历现在有的common_timeout_list,查看是否存在超时时长等于duration的list
		const struct common_timeout_list *ctl =
		    base->common_timeout_queues[i];
		if (duration->tv_sec == ctl->duration.tv_sec &&
		    duration->tv_usec ==
		    (ctl->duration.tv_usec & MICROSECONDS_MASK)) {
			EVUTIL_ASSERT(is_common_timeout(&ctl->duration, base));
			result = &ctl->duration; //如果存在duration等于传入的参数的common_timeout_list,那么就把这个common_timeout_list的duration存到result中返回即可。
			goto done;
		}
	}
	
	if (base->n_common_timeouts == MAX_COMMON_TIMEOUTS) {
		event_warnx("%s: Too many common timeouts already in use; "
		    "we only support %d per event_base", __func__,
		    MAX_COMMON_TIMEOUTS);
		goto done;
	}
	if (base->n_common_timeouts_allocated == base->n_common_timeouts) { //如果base中的common_timeout_list分配满了
		int n = base->n_common_timeouts < 16 ? 16 : //如果少于16则分配16的容量,否则容量加倍
		    base->n_common_timeouts*2;
		struct common_timeout_list **newqueues =
		    mm_realloc(base->common_timeout_queues,
			n*sizeof(struct common_timeout_queue *));//重新分配common_timeout_queues的空间大小
		if (!newqueues) {
			event_warn("%s: realloc",__func__);
			goto done;
		}
		base->n_common_timeouts_allocated = n;  //更新common_timeout_queues地址及其容量
		base->common_timeout_queues = newqueues;
	}
    //执行到这里说明没有common_timeout_list的duration等于传入的参数duration
	new_ctl = mm_calloc(1, sizeof(struct common_timeout_list));   //新分配一个common_timeout_list
	if (!new_ctl) {
		event_warn("%s: calloc",__func__);
		goto done;
	}
	TAILQ_INIT(&new_ctl->events); //初始化该duration对应的events链表为空
	new_ctl->duration.tv_sec = duration->tv_sec;
	new_ctl->duration.tv_usec =
	    duration->tv_usec | COMMON_TIMEOUT_MAGIC |
	    (base->n_common_timeouts << COMMON_TIMEOUT_IDX_SHIFT);  //把微秒转换为为带掩码、并且添上下标位
	evtimer_assign(&new_ctl->timeout_event, base,
	    common_timeout_callback, new_ctl); //给新分配的common_timeout_list中的timeout_event注册信息,回调函数为common_timeout_callback
	new_ctl->timeout_event.ev_flags |= EVLIST_INTERNAL;  //标志为内部使用的event
	event_priority_set(&new_ctl->timeout_event, 0);  //设置timeout_event的优先级为0
	new_ctl->base = base;
	base->common_timeout_queues[base->n_common_timeouts++] = new_ctl; //在common_timeout_queues现有元素的最后加上新创建的common_timeout_list
	result = &new_ctl->duration;  //result保存common_timeout_list的duration
 
done:
	if (result)
		EVUTIL_ASSERT(is_common_timeout(result, base));
 
	EVBASE_RELEASE_LOCK(base, th_base_lock);
	return result;  //返回的result是已经设置过相应标志位的common_timeout,也就是指定了超时时长为duration的common_timeout,之后就可以用result作为参数调用event_add,就可以把event添加到相应的common_timeout_list中
}

4.8、激活common_timeout对应的event

被插入到heap中的timeout_event设置的回调函数是common_timeout_callback,当min_heap中的timeout_event发生超时而激活后,就会直接去调用common_timeout_callback,该函数定义如下:

static void
common_timeout_callback(evutil_socket_t fd, short what, void *arg)
{
	struct timeval now;
	struct common_timeout_list *ctl = arg;  //传入的参数是event所在的那个common_timeout_list
	struct event_base *base = ctl->base;
	struct event *ev = NULL;
	EVBASE_ACQUIRE_LOCK(base, th_base_lock);
	gettime(base, &now);  //获取系统时间
	while (1) {
		ev = TAILQ_FIRST(&ctl->events); //遍历这个common_timeout_list中的所有event,如果有超时的就添加到激活队列中
		if (!ev || ev->ev_timeout.tv_sec > now.tv_sec ||
		    (ev->ev_timeout.tv_sec == now.tv_sec &&
			(ev->ev_timeout.tv_usec&MICROSECONDS_MASK) > now.tv_usec))
			break;
		event_del_internal(ev);
		event_active_nolock(ev, EV_TIMEOUT, 1);
	}
	if (ev) //此时的ev如果不为空,那么它就是未来最先超时的那个event
		common_timeout_schedule(ctl, &now, ev); //重新将这个event的超时时间加上common_timeout_callback添加到min_heap中
	EVBASE_RELEASE_LOCK(base, th_base_lock);
}

common_timeout_callback函数的作用就是遍历common_timeout_list中的event,激活所有超时的event,并且根据未来最先超时的那个event重新设置一个新的“代表”timeout_event插入到min_heap中。

你可能感兴趣的:(libevent深入浅出,c++)