libco源码剖析(3)-定时器与事件注册

libco源码剖析(3)-定时器与事件注册

引言

libco源码剖析(1) - 共享栈与协程的创建

libco源码剖析(2)-协程生命周期与协程调度

本文会继续介绍libco定时器的实现和协程事件的注册。

定时器

服务器程序通常需要处理众多定时事件,如何有效地组织与管理这些定时事件对服务器的性能至关重要。为此,我们要将每个定时事件分别封装成定时器,并使用某种容器类数据结构,比如链表、排序链表和时间轮,将所有定时器串联起来,以实现对定时事件的统一管理。

在libco 中,使用了单级时间轮来管理其内部的超时事件。对超时事件的添加删除查询操作均可以达到 O(1) 的时间复杂度,是一个非常高效的数据结构。

struct stTimeout_t
{
	/*
	   时间轮
	   超时事件数组,总长度为iItemSize,每一项代表1毫秒,为一个链表,代表这个时间所超时的事件。

	   这个数组在使用的过程中,会使用取模的方式,把它当做一个循环数组来使用,虽然并不是用循环链表来实现的
	*/
	stTimeoutItemLink_t *pItems;
	int iItemSize;   // 默认为60*1000

	unsigned long long ullStart; //目前的超时管理器最早的时间
	long long llStartIdx;  //目前最早的时间所对应的pItems上的索引
};

/*
* 超时链表
*/
struct stTimeoutItemLink_t
{
	stTimeoutItem_t *head;
	stTimeoutItem_t *tail;

};

/*
* 超时链表中的一个项
*/
struct stTimeoutItem_t
{
	enum
	{
		eMaxTimeout = 40 * 1000 //40s
	};

	stTimeoutItem_t *pPrev;	// 前一个元素
	stTimeoutItem_t *pNext; // 后一个元素

	stTimeoutItemLink_t *pLink; // 该链表项所属的链表

	unsigned long long ullExpireTime;

	OnPreparePfn_t pfnPrepare;  // 预处理函数,在eventloop中会被调用
	OnProcessPfn_t pfnProcess;  // 处理函数 在eventloop中会被调用

	void *pArg; // self routine pArg 是pfnPrepare和pfnProcess的参数

	bool bTimeout; // 是否已经超时
};

libco 的时间轮是一个环形数组的实现,如下图所示:

libco源码剖析(3)-定时器与事件注册_第1张图片

在这个环形数组中,数组中每个元素代表 1ms。而 libco 将环形数组的总长度设为 60*1000, 即最多可以表达 1 分钟以内的超时事件,且超时精度是毫秒。而且,有可能会有多个超时事件在同一时刻发生,因此数组中的元素是个链表,代表同在该时刻触发的超时事件。在 libco 初始化时,ullStart 被初始化为当前时刻的时间戳 (单位为毫秒),llStartIdx 初始化为 0。

向定时器里添加事件:

/*
* 将事件添加到定时器中
* @param apTimeout - (ref) 超时管理器
* @param apItem    - (in) 即将插入的超时事件
* @param allNow    - (in) 当前时间
*/
int AddTimeout( stTimeout_t *apTimeout,stTimeoutItem_t *apItem ,unsigned long long allNow )
{

	// 当前时间管理器的最早超时时间
	if( apTimeout->ullStart == 0 )
	{ 
		// 设置时间轮的最早时间是当前时间
		apTimeout->ullStart = allNow;
		// 设置最早时间对应的index 为 0
		apTimeout->llStartIdx = 0;
	}

	if( allNow < apTimeout->ullStart )
	{
		co_log_err("CO_ERR: AddTimeout line %d allNow %llu apTimeout->ullStart %llu",
					__LINE__,allNow,apTimeout->ullStart);

		return __LINE__;
	}

	if( apItem->ullExpireTime < allNow )
	{
		co_log_err("CO_ERR: AddTimeout line %d apItem->ullExpireTime %llu allNow %llu apTimeout->ullStart %llu",
					__LINE__,apItem->ullExpireTime,allNow,apTimeout->ullStart);

		return __LINE__;
	}

	// 计算当前事件的超时时间和超时管理器的最早时间的差距
	int diff = apItem->ullExpireTime - apTimeout->ullStart;

	if( diff >= apTimeout->iItemSize )
	{
		co_log_err("CO_ERR: AddTimeout line %d diff %d",
					__LINE__,diff);

		return __LINE__;
	}
	/*
	  计算出该事件的超时事件在超时管理器所在的槽的位置
	  apTimeout->pItems + ( apTimeout->llStartIdx + diff ) % apTimeout->iItemSize , apItem );
	  
	  然后在该位置的槽对应的超时链表的尾部添加一个事件
	*/
	AddTail( apTimeout->pItems + ( apTimeout->llStartIdx + diff ) % apTimeout->iItemSize , apItem );

	return 0;
}

