libco源码解析---co_eventloop

引言

  我们总能在运行libco协程代码的最后看到对于函数co_eventloop的调用,它可以理解为主协程执行的函数。我们举一个简单的例子来说明它的作用:

 1 void* routinefun(void* args){
 2     co_enable_hook_sys();
 3     while(true){
 4         poll(NULL, 0, 1000);
 5     }
 6     return 0;
 7 }
 8 
 9 int main(int argc,char *argv[])
10 {
11     vector<task_t> v;
12     for(int i=1;i<argc;i+=2)
13     {
14         task_t task = { 0 };
15         SetAddr( argv[i],atoi(argv[i+1]),task.addr );
16         v.push_back( task );
17     }
18 
19     for(int i=0;i<2;i++)
20     {
21         stCoRoutine_t *co = 0;
22         co_create( &co,NULL,routinefun,v2 );
23         printf("routine i %d\n",i);
24         co_resume( co );
25     }
26 
27     co_eventloop( co_get_epoll_ct(),0,0 );
28 
29     return 0;
30 }

  这段代码非常简单,主协程运行两个协程,协程函数所做的事情就是使用poll切换执行权,并在一秒后切换回来(超时)。这里线程的执行过程是这样的,我们把主协程看做A,其他两个协程看做BC。执行过程为:

  • B协程执行,使用poll把一个stPoll_t结构插入时间轮,切换执行权,回到A协程。
  • C协程执行,使用poll把一个stPoll_t结构插入时间轮,切换执行权,回到A协程。
  • 此时A协程执行Eventloop中,不停的循环,直到B协程注册的事件超时,调用回调回到B协程。
  • B协程继续执行,再次使用poll,重复第一步,回到A协程。
  • A协程继续执行Eventloop,不停的循环,直到C协程注册的事件超时,调用回调回到C协程。
  • C协程继续执行,再次使用poll,重复第二步,回到A协程。

  这样我们就可以看清楚co_eventloop到底做了什么,其实就是不停的轮询等待其他协程注册的事件成立,仅此而已。主协程就相当于libco非对称协程当中的一个特殊的调度器,负责唤醒协程B和协程C,它绝不会通过yied操作主动让出CPU,但是可以通过co_resume。唤醒另一个协程,唤醒协程的回调函数OnSignalProcessEvent或OnPollProcessEvent。源码如下:

 1 static void OnSignalProcessEvent( stTimeoutItem_t * ap )
 2 {
 3     stCoRoutine_t *co = (stCoRoutine_t*)ap->pArg;
 4     co_resume( co );
 5 }
 6 
 7 void OnPollProcessEvent( stTimeoutItem_t * ap )
 8 {
 9     stCoRoutine_t *co = (stCoRoutine_t*)ap->pArg;
10     co_resume( co );
11 }

 

co_eventloop

