博客一级目录
二级目录——libco源码分析/学习笔记
参考大牛GitHub注释
由于本源代码蛮长的,所以按照功能划分模块来分析,分为若干部分,详见二级目录↑
代码文件:co_routine.h,co_routine.cpp,co_routine_inner.h
参考博客
每当启动(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,不会主动让出 。
struct stCoRoutine_t为协程环境变量类型(主要是物理环境,比如上下文)。保存着运行时协程运行环境所有信息。
(销毁见第三部分co_free函数)
struct stCoRoutine_t
{
stCoRoutineEnv_t *env; //全局协程管理结构,协程嵌套调用栈管理结构体
pfn_co_routine_t pfn; //协程主函数
void *arg; //协程主函数的参数
coctx_t ctx; //协程上下文信息,包括寄存器、用户栈信息
char cStart; //主函数是否被运行过
char cEnd; //主函数是否运行结束过
char cIsMain; //协程是否是主协程
char cEnableSysHook;//允许hook标记
char cIsShareStack; //本结构体成员stack_mem用户栈是否是从share栈中取下来的
void *pvEnv;
//char sRunStack[ 1024 * 128 ];
stStackMem_t* stack_mem; //用户栈结构体
//save satck buffer while confilct on same stack_buffer;
char* stack_sp; //下面三个成员都是用于对用户栈内容的备份,但是销毁env的时候不知道为什么没有销毁这个备份,推测之前有提前销毁备份的动作
unsigned int save_size;//save_buffer中保存的数据大小
char* save_buffer; //共享栈的时候,此指针指向临时保存栈内有效数据的内存空间
stCoSpec_t aSpec[1024];//协程私有数据
};
代码段 小部件
struct stCoRoutineEnv_t是全局协程管理结构,主要用于控制协程嵌套。
/*
* 该结构的作用是什么呢? - 我们知道, 非对称协程允许嵌套创建子协程, 为了记录这种嵌套创建的协程, 以便子协程退出
* 时正确恢复到挂起点(挂起点位于父协程中), 我们就需要记录这种嵌套调用过程; 另外, 协程中的套接字向内核注册了事件,
* 我们必须保存套接字和协程的对应关系, 以便该线程的eventloop中检测到套接字上事件发生时, 能够恢复该套接字对应的
* 协程来处理事件.
* */
struct stCoRoutineEnv_t
{
stCoRoutine_t *pCallStack[ 128 ]; //调用栈
// 该线程内允许嵌套创建128个协程(即协程1内创建协程2, 协程2内创建协程3... 协程127内创建协程128.
// 该结构虽然是数组, 但将其作为栈来使用, 满足后进先出的特点)
int iCallStackSize;
// 该线程内嵌套创建的协程数量, 即pCallStack数组中元素的数量
stCoEpoll_t *pEpoll;
// epoll抽象体,管理所有事件。该线程内的epoll实例(套接字通过该结构内的epoll句柄向内核注册事件), 也用于该线程的事件循环eventloop中
//for copy stack log lastco and nextco
stCoRoutine_t* pending_co; //在co_swap函数中这临时保存将要占用share stack条目的协程是哪一个。
stCoRoutine_t* occupy_co; //在co_swap函数中这临时保存当前的share stack条目正在被哪一个协程占用
};
co_create_env()函数负责struct stCoRoutine_t的空间申请以及初始化。
参数1就是全局协程管理结构,参数2:内部有stShareStack_t共享栈底层类型变量。参数3和参数4是协程入口函数和参数。
stStackMem_t是普通栈底层类型。包含栈空间指针,栈帧,栈size,使用者。
struct stCoRoutine_t *co_create_env( stCoRoutineEnv_t * env,
const stCoRoutineAttr_t* attr,pfn_co_routine_t pfn,void *arg )
{
stCoRoutineAttr_t at;
if( attr ) //拷贝attr
{
memcpy( &at,attr,sizeof(at) );
}
if( at.stack_size <= 0 )
{
at.stack_size = 128 * 1024;
}
else if( at.stack_size > 1024 * 1024 * 8 )
{
at.stack_size = 1024 * 1024 * 8;
}
if( at.stack_size & 0xFFF ) //如果size大于等于2的12次方(1024*4),size就向上对齐
{
at.stack_size &= ~0xFFF;
at.stack_size += 0x1000;
}
stCoRoutine_t *lp = (stCoRoutine_t*)malloc( sizeof(stCoRoutine_t) );
memset( lp,0,(long)(sizeof(stCoRoutine_t)));
lp->env = env; //stCoRoutineEnv_t
lp->pfn = pfn; //入口函数
lp->arg = arg; //入口函数参数
stStackMem_t* stack_mem = NULL; //stStackMem_t包含栈空间指针,栈帧,栈size,使用者。
if( at.share_stack )//若共享栈存在,则stack_mem从共享栈中取,否则直接向操作系统申请。
{
stack_mem = co_get_stackmem( at.share_stack);
//co_get_stackmem()函数是从共享栈阵列中取出下一个共享栈
at.stack_size = at.share_stack->stack_size;
}
else
{
stack_mem = co_alloc_stackmem(at.stack_size);//申请并初始化一个新栈结构
}
lp->stack_mem = stack_mem;
lp->ctx.ss_sp = stack_mem->stack_buffer;//协程独立的栈空间
lp->ctx.ss_size = at.stack_size;//协程栈空闲大小
lp->cStart = 0;//=0意思是下一次运行是第一次运行。也就是还没运行过
lp->cEnd = 0;
lp->cIsMain = 0;
lp->cEnableSysHook = 0;
lp->cIsShareStack = at.share_stack != NULL; //栈是从共享栈上申请的?
lp->save_size = 0;
lp->save_buffer = NULL;
return lp;
}
int co_create( stCoRoutine_t **ppco,const stCoRoutineAttr_t *attr,pfn_co_routine_t pfn,void *arg );
创建一个协程,申请、初始化协程控制块;并负责协程环境的初始化。
参数1:协程控制块(结构体)指针的指针。
参数2:内存池(share stack),可以写NULL表示不使用内存池。
参数3:函数指针:typedef void *(*pfn_co_routine_t)( void * );
参数4:参数3表示的函数的参数。void*类型。
返回值:始终返回0。
注意:主协程不需要create(由操作系统创建),也不需要resume(由操作系统赋予执行权),更不需要yield(return或exit)。一旦create了其它协程,它就会被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(), attr, pfn,arg ); //申请空间并初始化stCoRoutine_t协程环境
*ppco = co;
return 0;
}
上面代码提到了co_init_curr_thread_env(),那我们按图索骥看一下它的实现和功能。
void co_init_curr_thread_env(),初始化当前线程的主协程。嵌套协程的栈等信息。
void co_init_curr_thread_env()
{
pid_t pid = GetPid();
g_arrCoEnvPerThread[ pid ] = (stCoRoutineEnv_t*)calloc( 1,sizeof(stCoRoutineEnv_t) );
//每个线程的全局协程管理结构都保存在g_arrCoEnvPerThread[]里,线程pid就是编号。
stCoRoutineEnv_t *env = g_arrCoEnvPerThread[ pid ];
env->iCallStackSize = 0; //初始化的之前嵌套协程数量为0
struct stCoRoutine_t *self = co_create_env( env, NULL, NULL,NULL );
//env结构体,不使用共享栈,入口函数/参数为空
self->cIsMain = 1;
env->pending_co = NULL;
env->occupy_co = NULL;
coctx_init( &self->ctx );
//之前pCallStack是空的,self是当前协程 iCallStackSize为嵌套协程栈内元素数量,也是栈顶指针(空递增)
env->pCallStack[ env->iCallStackSize++ ] = self;//将主协程加入调用栈首部
stCoEpoll_t *ev = AllocEpoll();//申请epoll结构
SetEpoll( env,ev );//与当前env绑定
}
void co_resume(stCoRoutine_t co)函数,执行一个协程(交换执行权限,执行co,阻塞当前协程)。如果co是第一次执行会自动提前初始化上下文。
void co_resume( stCoRoutine_t *co )
{
stCoRoutineEnv_t *env = co->env;
stCoRoutine_t *lpCurrRoutine = env->pCallStack[ env->iCallStackSize - 1 ];
//找到正在运行的协程,它就是协程栈中上一个协程。
if( !co->cStart ) //如果协程是第一次运行,则初始化上下文信息。
{
coctx_make( &co->ctx,(coctx_pfn_t)CoRoutineFunc,co,0 );
// 其中第二个参数是协程开始运行的入口函数,其实里面实际调用的函数co->pfn,
// 也就是co_create里设置的。
co->cStart = 1;//标记这次运行
}
env->pCallStack[ env->iCallStackSize++ ] = co; //将co入栈
co_swap( lpCurrRoutine, co );//切换协程,执行协程co。
}
co_swap函数见第三部分,注意区分co_swap和coctx_swap,前者在c++语言层面,后者是汇编层面,