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
本文来自博客园,作者:Mr-xxx,转载请注明原文链接:https://www.cnblogs.com/MrLiuZF/p/15046793.html