顺着上一篇文章示例,这里对文章中所涉及的函数及数据类型逐一进行展开分析,以期学习理解libco
的实现原理,帮助我们加深对协程的理解,并帮助我们更加有效地使用libco
协程库并排查问题。
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 = co;
return 0;
}
其中函数有四个参数,具体作用为:
stCoRoutine_t** ppco
: 出参,返回创建成功后的协程对象const stCoRoutineAttr_t* attr
: 入参,指定创协的协程属性参数pfn_co_routine_t pfn
: 入参,指定协程的执行函数void* arg
: 入参,指定协程的执行函数的参数调用函数co_create
第一步会检查是否已创建协程环境变量,如果没有则新建初始化,该变量是线程共享的,仅在线程的第一个协程创建的时候一起产生,其协程环境变量的定义如下:
struct stCoRoutineEnv_t
{
// 线程协程栈,容量有限
stCoRoutine_t* pCallStack[128];
// 记录协程栈顶为位置
int iCallStackSize;
// 协程epoll支持
stCoEpoll_t* pEpoll;
//for copy stack log lastco and nextco
// 仅在协程共享栈模式的协程切换时候使用
// 当前协程对象指针
stCoRoutine_t* pending_co;
// 协程切换目标协程对象指针
stCoRoutine_t* occupy_co;
};
从stCoRoutineEnv_t
的定义可以看出,线程的协程是用栈的方式管理的,该栈用户保存协程的链式调用栈信息,变量iCallStackSize
指定当前调用栈的栈顶位置。
新建初始化协程环境时,除了创建了协程环境变量,还添加了首个协程。为了便于理解,这里先做个约定,在libco
里面,一个协程仅会在一个线程上运行,一个线程允许有多个协程,我们将主线程代码也称之为一个协程并命名为主协程,因此一个线程至少有一个主协程,然后还有我们的业务工作协程。函数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;
// 初始化协程上下文,实际执行的为对象memset为0
coctx_init(&self->ctx);
// 设置主协程为协程栈的第一个协程
env->pCallStack[env->iCallStackSize++] = self;
// 初始化并设置epoll对象
stCoEpoll_t* ev = AllocEpoll();
SetEpoll(env, ev);
}
在调用函数co_init_curr_thread_env
新建初始化协程环境变量后,会将当前执行的主线程(可简单认为是main
函数)初始化为主协程,并将之入协程调用栈,成为第一个协程栈对象。最后,初始化本线程协程环境的epoll
对象,完成协程环境变量的初始化。可以发现,协程环境变量的初始化一个线程只执行一次,后续不再重复执行,后面创建协程时通过co_get_curr_thread_env
函数获取线程全局共享的协程变量环境直接使用。
接着函数co_create
的源码继续看,完成协程变量环境初始化后,则调用协程真正的协程创建函数co_create_env
,传入外部入参及环境协程环境变量,并返回创建成功的协程对象指针,源码信息如下:
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;
}
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;
}
// 申请内存创建协程对象(类似于进程控制块,此处可称之为协程控制块,CCB)
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;
// 设置协程运行栈的栈顶指针(相对于bp是低地址)
lp->ctx.ss_sp = stack_mem->stack_buffer;
// 设置协程运行栈的大小
lp->ctx.ss_size = at.stack_size;
// 设置协程栈其他标志
// cStart=0,未启动;
// cIsMain=0,非主协程
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_create_env
主要完成的操作为:
协程对象类型stCoRoutine_t
,我们可参照线程进程的概念,称之为协程控制块,即CCB,其定义如下:
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; // 是否使用共享运行栈
// 程序系统环境变量的指针
void* pvEnv;
//char sRunStack[ 1024 * 128 ];
stStackMem_t* stack_mem;
//save satck buffer while confilct on same stack_buffer;
char* stack_sp;
unsigned int save_size;
char* save_buffer;
// 用于setspecific和getspecific的键值对存储
stCoSpec_t aSpec[1024];
};
此时,一个协程所需要的协程控制块、运行栈、执行上下文、协程执行函数都已经具备,协程创建完毕。
创建协程时,我们指定了执行协程函数的参数类型为stPara_t
,其中包含一个协程条件变量stCoCond_t
,生产者消费者之间的通讯使用的是条件变量,但是此处的条件变量非系统提供的原始类型,而是libco
包装后的数据类型stCoCond_t
,其定义如下:
struct stCoCondItem_t
{
stCoCondItem_t *pPrev;
stCoCondItem_t *pNext;
stCoCond_t *pLink;
stTimeoutItem_t timeout;
};
struct stCoCond_t
{
stCoCondItem_t *head;
stCoCondItem_t *tail;
};
stCoCond_t
仅有两个成员变量head
和tail
,其类型均为stCoCondItem_t
,而stCoCondItem_t
为带有超时的双向链表元素类型,因此stCoCond_t
为一个双向链表(list
)的数据类型。
函数co_cond_alloc
仅是申请分配了类型为stCoCond_t
的对象,后续使用是通过定时器来管理的。
在分析stCoRoutineEnv_t
结构中,有一个stCoEpoll_t
类型的pEpoll
指针成员,作为stCoRoutineEnv_t
的成员,这个结构也是一个全局性的资源,被同一个线程上所有协程共享,是一个跟epoll
的事件循环相关的变量,其定义如下:
struct stCoEpoll_t
{
// epoll 实例的⽂件描述符
int iEpollFd;
// 值为 10240 的整型常量。作为epoll_wait()系统调用的第三个参数
// 即⼀次 epoll_wait 最多返回的就绪事件个数
static const int _EPOLL_SIZE = 1024 * 10;
// 类型为stTimeout_t的结构体指针
// 该结构实际上是⼀个时间轮(Timingwheel)定时器
struct stTimeout_t* pTimeout;
// 指向 stTimeoutItemLink_t 类型的结构体指针
// 该指针实际上是⼀个链表头,链表用于临时存放超时事件的 item
struct stTimeoutItemLink_t* pstTimeoutList;
// 指向 stTimeoutItemLink_t 类型的结构体指针,也是指向⼀个链表
// 该链表用于存放epoll_wait得到的就绪事件和定时器超时事件
struct stTimeoutItemLink_t* pstActiveList;
// 对 epoll_wait()第⼆个参数的封装,即⼀次 epoll_wait 得到的结果集
co_epoll_res* result;
};
我们知道,定时器是事件驱动模型的网络框架一个必不可少的功能。网络I/O的超时,定时任务,包括定时等待(poll或timedwait)都依赖于此。一般而言,使用定时功能时,我们首先向定时器中注册一个定时事件TimerEvent
,在注册定时事件时需要指定这个事件在未来的触发时间。在到了触发时间点后,我们会收到定时器的通知。
网络框架里的定时器可以看做由两部分组成,第一部分是保存已注册timerevents
的数据结构。第二部分则是定时通知机制,保存已注册的timerevents,一般选用红黑树,比如nginx;另外一种常见的数据结构便是时间轮,libco就使用了这种结构。当然你也可以直接用链表来实现,只是时间复杂度比较高,在定时任务很多时会很容易成为框架的性能瓶颈。
定时器的第二部分,高精度的定时(精确到微秒级)通知机制,一般使用getitimer/setitimer这类接口,需要处理信号,是个比较麻烦的事。不过对一般的应16用而言,精确到毫秒就够了。精度放宽到毫秒级时,可以顺便用epoll/kqueue这样的系统调用来完成定时通知;这样一来,网络I/O事件通知与定时事件通知的逻辑就能统一起来了。笔者之前实现过的一个基于libcurl的异步HTTPclient,其中的定时器功能就是用epoll配合红黑树实现的。libco内部也直接使用了epoll来进行定时,不同的只是保存timerevents的用的是时间轮而已。
使用epoll加时间轮的实现定时器的算法如下:
示例中,有两处用到条件变量,分别为co_cond_signal
及co_cond_timedwait
,其功能类似于pthread_cond_signal
和pthread_cond_timewait
,其中co_cond_signal
的源码实现如下:
int co_cond_signal(stCoCond_t* si)
{
// POP并取得头部对象
stCoCondItem_t* sp = co_cond_pop(si);
if (!sp)
{
return 0;
}
// 移除头部的超时对象
RemoveFromLink<stTimeoutItem_t, stTimeoutItemLink_t>(&sp->timeout);
// 将该对象处理到就绪(活动)链表中
AddTail(co_get_curr_thread_env()->pEpoll->pstActiveList, &sp->timeout);
return 0;
}
resume
该函数用于主动让出执行权限去启动别的协程,其源码内容如下:
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_swap(lpCurrRoutine, co);
}
当执行co_swap
函数后,并不会立即返回,因为该函数已经将程序的执行切换到别的协程,因此需要等别的协程执行完或者主动让出执行权限后才会返回。
co_swap
的实现源码如下:
void co_swap(stCoRoutine_t* curr, stCoRoutine_t* pending_co)
{
stCoRoutineEnv_t* env = co_get_curr_thread_env();
//get curr stack 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);
}
}
// 协程上下文切换,从curr切换到pending_co
// 使用汇编代码实现,实现主要寄存器的保持及恢复,完成寄存器的切换
//swap context
coctx_swap(&(curr->ctx), &(pending_co->ctx));
//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);
}
}
}
在libco
中,协程与协程之间存在这严格的调用与被调用的关系,是一种非对称的协程。非对称协程(asymmetric coroutines)是跟一个特定的调用者绑定的,协程让出 CPU 时,只能让回给原调用者,因此协程的切换操作,一般而言只有一个yield
操作,用于将程序控制流转移给另外的协程,即调用者协程。
这篇文章主要是针对上一篇文章的示例中使用到的内容做源码展开解读,因此文章内容看起来比较乱,是对示例的进一步分析,不适合学习libco
的整体情况,如需把握libco
的设计理论及整体技术方案,可参考此处