博客一级目录
二级目录——libco源码分析/学习笔记
由于本源代码蛮长的,所以按照功能划分模块来分析,分为若干部分,详见二级目录↑
代码文件:co_routine.h,co_routine.cpp,co_routine_inner.h
看了eventloop的代码后不禁感叹:妙啊!!!
libco管理定时事件便是使用时间轮这种数据结构
定时器前驱知识本篇只稍微提一下,具体知识请参考《Linux高性能服务器编程 游双 著》第11章,或其他方式学习。
先说有序链表方式:
将事件按照超时时间以升序的方式串起来(很显然添加一个事件的复杂度为O(n)),然后链表头部的事件就是最接近超时的事件的定时器。
时间堆:
时间堆是对有序链表的优化,利用堆我们可以O(logn)插入,O(1)取最接近超时的事件。除了用堆优化了以外其他跟上面差不多。
时间轮:(以下大部分描述来自于《Linux高性能服务器编程》)
时间轮是对有序链表的一种优化,通过一种hash的思想使得添加定时事件的时间复杂度降到接近O(1),是有序链表管理方式的一种改进,大大提高了效率。
上图方格代表事件(的定时器),轮盘上槽的数字=超时时间%(轮子转一圈所需时间) (%为取余运算)。
轮子中间的箭头是一个指针,每次轮子转动它都会指向下一格,而每次转动的时间(槽间隔ti)称为一个滴答时间,它实际上就是一个心博时间。该时间轮共有N个槽,因此它运行一周的时间为N×si。轮盘上每个槽都串了一个链表(无序),表示余数为槽内数值的定时器的集合,每个槽的链表(也就是这个集合)上所有的定时器具有相同的特征:它们的定时时间相差N×si的整数倍。时间轮正是利用这个关系将定时器散列到不同链表中的。假设我们现在指针指向槽cs,我们要添加一个定时时间为ti的定时器,则该定时器将被插入到槽ts对应的链表中:
ts=(cs+(ti/si))%N
很显然,对于时间轮而言,要提高定时精度,就要使si足够小;要提高执行效率,则要N值足够大。
上述图是简单时间轮(只有一个轮子)。而复杂实践论可能有多个轮子,不同轮子拥有不同粒度。相邻两个轮子,精度高的转一圈精度低的仅移动一槽,就像水表一样。
定时器总是离不开定时的。
先说一种计时方法。
static unsigned long long getCpuKhz(); 作用是获得cpu频率,如果你看不懂getCpuKhz这个函数,可以打开/proc/cpuinfo看一眼,就可以知道这里记载的是cpu的动态信息。
counter()主要是调用rdtscp这条汇编指令,将计数(来一个时钟脉冲+1)读出来。 将总共的时钟脉冲数读出再除以cpu的频率(每秒时钟脉冲)就是时间。
虽然对于现代计算机都是可以变频的,但这里不会有影响,查阅INTEL,有
1.The time stamp counter in newer processors may support an enhancement, referred to as invariant TSC.
Processor’s support for invariant TSC is indicated by CPUID.80000007H:EDX[8].
2.The invariant TSC will run at a constant rate in all ACPI P-, C-. and Tstates. This is the architectural behavior
moving forward. On processors with invariant TSC support, the OS may use the TSC for wall clock timer
services (instead of ACPI or HPET timers). TSC reads are much more efficient and do not incur the overhead
associated with a ring transition or access to a platform resource.
也就是说rdtsc或rdtscp不用考虑CPU的变频问题,他会以固定的速率增加。
static unsigned long long counter(void)
{
register uint32_t lo, hi;
register unsigned long long o;
__asm__ __volatile__ (
"rdtscp" : "=a"(lo), "=d"(hi)::"%rcx"
);//eax寄存器的值赋给lo,edx赋给hi
o = hi;//o为64位,将hi先放在低32位
o <<= 32;//移到高位
return (o | lo);//将lo放在低32位return
}
方法1:主要是使用counter将总共的时钟脉冲数读出再除以cpu的频率(每秒时钟脉冲)就是时间
方法2:gettimeofday自然不用说,好处是跨平台不用切换到内核态。读取1970年1月1日到现在的时间。
static unsigned long long GetTickMS()//返回值单位ms
{
#if defined( __LIBCO_RDTSCP__)
static uint32_t khz = getCpuKhz();
return counter() / khz;
#else
struct timeval now = { 0 };
gettimeofday( &now,NULL );
unsigned long long u = now.tv_sec;
u *= 1000;
u += now.tv_usec / 1000;
return u;
#endif
}
成员加括号的原因是:时间轮不是以类的形式给出的。libco整体上的风格都是手动实现面向对象的操作。
struct stTimeout_t:时间轮结构体
struct stTimeout_t
{
stTimeoutItemLink_t *pItems;//时间轮盘数组(连续地址的链表),只有两个成员:head,tail。
//尤其注意即使用了链表的数据结构,但是地址连续,可以使用基址+下标访问。
int iItemSize; //时间轮尺寸(槽个数N)也是最大计时时间。
unsigned long long ullStart;//时间轮起始时间(时间基址)
long long llStartIdx;//时间轮指针当前指向的位置。但是这个指针要对iItemSize取余才是真正的指针。
};
定时器(事件)结构体。
struct stTimeoutItem_t
{
enum
{
eMaxTimeout = 40 * 1000 //40s
};
stTimeoutItem_t *pPrev; //链表结构
stTimeoutItem_t *pNext;
stTimeoutItemLink_t *pLink;
unsigned long long ullExpireTime;//超时的绝对时间
//以下两个处理函数,互斥执行
OnPreparePfn_t pfnPrepare;
OnProcessPfn_t pfnProcess;//调用此函数会导致eventloop交出执行权,返回co_inner_poll继续执行。
void *pArg;
// 在co_poll_inner中存放的是stCoRoutine_t指针,用于保存回到co_poll_inner的协程控制字指针。
bool bTimeout;//是否超时。1=超时
};
TakeAllTimeout():让时间轮转动一下,并取出所有因此timeout的时间(定时器)。
好吧,看这个函数的时候本来很顺利地看了大部分然后看到join的操作之后把之前的思路全盘推翻......
事情是这样的:
libco中的时间轮跟上面讲解的不太一样。libco的时间轮有一个最大计时时间:itemsize(从这里可以看出每个槽对应时间为1)。超过这个时间的时间会向co_log_err中写一条错误信息,然后把超时时间强行赋值为itemsize-1(最大超时时间)。
由于GetTickMS()返回值单位是ms,所以时间轮时间单位也是ms。
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 )//error
{
return ;
}
int cnt = allNow - apTimeout->ullStart + 1;//计算时间差
if( cnt > apTimeout->iItemSize )
{//若时间差过大,应该是error了,但是为了保证健壮性则 假装 只过了允许的最大超时时间活在过去。
cnt = apTimeout->iItemSize;
//经过几次最大超时时间后应该可以撵上正确的时间。在eventloop中有相应的实现保证最终会正确超时。
}
if( cnt < 0 )
{//当前时间比开始时间还早就肯定是error了,说明传参出了问题,取不到任何事件。
return;
}
for( int i = 0;illStartIdx + i) % apTimeout->iItemSize;
Join( apResult,apTimeout->pItems + idx );
}
apTimeout->ullStart = allNow;//让起始时间=当前时间。
apTimeout->llStartIdx += cnt - 1;//但是这个指针要对iItemSize取余才是真正的指针。
}
再来看看如何往时间轮中加入定时器(事件),跟取出相对应比较相似。前面健壮性描述就不再重复了。
参数1:时间轮
参数2:插入的定时器。包含超时的绝对时间点
参数3:当前时间。(用超时的绝对时间点-当前时间=相对超时时间)
int AddTimeout( stTimeout_t *apTimeout,stTimeoutItem_t *apItem ,
unsigned long long allNow )
{
if( apTimeout->ullStart == 0 )//init
{
{
apTimeout->ullStart = allNow;
apTimeout->llStartIdx = 0;
}
if( allNow < apTimeout->ullStart )//err
{
co_log_err("CO_ERR: AddTimeout line %d allNow %llu apTimeout->ullStart %llu",
__LINE__,allNow,apTimeout->ullStart);
return __LINE__;
}
if( apItem->ullExpireTime < allNow )//err
{
co_log_err("CO_ERR: AddTimeout line %d apItem->ullExpireTime %llu allNow %llu apTimeout->ullStart %llu",
__LINE__,apItem->ullExpireTime,allNow,apTimeout->ullStart);
return __LINE__;
}
unsigned long long diff = apItem->ullExpireTime - apTimeout->ullStart;
//相对超时时间(偏移时间)
if( diff >= (unsigned long long)apTimeout->iItemSize )
{
//diff超过了最大计时时间,error,但是为了保证健壮性将diff变为最大计时时间。
/* 这里要注意的是,虽然这里超出了允许的最大时间且报error,但是在eventloop中有相应的设计使得这个
计时器可以真正地在应该超时的地方超时,不会出错。做法是:这个计时器可以被提前取出,但是取出后发现它
还没有超时,就再将它放回去,如此经过几次进进出出后就可以延迟到它应该到达的时间点。感慨一下工程师设
计精妙吧。
*/
diff = apTimeout->iItemSize - 1;
co_log_err("CO_ERR: AddTimeout line %d diff %d",
__LINE__,diff);
//return __LINE__;
}
AddTail( apTimeout->pItems + ( apTimeout->llStartIdx + diff ) % apTimeout->iItemSize , apItem );
//将定时器加入到时间轮相应位置。
return 0;
}
先看数据结构。顺便感慨一下万物皆对象。
事件循环结构体。
struct stCoEpoll_t
{
int iEpollFd; //epoll文件号,跟co_poll搭配(见后面的讲解),后面用co_epollwait函数从中取active事件。
static const int _EPOLL_SIZE = 1024 * 10;
struct stTimeout_t *pTimeout;//时间轮
struct stTimeoutItemLink_t *pstTimeoutList;//timeout链表 结构体:只有head,tail。
struct stTimeoutItemLink_t *pstActiveList;
//active链表,每次eventloop后把所有active事件处理完毕后此链表为空,包含两部分事件:1.epoll就绪事件、2.时间轮超时事件。
co_epoll_res *result;
};
然后是事件循环。
参数1:事件循环结构体。
参数2:函数结束之前调用了一下(如果不为NULL的话),推测是某个管理函数。
参数3:pfn的参数。
eventloop是跟co_poll_inner配合使用的。
有个问题:如果通过co_poll函数添加的事件还没有超时就触发了,那么会把它从epoll中删掉,但是时间轮中还在呀?那这样会不会就多触发一次?
void co_eventloop( stCoEpoll_t *ctx,pfn_co_eventloop_t pfn,void *arg )
{
if( !ctx->result )//若没有分配空间,就分配
{
ctx->result = co_epoll_res_alloc( stCoEpoll_t::_EPOLL_SIZE );
}
co_epoll_res *result = ctx->result;
for(;;)
{
int ret = co_epoll_wait( ctx->iEpollFd,result,stCoEpoll_t::_EPOLL_SIZE, 1 );
//从epoll结构中选取active事件摘下存放在result链表里。
stTimeoutItemLink_t *active = (ctx->pstActiveList);
//active事件链表,该链表同时包含epoll函数active事件和实践论上timeout的事件。
stTimeoutItemLink_t *timeout = (ctx->pstTimeoutList);//timeout事件链表,链表为空。
memset( timeout,0,sizeof(stTimeoutItemLink_t) );
//之前的timeout事件已经全都处理完毕了(链表为空),把结构清空(head和tail清空)。
for(int i=0;ievents[i].data.ptr;
//从链表result中取出一个active事件。
if( item->pfnPrepare )
//若有预处理函数就调用预处理函数,就不进一步操作了。可见pfnPrepare和pfnProcess是互斥关系。
{
item->pfnPrepare( item,result->events[i],active );
}
else
{
AddTail( active,item );//没有的话就把它加入到active链表中。
}
}
unsigned long long now = GetTickMS();
//获取当前绝对时间,与时间轮起始时间的时间差作为一个滴答时间
TakeAllTimeout( ctx->pTimeout,now,timeout );
//让时间轮转动一下,并取出所有因此timeout的事件(定时器)。
stTimeoutItem_t *lp = timeout->head;
while( lp )
{
//printf("raise timeout %p\n",lp);
lp->bTimeout = true;//标记为超时
lp = lp->pNext;
}
Join( active,timeout );
//把超时事件链表也添加到active链表里。
lp = active->head;
while( lp )
{
PopHead( active );
//将一个active事件取下来
if (lp->bTimeout && now < lp->ullExpireTime)
{
//如果是超时事件,而且时间没超时。
/*触发条件:如果向时间轮加入的计时器超时时间过大且超过了最大计时时间,然后虽然会报error但是还会
加入到时间轮中。后来将它取出时,它没有真正的超时,也就是绝对超时时间大于当前时间,就会触发这个条件。
这样的话,我们将它重新加入到时间轮中继续等待,如此循环,直到真正满足它的时间点后就不会触发这个条,
这样就很完美地解决了时间过长无法存入的问题。
这里不得不感叹一下腾讯工程师设计的精妙。
*/
int ret = AddTimeout(ctx->pTimeout, lp, now);
if (!ret) //ret==0表示AddTimeout成功(时间超过最大超时时间也会返回0)。
{
lp->bTimeout = false;//重置计时器为未超时。
lp = active->head;
continue;
}
//因为代码里写的时间已经是满足ret==0的条件了,若这样还是不成功,那就是天意了。
}
if( lp->pfnProcess )//若存在就执行一下这个函数。
{//此函数会使eventloop放弃执行权,转到co_poll_inner继续执行。
//poll_inner上交执行权之后会从这继续执行,完成收尾工作,lp会被从链表中摘下销毁。
lp->pfnProcess( lp );
}
lp = active->head;
}
if( pfn )//推测是钩子函数
{
if( -1 == pfn( arg ) )
{
break;
}
}
}
}
看了eventloop后,是不是感觉很精巧啊。
看一下各种结构体。
struct stPollItem_t : public stTimeoutItem_t//继承了定时器
{
struct pollfd *pSelf;
stPoll_t *pPoll;
struct epoll_event stEvent;
};
struct stPoll_t : public stTimeoutItem_t //继承了定时器
{
struct pollfd *fds;
nfds_t nfds; // typedef unsigned long int nfds_t;
stPollItem_t *pPollItems;
int iAllEventDetach;//记录OnPollPreparePfn函数是否被调用过
int iEpollFd;
int iRaiseCnt;//prepare函数调用次数(OnPollPreparePfn函数)
};
这两个函数功能相同(只是最后两个参数顺序不同),co_poll直接调用并返回co_poll_inner的返回值。
所以我们只说co_poll_inner功能:
int co_poll_inner( stCoEpoll_t *ctx,struct pollfd fds[], nfds_t nfds, int timeout, poll_pfn_t pollfunc);
功能:io复用,可以参照Linux下的poll。它内部是用epoll来实现的,但是外部表现是poll的功能。所有事件都是one shot(触发之后就被epoll_ct删掉)。
参数1:eventloop控制块指针。因为poll要跟eventloop搭配。(数据结构在eventloop代码的上面)
参数2:监听的句柄数组,struct pollfd是Linux poll用到的数据结构。将会把fds数组中的事件放到epoll结构里(并不会销毁fds这个数组)。
参数3:用于标记数组fds中的结构体元素的总数量。
参数4:超时时间ms,若此参数不为空,则此时间不仅仅被加入到epoll中监听,还会被加入到事件轮中等待超时同样会转为active。
参数5:函数指针,超时(active)事件,定义如下:
typedef int (*poll_pfn_t)(struct pollfd fds[], nfds_t nfds, int timeout);
int co_poll_inner( stCoEpoll_t *ctx,struct pollfd fds[], nfds_t nfds, int timeout, poll_pfn_t pollfunc)
{
if (timeout == 0)
{
return pollfunc(fds, nfds, timeout);
}
if (timeout < 0)
{
timeout = INT_MAX;
}
int epfd = ctx->iEpollFd;
stCoRoutine_t* self = co_self();
//1.struct change
stPoll_t& arg = *((stPoll_t*)malloc(sizeof(stPoll_t))); //stPoll_t结构体
//以下代码为stPoll_t结构体初始化。这里代码作者使用的是引用&(感觉这个源码风格不统一可能是多人协作)
memset( &arg,0,sizeof(arg) );
arg.iEpollFd = epfd;//来自定时器
arg.fds = (pollfd*)calloc(nfds, sizeof(pollfd));//struct pollfd 申请了nfds个
arg.nfds = nfds;
stPollItem_t arr[2]; //临时内存池,不一定会使用到它,(取栈内存比向os申请快得多)。
if( nfds < sizeof(arr) / sizeof(arr[0]) && !self->cIsShareStack)
{//第一个条件判断了一下数组开的是不是够大,第二个条件检查share Stack的许可。
//(这里为什么要检查share shack许可啊?难道是因为arr无法持久?)
arg.pPollItems = arr;//若够大且允许,就使用数组。
}
else//否则还是老老实实地跟os申请
{
arg.pPollItems = (stPollItem_t*)malloc( nfds * sizeof( stPollItem_t ) );
}//pPollItems最终结果是指向了恰好是nfds*sizeof(stPollItem_t)大小的连续内存
memset( arg.pPollItems,0,nfds * sizeof(stPollItem_t) );//内存清零 还是初始化
arg.pfnProcess = OnPollProcessEvent;
//pfnProcess是定时器成员,OnPollProcessEvent函数指针,此函数作用是将ap中pArg保存的stCoRoutine_t*取出,赋予执行权。(回到本函数)
arg.pArg = GetCurrCo( co_get_curr_thread_env() );
//上面两句使eventloop可以很容易定位并回到co_poll_inner继续执行。
//2. add epoll
for(nfds_t i=0;i -1 )
{
ev.data.ptr = arg.pPollItems + i;
ev.events = PollEvent2Epoll( fds[i].events );//co_poll用的事件类型转为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);
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 );//往时间轮中加计时器
int iRaiseCnt = 0;
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;
iRaiseCnt = -1;
}
else
{
co_yield_env( co_get_curr_thread_env() );
//交出执行权,这里会等待eventloop被调用,由eventloop将co_poll事件处理完毕会重新将执行权归还本函数。
iRaiseCnt = arg.iRaiseCnt;
}
{//收尾工作。
//clear epoll status and memory
RemoveFromLink( &arg );
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 );
}//del
fds[i].revents = arg.fds[i].revents;
}
if( arg.pPollItems != arr )
{
free( arg.pPollItems );
arg.pPollItems = NULL;
}
free(arg.fds);
free(&arg);
}
return iRaiseCnt;
}