取出超时事件:

/*
* 以allNow参数为截止时间,取出所有的超时事件
*
* @param apTimeout (in) 超时管理器
* @param allNow (in)  截止时间 也就是当前时刻
* @param apResult (out) 最终的超时事件结果会放入此表中
*/
inline void TakeAllTimeout( stTimeout_t *apTimeout,unsigned long long allNow,stTimeoutItemLink_t *apResult )
{
	if( apTimeout->ullStart == 0 )
	{
		apTimeout->ullStart = allNow;
		apTimeout->llStartIdx = 0;
	}

	// 如果当前时间还未达到最早的超时时间,则直接返回
	if( allNow < apTimeout->ullStart )
	{
		return ;
	}

	// 用当前时间减去最早超时时间,因为时间轮里面的每一个槽代表了1ms。
	// 因此cnt刚好就代表了超时的槽数
	int cnt = allNow - apTimeout->ullStart + 1;

	if( cnt > apTimeout->iItemSize )
	{
		cnt = apTimeout->iItemSize;
	}
	if( cnt < 0 )
	{
		return;
	}

	for( int i = 0;i<cnt;i++)
	{
		int idx = ( apTimeout->llStartIdx + i) % apTimeout->iItemSize;

		// 把该格子上的所有超时时间都放进去(同一时刻可能有多个超时时间)
		Join<stTimeoutItem_t,stTimeoutItemLink_t>( apResult,apTimeout->pItems + idx  );
	}
	apTimeout->ullStart = allNow;
	apTimeout->llStartIdx += cnt - 1;
}

对于时间轮可以从以下两个思路进行优化:

  1. 单级时间轮的优化。我们可以对 libco 的单级时间轮做一些简单的优化,例如给每个超时事件加一个 rotation 参数,代表该超时事件会在第几轮触发,这样就可以在一个单级时间轮中存放无限长的超时事件了。但这样代价是超时事件的判断和取出将不会是 O(1) 了。
  2. 多级时间轮。Linux 内核中就采用了多级时间轮的机制,模拟了现实生活中水表刻度。即第一级的时间轮与普通的单级时间轮相同,而第二级时间轮的每个元素的时长等于第一级时间轮的全部总时长,依次类推。Linux 内核中一共采用了五级时间轮。第一级的时间轮所有事件消耗完成后,会触发第二级时间轮的事件迁移。

注册事件

相关结构体:

struct stPoll_t : public stTimeoutItem_t 
{
	struct pollfd *fds;
	nfds_t nfds; // typedef unsigned long int nfds_t;

	stPollItem_t *pPollItems;  // 其中的 pPollItems

	int iAllEventDetach; // 标识是否已经处理过了这个对象了

	int iEpollFd;

	int iRaiseCnt;  // poll的active的事件个数
};

struct stPollItem_t : public stTimeoutItem_t
{
	struct pollfd *pSelf;
	stPoll_t *pPoll;

	struct epoll_event stEvent;
};

stPoll_t对应每个pollfd结构体数组,stPollItem_t对应每个pollfd结构体。

以下函数是注册事件的主要函数,将要监测的事件(每个stPollItem_t对应一个fd)添加到epoll中,以及添加定时事件(stPoll_t对应一个定时事件),值得注意的是,协程在此函数中会发生切换。(和上篇文章中co_eventloop函数进行切换)

