文章目录
前言
本文对腾讯微信的协程库libco进行了简单解析,如有不当,请指正。
一、结构体定义
1.协程栈定义
libco使用的协程栈定义如下,支持独立栈和共享栈两种方式。
struct stStackMem_t
{
stCoRoutine_t* occupy_co; // 当前正在使用该共享栈的协程
int stack_size; // 栈的大小
char* stack_bp; // stack_buffer + stack_size 栈底
char* stack_buffer; // 栈的内容,也就是栈顶
};
/*
* 共享栈,这里的共享栈是个数组,每个元素分别是个共享栈
*/
struct stShareStack_t
{
unsigned int alloc_idx; // 应该是目前正在使用的那个共享栈的index
int stack_size; // 共享栈的大小,这里的大小指的是一个stStackMem_t*的大小
int count; // 共享栈的个数,共享栈可以为多个,所以以下为共享栈的数组
stStackMem_t** stack_array; //栈的内容,这里是个数组,元素是stStackMem_t*
};
2.协程定义
协程结构体定义如下:
//协程
struct stCoRoutine_t
{
stCoRoutineEnv_t *env; // 协程所在的运行环境,可以理解为,该协程所属的协程管理器
pfn_co_routine_t pfn; // 协程所对应的函数
void *arg; // 函数参数
coctx_t ctx; // 协程上下文,包括寄存器和栈
// 以下用char表示了bool语义,节省空间
char cStart; // 是否已经开始运行了
char cEnd; // 是否已经结束
char cIsMain; // 是否是主协程
char cEnableSysHook; // 是否要打开钩子标识,默认是关闭的
char cIsShareStack; // 是否要采用共享栈
void *pvEnv;
//char sRunStack[ 1024 * 128 ];
stStackMem_t* stack_mem; // 栈内存
//save satck buffer while conflict on same stack_buffer;
char* stack_sp;
unsigned int save_size; // save_buffer的长度
char* save_buffer; // 当协程挂起时,栈的内容会栈暂存到save_buffer中
stCoSpec_t aSpec[1024];
};
其中采用独立栈时使用stack_mem成员,采用共享栈时使用stack_sp、save_size和save_buffer成员。
3.协程上下文定义
协程上下文定义如下:
/*
* 协程上下文
*/
struct coctx_t
{
#if defined(__i386__)
void *regs[ 8 ]; // i386架构下需要8个寄存器
#else
void *regs[ 14 ]; //
#endif
size_t ss_size; // 栈空间的大小
char *ss_sp; // 栈空间
};
其中64位机器使用时regs存放了14个CPU寄存器地址,对应关系如下:
//-------------
// 64 bit
//low | regs[0]: r15 |
// | regs[1]: r14 |
// | regs[2]: r13 |
// | regs[3]: r12 |
// | regs[4]: r9 |
// | regs[5]: r8 |
// | regs[6]: rbp |
// | regs[7]: rdi |
// | regs[8]: rsi |
// | regs[9]: ret | //ret func addr
// | regs[10]: rdx |
// | regs[11]: rcx |
// | regs[12]: rbx |
//hig | regs[13]: rsp |
4.协程环境定义
协程环境结构体定义如下:
/*
* 线程所管理的协程的运行环境
* 一个线程只有一个这个属性
*/
struct stCoRoutineEnv_t
{
// 这里实际上维护的是个调用栈
// 最后一位是当前运行的协程,前一位是当前协程的父协程(即,resume该协程的协程)
// 可以看出来,libco只能支持128层协程的嵌套调用。这个绝对够了
stCoRoutine_t *pCallStack[ 128 ];
int iCallStackSize; // 当前调用栈长度
stCoEpoll_t *pEpoll; //主要是epoll,作为协程的调度器
//for copy stack log lastco and nextco
stCoRoutine_t* pending_co;
stCoRoutine_t* occupy_co;
};
stCoRoutineEnv_t其实就相当于协程的调度器,其中的pCallStack表示当前运行的协程。libco是一个非对称协程库,一个协程挂起以后,cpu控制权只能转到调用它的协程。所以pCallStack是按照调用关系存放的一组协程,其中pCallStack[0]是主协程,pCallStack[0]调用pCallStack[1],pCallStack[1]调用pCallStack[2].…以此类推。
二、API定义
1.协程调度器初始化及获取函数
协程调度器stCoRoutineEnv_t对象初始化及获取函数如下所示:
// 初始化当前线程的协程管理器
void co_init_curr_thread_env()
{
//当前的线程的ID
pid_t pid = GetPid();
g_arrCoEnvPerThread[ pid ] = (stCoRoutineEnv_t*)calloc( 1,sizeof(stCoRoutineEnv_t) );
stCoRoutineEnv_t *env = g_arrCoEnvPerThread[ pid ];
// 当前协程数为0
env->iCallStackSize = 0;
// 创建一个协程
struct stCoRoutine_t *self = co_create_env( env, NULL, NULL,NULL );
self->cIsMain = 1; // 标识是一个主协程
env->pending_co = NULL; // 初始化为 null
env->occupy_co = NULL; // 初始化为 null
// 初始化协程上下文
coctx_init( &self->ctx );
// 初始化协程管理器的时候,会把主协程放在第一个
env->pCallStack[ env->iCallStackSize++ ] = self;
stCoEpoll_t *ev = AllocEpoll();
SetEpoll( env,ev );
}
// 获取当前线程的协程管理器
stCoRoutineEnv_t *co_get_curr_thread_env()
{
return g_arrCoEnvPerThread[ GetPid() ];
}
可以看到,初始化stCoRoutineEnv_t 结构体时也会初始化主协程,并将主协程放在pCallStack的第一个位置。
2.协程创建函数
协程创建函数定义如下:
/**
* 根据协程管理器env, 新建一个协程
*
* @param env - (input) 协程所在线程的环境
* @param attr - (input) 协程属性,目前主要是共享栈
* @param pfn - (input) 协程所运行的函数
* @param arg - (input) 协程运行函数的参数
*/
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 )
{
memcpy( &at,attr,sizeof(at) );
}
if( at.stack_size <= 0 )
{
at.stack_size = 128 * 1024; // 默认的为128k
}
else if( at.stack_size > 1024 * 1024 * 8 )
{
at.stack_size = 1024 * 1024 * 8;
}
/* 地址对齐 */
if( at.stack_size & 0xFFF )
{
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;
lp->pfn = pfn;
lp->arg = arg;
stStackMem_t* stack_mem = NULL;
if( at.share_stack )
{
// 如果采用了共享栈模式,则获取到其中一个共享栈的内存
stack_mem = co_get_stackmem( at.share_stack);
at.stack_size = at.share_stack->stack_size;
}
else
{
// 如果没有采用共享栈,则分配内存
stack_mem = co_alloc_stackmem(at.stack_size);
}
lp->stack_mem = stack_mem;
// 设置该协程的context
lp->ctx.ss_sp = stack_mem->stack_buffer; // 栈地址
lp->ctx.ss_size = at.stack_size; // 栈大小
lp->cStart = 0;
lp->cEnd = 0;
lp->cIsMain = 0;
lp->cEnableSysHook = 0; // 默认不开启hook
lp->cIsShareStack = at.share_stack != NULL;
// 仅在共享栈的时候有意义
lp->save_size = 0;
lp->save_buffer = NULL;
return lp;
}
/**
* 创建一个协程对象
*
* @param ppco - (output) 协程的地址,未初始化,需要在此函数中将其申请内存空间以及初始化工作
* @param attr - (input) 协程属性,目前主要是共享栈
* @param pfn - (input) 协程所运行的函数
* @param arg - (input) 协程运行函数的参数
*/
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 );
*ppco = co;
return 0;
}
3.协程启动/恢复函数
协程启动/恢复函数定义如下:
/*
* 语义:继续运行协程
* 实际上:
* @param co - (input) 要切换的协程
*/
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->cStart = 1;
}
// 将指定协程放入线程的协程队列末尾
env->pCallStack[ env->iCallStackSize++ ] = co;
// 将当前运行的上下文保存到lpCurrRoutine中,同时将协程co的上下文替换进去
// 执行完这一句,当前的运行环境就被替换为 co 了
co_swap( lpCurrRoutine, co );
}
4.协程挂起函数
协程挂起函数定义如下:
/*
*
* 主动将当前运行的协程挂起,并恢复到上一层的协程
*
* @param env 协程管理器
*/
void co_yield_env( stCoRoutineEnv_t *env )
{
// 这里直接取了iCallStackSize - 2,那么万一icallstacksize < 2呢?
// 所以这里实际上有个约束,就是co_yield之前必须先co_resume, 这样就不会造成这个问题了
// last就是 找到上次调用co_resume(curr)的协程
stCoRoutine_t *last = env->pCallStack[ env->iCallStackSize - 2 ];
// 当前栈
stCoRoutine_t *curr = env->pCallStack[ env->iCallStackSize - 1 ];
env->iCallStackSize--;
// 把上下文当前的存储到curr中,并切换成last的上下文
co_swap( curr, last);
}
void co_yield( stCoRoutine_t *co )
{
co_yield_env( co->env );
}
5.协程切换函数
在co_yield_env和co_resume函数中都定义了co_swap函数,完成协程切换,定义如下:
/**
* 将原本占用共享栈的协程的内存保存起来。
* @param occupy_co 原本占用共享栈的协程
*/
void save_stack_buffer(stCoRoutine_t* occupy_co)
{
///copy out
stStackMem_t* stack_mem = occupy_co->stack_mem;
// 计算出栈的大小
int len = stack_mem->stack_bp - occupy_co->stack_sp;
if (occupy_co->save_buffer)
{
free(occupy_co->save_buffer), occupy_co->save_buffer = NULL;
}
occupy_co->save_buffer = (char*)malloc(len); //malloc buf;
occupy_co->save_size = len;
// 将当前运行栈的内容,拷贝到save_buffer中
memcpy(occupy_co->save_buffer, occupy_co->stack_sp, len);
}
/*
* 1. 将当前的运行上下文保存到curr中
* 2. 将当前的运行上下文替换为pending_co中的上下文
* @param curr
* @param pending_co
*/
void co_swap(stCoRoutine_t* curr, stCoRoutine_t* pending_co)
{
stCoRoutineEnv_t* env = co_get_curr_thread_env();
//get curr stack sp
//这里非常重要!!!: 这个c变量的实现,作用是为了找到目前的栈底,因为c变量是最后一个放入栈中的内容。
char c;
curr->stack_sp= &c;
if (!pending_co->cIsShareStack)
{
// 如果没有采用共享栈,清空pending_co和occupy_co
env->pending_co = NULL;
env->occupy_co = NULL;
}
else
{
// 如果采用了共享栈
env->pending_co = pending_co;
//get last occupy co on the same stack mem
// occupy_co指的是,和pending_co共同使用一个共享栈的协程
// 把它取出来是为了先把occupy_co的内存保存起来
stCoRoutine_t* occupy_co = pending_co->stack_mem->occupy_co;
//set pending co to occupy thest stack mem;
// 将该共享栈的占用者改为pending_co
pending_co->stack_mem->occupy_co = pending_co;
env->occupy_co = occupy_co;
if (occupy_co && occupy_co != pending_co)
{
// 如果上一个使用协程不为空, 则需要把它的栈内容保存起来。
save_stack_buffer(occupy_co);
}
}
// swap context
coctx_swap(&(curr->ctx),&(pending_co->ctx) );
// 这个地方很绕,上一步coctx_swap会进入到pending_co的协程环境中运行
// 到这一步,已经yield回此协程了,才会执行下面的语句
// 而yield回此协程之前,env->pending_co会被上一层协程设置为此协程
// 因此可以顺利执行: 将之前保存起来的栈内容,恢复到运行栈上
//stack buffer may be overwrite, so get again;
stCoRoutineEnv_t* curr_env = co_get_curr_thread_env();
stCoRoutine_t* update_occupy_co = curr_env->occupy_co;
stCoRoutine_t* update_pending_co = curr_env->pending_co;
// 将栈的内容恢复,如果不是共享栈的话,每个协程都有自己独立的栈空间,则不用恢复。
if (update_occupy_co && update_pending_co && update_occupy_co != update_pending_co)
{
// resume stack buffer
if (update_pending_co->save_buffer && update_pending_co->save_size > 0)
{
// 将之前保存起来的栈内容,恢复到运行栈上
memcpy(update_pending_co->stack_sp, update_pending_co->save_buffer, update_pending_co->save_size);
}
}
}
在co_swap函数中,如果采用的是共享栈,则先采用save_stack_buffer保存共享栈数据到自己的栈空间。关于共享栈和非共享栈的介绍可以看:小白学协程笔记3-实现自己的协程库-2021-2-22。然后采用coctx_swap函数完成上下文切换,coctx_swap函数是通过汇编语言实现的,如何进行切换已经在这篇文章中介绍:小白学协程笔记2-c语言实现协程-2021-2-10。coctx_swap会切换到指定协程运行。而当指定协程挂起时,会返回到coctx_swap的下一行代码运行。返回之后,若采用的是共享栈,则需要将数据从自己的栈,拷贝回共享栈中。
总结
本文对libco的代码进行了简单剖析,libco源码地址。