co_eventloop函数源码如下:

  1 /*
  2 * libco的核心调度
  3 * 在此处调度三种事件:
  4 * 1. 被hook的io事件,该io事件是通过co_poll_inner注册进来的
  5 * 2. 超时事件
  6 * 3. 用户主动使用poll的事件
  7 * 所以,如果用户用到了三种事件,必须得配合使用co_eventloop
  8 *
  9 * @param ctx epoll管理器
 10 * @param pfn 每轮事件循环的最后会调用该函数
 11 * @param arg pfn的参数
 12 */
 13 void co_eventloop( stCoEpoll_t *ctx,pfn_co_eventloop_t pfn,void *arg )
 14 {
 15     if( !ctx->result )// 给结果集分配空间
 16     {
 17         // _EPOLL_SIZE:epoll结果集大小
 18         ctx->result =  co_epoll_res_alloc( stCoEpoll_t::_EPOLL_SIZE );
 19     }
 20     co_epoll_res *result = ctx->result;
 21 
 22 
 23     for(;;)
 24     {
 25         //调用 epoll_wait() 等待 I/O 就绪事件,为了配合时间轮⼯作,这里的 timeout设置为 1 毫秒。
 26         int ret = co_epoll_wait( ctx->iEpollFd,result,stCoEpoll_t::_EPOLL_SIZE, 1 );
 27 
 28         /**
 29          * 获取激活事件队列和定时超时事件的临时存放链表
 30          * 不使用局部变量的原因是epoll循环并不是元素的唯一来源.例如条件变量相关(co_routine.cpp stCoCondItem_t)
 31          */
 32         stTimeoutItemLink_t *active = (ctx->pstActiveList);
 33         stTimeoutItemLink_t *timeout = (ctx->pstTimeoutList);
 34 
 35         //初始化timeout
 36         memset( timeout,0,sizeof(stTimeoutItemLink_t) );
 37 
 38         /**
 39          * 处理返回的结果集,如果pfnPrepare不为NULL,就直接调用注册的回调函数进行处理
 40          * 否则,将其直接加入就绪事件的队列,pfnPrepare实际上就是OnPollPreparePfn函数
 41          */
 42         for(int i=0;i<ret;i++)
 43         {
 44             // 获取在co_poll_inner放入epoll_event中的stTimeoutItem_t结构体
 45             stTimeoutItem_t *item = (stTimeoutItem_t*)result->events[i].data.ptr;
 46             // 如果用户设置预处理回调的话就执行
 47             if( item->pfnPrepare )
 48             {
 49                 // 若是hook后的poll的话,会把此事件加入到active队列中,并更新一些状态
 50                 item->pfnPrepare( item,result->events[i],active );
 51             }
 52             else
 53             {
 54                 AddTail( active,item );
 55             }
 56         }
 57 
 58         //从时间轮上取出已超时的事件,放到 timeout 队列。
 59         unsigned long long now = GetTickMS();
 60         TakeAllTimeout( ctx->pTimeout,now,timeout );
 61 
 62         //遍历 timeout 队列,设置事件已超时标志(bTimeout 设为 true)。
 63         stTimeoutItem_t *lp = timeout->head;
 64         // 遍历超时链表,设置超时标志,并加入active链表
 65         while( lp )
 66         {
 67             //printf("raise timeout %p\n",lp);
 68             lp->bTimeout = true;
 69             lp = lp->pNext;
 70         }
 71 
 72         //将 timeout 队列中事件合并到 active 队列。
 73         Join<stTimeoutItem_t,stTimeoutItemLink_t>( active,timeout );
 74 
 75         /**
 76          * 遍历 active 队列,调用⼯作协程设置的 pfnProcess() 回调函数 resume挂起的⼯作协程,
 77          * 处理对应的 I/O 或超时事件。
 78          */
 79         lp = active->head;
 80         // 开始遍历active链表
 81         while( lp )
 82         {
 83             // 在链表不为空的时候删除active的第一个元素 如果删除成功,那个元素就是lp
 84             PopHead<stTimeoutItem_t,stTimeoutItemLink_t>( active );
 85             //如果被设置为超时并且当前事件还没有到达实际设置的超时事件
 86             if (lp->bTimeout && now < lp->ullExpireTime)
 87             {
 88                 // 一种排错机制,在超时和所等待的时间内已经完成只有一个条件满足才是正确的
 89                 int ret = AddTimeout(ctx->pTimeout, lp, now);
 90                 if (!ret)//插入成功
 91                 {
 92                     //重新开始定时
 93                     lp->bTimeout = false;
 94                     lp = active->head;
 95                     continue;
 96                 }
 97             }
 98             if( lp->pfnProcess )
 99             {
100                 lp->pfnProcess( lp );
101             }
102 
103             lp = active->head;
104         }
105         // 每次事件循环结束以后执行该函数, 用于终止协程,相当于终止回调函数
106         if( pfn )
107         {
108             if( -1 == pfn( arg ) )
109             {
110                 break;
111             }
112         }
113 
114     }
115 }

  首先我们可以看到active和timeout链表都在stCoEpoll_t中存储,而这个结构是线程私有的。那么为什么不把这个值设置成局部变量呢?答案不在co_eventloop中,而藏在其他函数,比如libco实现的条件变量中,条件变量会在signal后把值放入到active链表或者timeout链表,而这些只能放在stCoEpoll_t中。

