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 的时间轮是一个环形数组的实现,如下图所示:
在这个环形数组中,数组中每个元素代表 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;
}
对于时间轮可以从以下两个思路进行优化:
O(1)
了。相关结构体:
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/