/**
* @param ctx epoll上下文
* @param fds[] fds 要监听的文件描述符 原始poll函数的参数,
* @param nfds  nfds fds的数组长度 原始poll函数的参数
* @param timeout timeout 等待的毫秒数 原始poll函数的参数
* @param pollfunc 原始的poll函数, g_sys_poll_func
*/
int co_poll_inner( stCoEpoll_t *ctx,struct pollfd fds[], nfds_t nfds, int timeout, poll_pfn_t pollfunc)
{
	if( timeout > stTimeoutItem_t::eMaxTimeout )
	{
		timeout = stTimeoutItem_t::eMaxTimeout;
	}

	int epfd = ctx->iEpollFd;

	// 获取当前协程
	stCoRoutine_t* self = co_self();

	//1.struct change
	stPoll_t& arg = *((stPoll_t*)malloc(sizeof(stPoll_t)));
	memset( &arg,0,sizeof(arg) );

	arg.iEpollFd = epfd;
	arg.fds = (pollfd*)calloc(nfds, sizeof(pollfd));
	arg.nfds = nfds;

	stPollItem_t arr[2];
	if( nfds < sizeof(arr) / sizeof(arr[0]) && !self->cIsShareStack)
	{
		// 如果监听的描述符只有1个或者0个, 并且目前的不是共享栈模型
		arg.pPollItems = arr;
	}
	else
	{
		// 如果监听的描述符在2个以上,或者协程本身采用共享栈
		arg.pPollItems = (stPollItem_t*)malloc( nfds * sizeof( stPollItem_t ) );
	}
	
	memset( arg.pPollItems,0,nfds * sizeof(stPollItem_t) );

	// 当事件到来的时候,就调用这个callback。
	// 这个callback内部做了co_resume的动作
	arg.pfnProcess = OnPollProcessEvent;
	

	// 保存当前协程,便于调用OnPollProcessEvent时恢复协程
	arg.pArg = GetCurrCo( co_get_curr_thread_env() );
	
	//2. add epoll
	for(nfds_t i=0;i<nfds;i++)
	{
		// 将事件添加到epoll中
		arg.pPollItems[i].pSelf = arg.fds + i;
		arg.pPollItems[i].pPoll = &arg;

		// 设置一个预处理的callback
		// 这个函数会在事件active的时候首先触发
		arg.pPollItems[i].pfnPrepare = OnPollPreparePfn; 

		struct epoll_event &ev = arg.pPollItems[i].stEvent;

		// 如果大于-1,说明要监听fd的相关事件了
		// 否则就是个timeout事件
		if( fds[i].fd > -1 )
		{
			// 这个相当于是个userdata, 当事件触发的时候,可以根据这个指针找到之前的数据
			ev.data.ptr = arg.pPollItems + i;

			// 将poll的事件类型转化为epoll
			ev.events = PollEvent2Epoll( fds[i].events );

			// 将fd添加入epoll中
			int ret = co_epoll_ctl( epfd,EPOLL_CTL_ADD, fds[i].fd, &ev );

			if (ret < 0 && errno == EPERM && nfds == 1 && pollfunc != NULL)
			{
				// 如果注册失败
				if( arg.pPollItems != arr )
				{
					free( arg.pPollItems );
					arg.pPollItems = NULL;
				}
				free(arg.fds);
				free(&arg);
				
				// 使用最原生的poll函数
				return pollfunc(fds, nfds, timeout);
			}
		}
		//if fail,the timeout would work
	}

	//3.add timeout
	// 获取当前时间
	unsigned long long now = GetTickMS();

	arg.ullExpireTime = now + timeout;	
	
	// 将其添加到超时链表中
	int ret = AddTimeout( ctx->pTimeout,&arg,now );

	// 如果出错了
	if( ret != 0 )
	{
		co_log_err("CO_ERR: AddTimeout ret %d now %lld timeout %d arg.ullExpireTime %lld",
				ret,now,timeout,arg.ullExpireTime);
		errno = EINVAL;

		if( arg.pPollItems != arr )
		{
			free( arg.pPollItems );
			arg.pPollItems = NULL;
		}
		free(arg.fds);
		free(&arg);

		return -__LINE__;
	}

	// 注册完事件,就yield。切换到其他协程
	// 当事件到来的时候,就会调用callback。
	co_yield_env( co_get_curr_thread_env() );

	// --------------------分割线---------------------------
	// 注意:!!这个时候,已经和上面的逻辑不在同一个时刻处理了
	// 这个时候,协程已经resume回来了!!

	// 清理数据

	// 将该项从超时链表中删除
	RemoveFromLink<stTimeoutItem_t,stTimeoutItemLink_t>( &arg );

	// 将该项涉及事件全部从epoll中删除掉
	// 事件一定要删除,不删除会出现误resume的问题
	for(nfds_t i = 0;i < nfds;i++)
	{
		int fd = fds[i].fd;
		if( fd > -1 )
		{
			co_epoll_ctl( epfd,EPOLL_CTL_DEL,fd,&arg.pPollItems[i].stEvent );
		}
		fds[i].revents = arg.fds[i].revents;
	}

	// 释放内存啦
	int iRaiseCnt = arg.iRaiseCnt;
	if( arg.pPollItems != arr )
	{
		free( arg.pPollItems );
		arg.pPollItems = NULL;
	}

	free(arg.fds);
	free(&arg);

	return iRaiseCnt;
}


参考资料

https://www.cyhone.com/articles/time-wheel-in-libco/

你可能感兴趣的:(c++,后端,linux,服务器)