文章目录
协程的创建:co_create()
前面系列的文章已经提到过来,libco协程使用的第一步就是使用co_create()函数创建一个协程,其定义在co_routine.cpp文件中:
int co_create( stCoRoutine_t **ppco,const stCoRoutineAttr_t *attr,pfn_co_routine_t pfn,void *arg );
我们首先来解释以下参数,然后再去看函数的定义:
- ppco :这是一个输出参数,co_create会创建一个协程控制块CPB,可以类比于进程控制块PCB
- attr :输入参数,还记得stCoRoutineAttr_t是个什么结构么?其用于指定要创建协程的属性。
- pfn:其本质是
void *(*pfn_co_routine_t)( void * )
,也就是协程执行的任务函数 - arg:传递给任务函数的参数
知道了这些参数,我们再来看一下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(), attr, pfn,arg );
//ppco作为输出参数,得到这个协程对象的地址
*ppco = co;
return 0;
}
初始化线程环境:co_init_curr_thread_env()
可以看到,co_create会先判断当前的环境是否初始化,判断标准是一个全局变量gCoEnvPerThread
,其是一个stCoRoutineEnv_t
类型的指针:
static __thread stCoRoutineEnv_t* gCoEnvPerThread = NULL;
stCoRoutineEnv_t *co_get_curr_thread_env()
{
return gCoEnvPerThread;
}
如果当前线程环境未初始化,那么就调用co_init_curr_thread_env()
函数去初始化:
void co_init_curr_thread_env()
{
//申请内存
gCoEnvPerThread = (stCoRoutineEnv_t*)calloc( 1, sizeof(stCoRoutineEnv_t) );
stCoRoutineEnv_t *env = gCoEnvPerThread;
//初始化当前栈指针未栈底
env->iCallStackSize = 0;
//创建主协程
struct stCoRoutine_t *self = co_create_env( env, NULL, NULL,NULL );
self->cIsMain = 1;
//初始化主协程共享栈相关指针未空
env->pending_co = NULL;
env->occupy_co = NULL;
//初始化主协程CPU上下文
coctx_init( &self->ctx );
//将主协程放入协程跟踪栈的栈底
env->pCallStack[ env->iCallStackSize++ ] = self;
//创建一个事件循环
stCoEpoll_t *ev = AllocEpoll();
//这里先不解释,暂且跳过,可以理解为开启调度器的事件循环
SetEpoll( env,ev );
}
创建协程对象:co_create_env()
大致的一个流程我大致写了以下注释,其中调用了co_create_env这个函数去创建一个协程对象,那我们来看一下这个函数的声明:
struct stCoRoutine_t *co_create_env( stCoRoutineEnv_t * env, const stCoRoutineAttr_t* attr,
pfn_co_routine_t pfn,void *arg );
可以看到,其参数都跟co_create是一样的,我就不多介绍了,直接来看它做了什么:
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 )
{
//如果有输入参数,那么就把输入参数的结构内容拷贝至at
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 )
{
at.stack_size &= ~0xFFF;
//加上4MB
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;
//初始化上下文的栈信息
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;
lp->cIsShareStack = at.share_stack != NULL;
lp->save_size = 0;
lp->save_buffer = NULL;
return lp;
}
相关代码已经添加注释了,其实其干的事情就是申请一个协程对象的空间并初始化协程对象的各种属性。
那么我们现在回到co_init_curr_thread_env
函数。
初始化CPU上下文:coctx_init
将主协程压栈之前,调用了coctx_init()
去初始化了主协程的CPU上下文,其定义在了coctx.cpp
文件中:
int coctx_init(coctx_t* ctx) {
memset(ctx, 0, sizeof(*ctx));
return 0;
}
其实它就是将内存初始化为0了。
之后代码把主协程放入了协程跟踪栈之中,形成了以下的结构:
然后创建了一个epoll事件循环,开启此事件循环。
小结
创建协程只要包括以下内容:
- 初始化当前的线程环境,其又包括:
1.1 申请环境对象stCoRoutineEnv_t的内存
1.2 创建主协程对象并压栈
1.3 开启事件循环 - 创建协程对象
协程的启动:co_resume()
在调用co_create创建协程返回后,便可以调用co_resume函数将它启动了,该函数定义在co_routine.cpp
函数中:
void co_resume( stCoRoutine_t *co );
- co:传入的协程对象,既co_create的输出参数
co_resume的参数比较简单,就是一个指向协程的指针,它负责启动它。
博主说:
这里有个值得思考的地方,为什么用resume这个单词而不是start这个单词?
我们先来看一下resume这个单词的意思:
在拥抱新技术——协程一文中,我提到了libco是非对称协程,协程在让出CPU后要恢复执行的时候,还是要co_resume去恢复启动此协程。从语义上来说,start只有一次,而resume可以有多次。
好了,我们再来看一下co_resume函数做了啥:
void co_resume( stCoRoutine_t *co )
{
//获得当前协程的环境
stCoRoutineEnv_t *env = co->env;
//获取当前运行的协程
stCoRoutine_t *lpCurrRoutine = env->pCallStack[ env->iCallStackSize - 1 ];
if( !co->cStart )
{
//加载CPU上下文信息
coctx_make( &co->ctx,(coctx_pfn_t)CoRoutineFunc,co,0 );
co->cStart = 1;
}
//将当前协程压入协程跟踪栈
env->pCallStack[ env->iCallStackSize++ ] = co;
//进行协程的切换,挂起当前协程,开始执行co协程
co_swap( lpCurrRoutine, co );
}
这里重点关注以下两个函数coctx_make
和co_swap
。
加载上下文信息:coctx_make()
首先,我们注意coctx_make
的第二个参数,其是一个函数指针,我们先来剖析以下这个函数的信息:
static int CoRoutineFunc( stCoRoutine_t *co,void * )
{
//如果传入了函数就执行
if( co->pfn )
{
co->pfn( co->arg );
}
//设置执行标记为执行完
co->cEnd = 1;
//获取线程运行环境
stCoRoutineEnv_t *env = co->env;
//出让CPU资源
co_yield_env( env );
return 0;
}
可以看到,这个函数实际就是执行我们传入回调函数的地方,执行完函数之后,这个协程就会出让CPU资源。
之后再来看一下函数coctx_make
的定义,定义在coctx.cpp
文件中:
int coctx_make(coctx_t* ctx, coctx_pfn_t pfn, const void* s, const void* s1) {
//获得栈顶地址
char* sp = ctx->ss_sp + ctx->ss_size - sizeof(void*);
sp = (char*)((unsigned long)sp & -16LL);
//初始化寄存器
memset(ctx->regs, 0, sizeof(ctx->regs));
//定义保存返回地址,其在栈顶
void** ret_addr = (void**)(sp);
//保存传入的函数pfn为返回地址
*ret_addr = (void*)pfn;
//保存regs[13]为当前协程的栈顶,以便下次恢复
ctx->regs[kRSP] = sp;
//regs[9]为执行函数的地址
ctx->regs[kRETAddr] = (char*)pfn;
//regs[7]为当前协程对象地址
ctx->regs[kRDI] = (char*)s;
ctx->regs[kRSI] = (char*)s1;
return 0;
}
大致注释已经给出,其实这里就是加载当前协程上下文信息。
协程的切换:co_swap()
co_swap函数的作用就是切换协程之间的上下文和栈信息。
我们来看一下这个函数做了什么,先来看一下它的声明吧,在co_routine.cpp
函数中:
void co_swap(stCoRoutine_t* curr, stCoRoutine_t* pending_co);
- curr:当前协程
- pending_to:要切换入的执行协程
我们再来看一下它究竟是如何切换栈信息和上下文信息的:
void co_swap(stCoRoutine_t* curr, stCoRoutine_t* pending_co);
{
//得到当前线程环境
stCoRoutineEnv_t* env = co_get_curr_thread_env();
//得到当前协程的栈指针sp
char c;
curr->stack_sp= &c;
if (!pending_co->cIsShareStack)
{
//独占栈模式
env->pending_co = NULL;
env->occupy_co = NULL;
}
else
{
//共享栈模式,不考虑
env->pending_co = pending_co;
//get last occupy co on the same stack mem
stCoRoutine_t* occupy_co = pending_co->stack_mem->occupy_co;
//set pending co to occupy thest stack mem;
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);
}
}
//切换上下文信息
coctx_swap(&(curr->ctx),&(pending_co->ctx) );
//切换新协程的栈信息以及运行环境的信息
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)
{
//切换栈信息
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);
}
}
}
libco精华 —— 上下文信息切换:coctx_swap()
切换栈信息部分很好理解,重点是切换上下文信息那里,也就是coctx_swap
函数。
博主认为,coctx_swap
才是整个libco的精华所在,在其内部采用了精简的汇编代码完成了协程在用户态的切换,而不用陷入到内核态去完成协程的调度。
我们来看一下这个汇编代码:
.globl coctx_swap
#if !defined( __APPLE__ )
.type coctx_swap, @function
#endif
coctx_swap: //函数coctx_swap
#if defined(__i386__)
movl 4(%esp), %eax //sp 将第一个参数也就是当前协程信息的地址保存下来
movl %esp, 28(%eax)
movl %ebp, 24(%eax)
movl %esi, 20(%eax)
movl %edi, 16(%eax)
movl %edx, 12(%eax)
movl %ecx, 8(%eax)
movl %ebx, 4(%eax)
//上面会保存当前协程的寄存器信息
//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//下面加载将要执行的协程的寄存器信息
movl 8(%esp), %eax
movl 4(%eax), %ebx
movl 8(%eax), %ecx
movl 12(%eax), %edx
movl 16(%eax), %edi
movl 20(%eax), %esi
movl 24(%eax), %ebp
movl 28(%eax), %esp
ret
上面的注释已经给出,可以结合下图进行看:
小结
协程的切换主要做了以下事情:
- 获取当前下运行环境以及当前正在进行的协程current
- 加载将要执行的pending_to协程的执行信息,包括执行的函数以及参数等
- pending_to协程压栈,进入协程跟踪栈
- 交换pending_to 和current的栈以及上下文信息
加入说current协程是协程A,pending_to协程是协程B,那么此时协程跟踪栈就会发生入下图的变化:
协程的挂起:co_yield_env()
刚刚在CoRoutineFunc
函数中的最后会调用co_yield_env
函数,出让CPU资源,那么它是怎么实现的呢?
在非对称协程理论中,yield和resume是个相对的操作。A协程resume启动了B协程,那么之后当B协程执行yield操作时才会返回到A协程。换句话说:只有被调协程yield让出CPU,调用者协程的co_swap函数才能返回到原点,即返回到原来co_resume的位置。
注:
yield释义:
我们现在来看一下,co_yield_env函数做了什么吧:
void co_yield_env( stCoRoutineEnv_t *env )
{
//获取协程跟踪栈中的的倒数第一个和第二个元素
stCoRoutine_t *last = env->pCallStack[ env->iCallStackSize - 2 ];
stCoRoutine_t *curr = env->pCallStack[ env->iCallStackSize - 1 ];
//协程栈元素减一
env->iCallStackSize--;
//交换当前协程curr以及last的信息,将CPU权力移交到last协程
co_swap( curr, last);
}
那么,其实很简单了,经过出让资源后,当前协程跟踪栈内的情况就变成如下情况了:
协程的退出
这里讲的退出,有别于协程的挂起,是指协程任务结束后发生的过程。换而言之,就是协程任务函数内部执行了return语句,结束了它的生命周期。
同协程挂起一样,协程退出时也将CPU控制权移交给它的调用者,这也是通过co_yield_env
函数来完成的。
最后,当任务结束后记得要调用co_free()
或者co_release()
销毁这个临时性的协程,否则将引起内存泄漏。
void co_free( stCoRoutine_t *co )
{
if (!co->cIsShareStack)
{
//独占栈
//释放协程栈信息
free(co->stack_mem->stack_buffer);
free(co->stack_mem);
}
else
{
if(co->save_buffer)
free(co->save_buffer);
if(co->stack_mem->occupy_co == co)
co->stack_mem->occupy_co = NULL;
}
//释放协程对象
free( co );
}
co_release也是调用co_free,就不放代码了
参考文献
[1] C++开源协程库libco详解