这是我整合自己对协程的一些理解,文章中的图我有些是在网上直接拷贝的,在这里感谢原作者。
从控制台传进来的参数allfont,font,每次循环会创建font个task,task的概念可以说就是协程。协程通过stCoRoutine_t结构来描述,就像我们进程的task_struct一样,保存着运行时,关于协程运行环境的所有信息,所以每个task有一个叫做co的成员。
co指向stCoRoutine_t结构体。
让我们看看co_create函数里面都干了什么。
int co_create( stCoRoutine_t **ppco,const stCoRoutineAttr_t *attr,pfn_co_routine_t pfn,void *arg )
{
if( !co_get_curr_thread_env() ) {
//注意这里只有在协程第一次创建时调用,创建主协程并压入协程栈
co_init_curr_thread_env();
}
stCoRoutine_t *co = co_create_env( co_get_curr_thread_env(),pfn,arg );//初始化
*ppco = co;
return 0;
}
////////////
co_init_curr_thread_env(){
.............
////只显示核心部分
// env为struct stCoRoutineEnv_t类型
struct stCoRoutine_t *self = co_create_env( env,NULL,NULL );
self->cIsMain = 1;
// 每个协程里有个coctx_t类型的成员,这个属于协程较底层的支撑信息,后面细讲
coctx_init( &self->ctx );
// 调用co_init_curr_thread_env时,pCallStack里面是空的,
// 这里的self是指当前的进程/线程,也就是说当前的进程/线程也算是协程的一种
env->pCallStack[ env->iCallStackSize++ ] = self;
// 申请epoll结构
stCoEpoll_t *ev = AllocEpoll();
// 与当前env绑定
SetEpoll( env,ev );
.........
}
///////////////////////
//co_creat_env
struct stCoRoutine_t *co_create_env( stCoRoutineEnv_t * env,pfn_co_routine_t pfn,void *arg )
{
stCoRoutine_t *lp = (stCoRoutine_t*)malloc( sizeof(stCoRoutine_t) );
memset( lp,0,(long)((stCoRoutine_t*)0)->sRunStack );
// 每一个协程都指向进程内的env
lp->env = env;
lp->pfn = pfn;
lp->arg = arg;
// 每个协程拥有128KB的私有栈
lp->ctx.ss_sp = lp->sRunStack ;
lp->ctx.ss_size = sizeof(lp->sRunStack) ;
return lp;
}
stCoRoutineEnv_t结构里的pCallStack不是普通意义上我们讲的那个程序运行栈,那个ESP(RSP)寄存器指向的栈,是用来保留程序运行过程中局部变量以及函数调用关系的。但是,这个pCallStack又跟ESP(RSP)指向的栈有相似之处。如果将协程看成一种特殊的函数,那么这个pCallStack就时保存这些函数的调用链的栈。我们已经讲过,非对称协程最大特点就是协程间存在明确的调用关系;甚至在有些文献中,启动协程被称作call,挂起协程叫return。非对称协程机制下的被调协程只能返回到调用者协程,这种调用关系不能乱,因此必须将调用链保存下来。这即是pCallStack的作用,将它命名为“调用栈”实在是恰如其分。
每当启动(resume)一个协程时,就将它的协程控制块stCoRoutine_t 结构指针保存在pCallStack的“栈顶”,然后“栈指针”iCallStackSize加1,最后切换context到待启动协程运行。当协程要让出(yield)CPU时,就将它的stCoRoutine_t从pCallStack
弹出,“栈指针”iCallStackSize减1,然后切换context到当前栈顶的协程(原来被挂起的调用者)恢复执行。这个“压栈”和“弹栈”的过程我们在co_resume()和co_yield()函数中将会再次讲到那么这里有一个问题,libco程序的第一个协程呢,假如第一个协程yield时,CPU控制权让给谁呢?关于这个问题,我们首先要明白这“第一个”协程是什么。实际上,libco的第一个协程,即执行main函数的协程,是一个特殊的协程。这个协程又可以称作主协程,它负责协调其他协程的调度执行(后文我们会看到,还有网络
I/O以及定时事件的驱动),它自己则永远不会yield,不会主动让出
CPU。不让出CPU,不等于说它一直霸占着CPU。我们知道CPU执行权有两种转移途径,一是通过yield让给调用者,其二则是resume启动其他协程运行。
让我们看一下co_resume函数
void co_resume( stCoRoutine_t *co )
{
stCoRoutineEnv_t *env = co->env;
// 取到当前正在运行的co,也就是变量lpCurrRoutine所保存到值
stCoRoutine_t *lpCurrRoutine = env->pCallStack[ env->iCallStackSize - 1 ];
// 首次运行需要设置一些环境
if ( !co->cStart ) {
// 设置运行环境的上下文信息,主要是在协程切换时一些需要的寄存器和栈等。
// 其中第二个参数是协程开始运行的入口函数,其实里面实际调用的函数co->pfn,
// 也就是co_create里设置的。
coctx_make( &co->ctx,(coctx_pfn_t)CoRoutineFunc,co,0 );
co->cStart = 1;
}
// 将要运行的协程入栈
env->pCallStack[ env->iCallStackSize++ ] = co;
// 切换上下文,lpCurrRoutine挂起,co开始运行,运行点就是CoRoutineFunc。
//线程切换函数
coctx_swap( &(lpCurrRoutine->ctx),&(co->ctx) );
}
leal -4(%esp),%esp 把 esp-4 的值 保存到 esp 中
movl 4(%esp),%eap 把 esp+4 的值 传送给 eap
接下来我们说一说accept_routine函数
co_poll() 该接口不仅可以在用户协程程序中直接使用,在hook_sys中也是被hook成poll()的形式而被大量使用。其操作较为复杂,分为以下几个步骤:
(1). 首先创建stPoll_t对象,设置完描述符相关的参数后,最重要的是设置pfnProcess=OnPollProcessEvent、pArg=co_self();当事件就绪后就是通过这个函数和参数将自己切换回来;
(2). 将自己添加到ctx->pTimeout队列中去。关于这个ctx->pTimeout队列,其实是一个简单的链表数组结构,可以维持60x1000ms=60s的超时间隔,然后新的Item要插入进去的话,是就算其相对表头绝对超时时间的偏移长度来定位到指定的数组位置并进行插入的。当然这个接口设计的有点问题,就是poll的系统调用可以设置timeout=-1表示永不超时,但是这里的超时是必须设置且不能超过相对超时表头(理论是)60s,使用起来可能会有些误解。通过这样的数据结构,每次epoll_wait循环后取超时事件就十分方便快速了。
(3). epoll_ctl通过EPOLL_CTL_XXX将实际的事件侦听添加到操作系统中去。这里才发现poll和epoll的事件类型好像不兼容,所以两者常常会用函数转来转去的。
(4). co_yield_env()将自己切换出去;
(5). 后续执行表明因为事件就绪或者超时的因素又被切换回来了,此时调用epoll_ctl将事件从操作系统中删除掉(这也暗示了是ONE SHOT模式的哦),保存返回得到的就绪事件。
(6). 释放资源,本次调用完成。
接下来是read_routine函数
最后一个co_eventloop函数
参考文章:
http://www.liuhaihua.cn/archives/436337.html
http://blog.csdn.net/kobejayandy/article/details/46993021
http://blog.csdn.net/qq910894904/article/details/41911175
http://blog.csdn.net/brainkick/article/details/48676403
http://www.tuicool.com/articles/fQjuAfq
http://www.360doc.com/content/14/0901/19/14106735_406332309.shtml