stCoEpoll_t函数源码:

 1 struct stCoEpoll_t
 2 {
 3     int iEpollFd;    // epoll 实例的⽂件描述符
 4 
 5     /**
 6      * 值为 10240 的整型常量。作为 epoll_wait() 系统调用的第三个参数,
 7      * 即⼀次 epoll_wait 最多返回的就绪事件个数。
 8      */
 9     static const int _EPOLL_SIZE = 1024 * 10;
10 
11     /**
12      * 该结构实际上是⼀个时间轮(Timingwheel)定时器,只是命名比较怪,让⼈摸不着头脑。
13      * 单级时间轮来处理其内部的超时事件。
14      */
15     struct stTimeout_t *pTimeout;
16 
17     /**
18      * 该指针实际上是⼀个链表头。链表用于临时存放超时事件的 item。
19      */
20     struct stTimeoutItemLink_t *pstTimeoutList;
21 
22     /**
23      * 也是指向⼀个链表。该链表用于存放 epoll_wait 得到的就绪事件和定时器超时事件。
24      */
25     struct stTimeoutItemLink_t *pstActiveList;
26 
27     /**
28      * 对 epoll_wait()第⼆个参数的封装,即⼀次 epoll_wait 得到的结果集。
29      */
30     co_epoll_res *result;
31 
32 };

  还有这里的timeout链表其实最终会合并到active中,先分开纯粹是为了处理方便一点。然后就是把事件从epoll结果集中拿出来,如果需要的话就去执行预处理回调。我们来看看预处理回调,我们曾在poll中提到过

 1 void OnPollPreparePfn( stTimeoutItem_t * ap,struct epoll_event &e,stTimeoutItemLink_t *active )
 2 {
 3     stPollItem_t *lp = (stPollItem_t *)ap;
 4     // 把epoll此次触发的事件转换成poll中的事件
 5     lp->pSelf->revents = EpollEvent2Poll( e.events );
 6 
 7 
 8     stPoll_t *pPoll = lp->pPoll;
 9     // 已经触发的事件数加一
10     pPoll->iRaiseCnt++;
11 
12     // 若此事件还未被触发过
13     if( !pPoll->iAllEventDetach )
14     {
15         // 设置已经被触发的标志
16         pPoll->iAllEventDetach = 1;
17 
18         // 将该事件从时间轮中移除
19         // 因为事件已经触发了,肯定不能再超时了
20         RemoveFromLink<stTimeoutItem_t,stTimeoutItemLink_t>( pPoll );
21 
22         // 将该事件添加到active列表中
23         AddTail( active,pPoll );
24 
25     }
26 }

所做的事情:

  • 将触发的事件由epoll类型转换为poll类型
  • 触发的事件数加1
  • 如果当前事件是第一次被触发,就设置触发标志位,并将其从超时事件队列中移除,最后将其加入就绪事件队列active,如果不是第一次触发,就不再进行任何处理

其实所做的事情就是把epoll事件对应的stPoll_t结构中的值执行一些修改,并把此项插入到active链表中。

然后就是从时间轮中取出根据目前时间来说已经超时的事件,并插入到timeout链表中:

 1 inline void TakeAllTimeout( stTimeout_t *apTimeout,unsigned long long allNow,stTimeoutItemLink_t *apResult )
 2 {
 3     // 第一次调用是设置初始时间
 4     if( apTimeout->ullStart == 0 )
 5     {
 6         apTimeout->ullStart = allNow;
 7         apTimeout->llStartIdx = 0;
 8     }
 9 
10     // 当前时间小于初始时间显然是有问题的
11     if( allNow < apTimeout->ullStart )
12     {
13         return ;
14     }
15     // 求一个取出事件的有效区间
16     int cnt = allNow - apTimeout->ullStart + 1;
17     if( cnt > apTimeout->iItemSize )
18     {
19         cnt = apTimeout->iItemSize;
20     }
21     if( cnt < 0 )
22     {
23         return;
24     }
25     for( int i = 0;i<cnt;i++)
26     {    // 把上面求的有效区间过一遍,某一项存在数据的话插入到超时链表中
27         int idx = ( apTimeout->llStartIdx + i) % apTimeout->iItemSize;
28         // 链表操作,没什么可说的
29         Join<stTimeoutItem_t,stTimeoutItemLink_t>( apResult,apTimeout->pItems + idx  );
30     }
31     // 更新时间轮属性
32     apTimeout->ullStart = allNow;
33     apTimeout->llStartIdx += cnt - 1;
34 }

  然后就是把超时链表处理以后加入到active链表啦。

  然后就是遍历active链表,一一执行每一个事件的回调啦,当然没执行一次回调就意味着一次协程的切换,因为我们在poll中注册的回调执行co_resume。

  循环的最后调用了pfn,这是一个我们在调用co_eventloop时传入的函数指针,它的作用是什么呢?跳出Eventloop循环的时候使用,因为不是所有的协程使用都想例子一样把 co_eventloop放在函数最后,协程更多的是嵌到代码中,我们需要在有些时候终止eventloop,传入一个终止回调就是一个不错的方法。

co_eventloop完成的工作总结:

1、监听所有事件

2、如果必要的话,对就绪事件做一些预处理,再将其加入就绪事件队列,否则直接加入就绪事件队列

3、获取超时事件

4、将超时事件合并到就绪事件队列

5、遍历整个就绪事件队列,调用相应的回调函数,唤醒相应的协程对改事件进行处理

6、如果需要跳出循环的话就跳出循环,否则继续执行epoll_wait开始监听事件

转载:

https://blog.csdn.net/weixin_43705457/article/details/106891077?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522162694514116780271590368%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=162694514116780271590368&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_v2~rank_v29-1-106891077.pc_search_result_cache&utm_term=libco+co_eventloop&spm=1018.2226.3001.4187

posted @ 2021-07-22 22:38  Mr-xxx  阅读(273)  评论(0编辑  收藏  举报