文章目录
- 前言
- 一、结构体定义
-
- 1.协程栈定义
- 2.协程定义
- 3.协程上下文定义
- 4.协程环境定义
- 二、API定义
-
- 1.协程调度器初始化及获取函数
- 2.协程创建函数
- 3.协程启动/恢复函数
- 4.协程挂起函数
- 5.协程切换函数
- 总结
前言
本文对腾讯微信的协程库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; // 应该是目前正在使用的那个共享栈的indexint 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个寄存器
#elsevoid *regs[ 14 ]; //
#endifsize_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 nextcostCoRoutine_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()
{
//当前的线程的IDpid_t pid = GetPid(); g_arrCoEnvPerThread[ pid ] = (stCoRoutineEnv_t*)calloc( 1,sizeof(stCoRoutineEnv_t) );stCoRoutineEnv_t *env = g_arrCoEnvPerThread[ pid ];// 当前协程数为0env->iCallStackSize = 0;// 创建一个协程struct stCoRoutine_t *self = co_create_env( env, NULL, NULL,NULL );self->cIsMain = 1; // 标识是一个主协程env->pending_co = NULL; // 初始化为 nullenv->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;// 设置该协程的contextlp->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; // 默认不开启hooklp->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 outstStackMem_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_coenv->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_copending_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 contextcoctx_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 bufferif (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源码地址。