非对称协程(asymmetric coroutines):是跟一个特定的调用者绑定的,协程让出 CPU 时,只能让回给原调用者。那到底是什么东西“不对称”呢?其实,非对称在于程序控制流转移到被调协程时使用的是 call/resume 操作,而当被调协程让出 CPU时使用的却是 return/yield 操作。此外,协程间的地位也不对等,caller 与 callee 关系是确定的,不可更改的,非对称协程只能返回最初调用它的协程。
对称协程(symmetric coroutines):启动之后就跟启动之前的协程没有任何关系了。协程的切换操作,一般而言只有一个操作,yield,用于将程序控制流转移给另外的协程。对称协程机制一般需要一个调度器的支持,按一定调度算法去选择 yield的目标协程。libco本质上是一种非对称协程,libco 内部为保存协程的调用链留了一个 stack 结构,而这个 stack 大小只有固定的128。使用 libco,如果不断地在一个协程运行过程中启动另一个协程,随着嵌套深度增加就可能会造成这个栈空间溢出。
关键数据结构:
//libco 的协程控制块 stCoRoutine_t
struct stCoRoutine_t
{
//libco 的协程一旦创建之后便跟创建时的那个线程绑定了的,是不支持在不同线程间迁移(migrate)的。这个 env,即同属于一个线程所有协程的执行环境,包括了当前运行协程、上次切换挂起的协程、嵌套调用的协程栈,和一个 epoll 的封装结构(TBD)。
stCoRoutineEnv_t *env; //协程执行的环境
pfn_co_routine_t pfn; //实际待执行的协程函数
void *arg; //实际待执行的协程参数
//ctx 是一个 coctx_t 类型的结构,用于协程切换时保存 CPU 上下文(context)的;所谓的上下文,即esp、ebp、eip和其他通用寄存器的值。
coctx_t ctx;
//状态和标志变量
char cStart;
char cEnd;
char cIsMain;
char cEnableSysHook;
char cIsShareStack;
//用于保存程序系统环境变量的指针
void *pvEnv;
//协程运行时的栈内存char sRunStack[1024 * 128 ];
stStackMem_t* stack_mem;//实现 stackful 协程(与之相对的还有一种 stackless 协程)的两种技术:Separate coroutine stacks 和 Copying the stack(又叫共享栈)。实现细节上,前者为每一个协程分配一个单独的、固定大小的栈;而后者则仅为正在运行的协程分配栈内存,当协程被调度切换出去时,就把它实际占用的栈内存 copy 保存到一个单独分配的缓冲区;当被切出去的协程再次调度执行时,再一次 copy 将原来保存的栈内存恢复到那个共享的、固定大小的栈内存空间。通常情况下,一个协程实际占用的(从 esp 到栈底)栈空间,相比预分配的这个栈大小(比如 libco的128KB)会小得多;这样一来,copying stack 的实现方案所占用的内存便会少很多。当然,协程切换时拷贝内存的开销有些场景下也是很大的。因此两种方案各有利弊,而libco 则同时实现了两种方案,默认使用前者,也允许用户在创建协程时指定使用共享栈。
//在共享栈缓冲区上确认保存stack缓冲区
char* stack_sp;
unsigned int save_size;
char* save_buffer;stCoSpec_t aSpec[1024];
}//保存协程上下文
struct coctx_t:
{
#if defined(__i386__)
void *regs[ 8 ];
#else
void *regs[ 14 ];
#endif
size_t ss_size;
char *ss_sp;
};//这个结构是跟运行的线程绑定了的,运行在同一个线程上的各协程是共享该结构的,是全局性的资源
struct stCoRoutineEnv_t
{
stCoRoutine_t *pCallStack[ 128 ];
int iCallStackSize;
stCoEpoll_t *pEpoll;//记录协程切换时占有共享栈 lastco的和将要切换运行nextco的协程
stCoRoutine_t* pending_co;
stCoRoutine_t* occupy_co;
};
//stCoRoutineEnv_t 结构里的 pCallStack 不是普通意义上的程序运行栈,那个 ESP(RSP)寄存器指向的栈,是用来保留程序运行过程中局部变量以及函数调用关系的。但是,这个 pCallStack 又跟 ESP(RSP)指向的栈有相似之处。如果将协程看成一种特殊的函数,那么这个 pCallStack 就是保存这些函数的调用链的栈。非对称协程最大特点就是协程间存在明确的调用关系;甚至在有些文献中,启动协程被称作 call,挂起协程叫 return。非对称协程机制下的被调协程只能返回到调用者协程,这种调用关系不能乱,因此必须将调用链保存下来。这即是 pCallStack “调用栈”的作用每当启动(resume)一个协程时,就将它的协程控制块 stCoRoutine_t 结构指针保存在 pCallStack 的“栈顶”,然后“栈指针”iCallStackSize 加 1,最后切换 context 到待启动协程运行。当协程要让出(yield)CPU 时,就将它的 stCoRoutine_t 从 pCallStack 弹出,“栈指针”iCallStackSize 减 1,然后切换 context 到当前栈顶的协程(原来被挂起的调用者)恢复执行。
libco 程序的第一个协程呢? 假如第一个协程 yield 时,CPU控制权让给谁呢?首先我们要明白这“第一个”协程是什么。实际上,libco 的第一个协程,即执行 main 函数的协程,是一个特殊的协程。这个协程又可以称作主协程,它负责协调其他协程的调度执行(网络 I/O 以及定时事件的驱动),它自己则永远不会 yield,不会主动让出 CPU。不让出(yield)CPU,不等于说它一直霸占着 CPU。我们知道 CPU 执行权有两种转移途径,一是通过 yield 让给调用者,其二则是 resume 启动其他协程运行。后文我们可以清楚地看到,co_resume()与 co_yield() 都伴随着上下文切换,即 CPU 控制流的转移。当你在程序中第一次调用co_resume() 时,CPU 执行权就从主协程转移到了 resume 目标协程上了。
主协程是在什么时候创建出来的呢?什么时候 resume 的呢?事实上,主协程是跟 stCoRoutineEnv_t 一起创建的。主协程也无需调用 resume 来启动,它就是程序本身,就是 main 函数。主协程是一个特殊的存在,可以认为它只是一个结构体而已。在程序首次调用 co_create() 时,此函数内部会判断当前进程(线程)的 stCoRoutineEnv_t 结构是否已分配,如果未分配则分配一个,同时分配一个 stCoRoutine_t 结构,并将 pCallStack[0] 指向主协程。此后如果用 co_resume() 启动协程,又会将 resume 的协程压入 pCallStack 栈。
在图中,coroutine2 处于栈顶,也就是说,当前正在 CPU 上 运行的协程是coroutine2。coroutine2 的调用者是谁呢?是谁 resume了coroutine2 呢?是 coroutine1。coroutine1 则是主协程启动的,即在 main 函数里 resume 的。当 coroutine2 让出 CPU 时,只能让给 coroutine1;如果 coroutine1 再让出 CPU,那么又回到了主协程的控制流上了。当控制流回到主协程上时,主协程在干些什么呢?main函数最终调用了 co_eventloop()。该函数是一个基于 epoll/kqueue 的事件循环,负责调度其他协程运行,stCoRoutineEnv_t 结构中的 pEpoll 即使在这里使用。
协程的主要函数分析:
创建协程函数:int co_create( stCoRoutine_t **co,const stCoRoutineAttr_t *attr,void *(*routine)(void*),void *arg );
co: stCoRoutine_t** 类型的指针。输出参数,co_create 内部会为新协程分配⼀个“协程控制块”,*ppco 将指向这个分配的协程控制块。
attr: stCoRoutineAttr_t* 类型的指针。输⼊参数,用于指定要创建协程的属性,可为 NULL。实际上仅有两个属性:栈⼤小、指向共享栈的指针(使用共享栈模式)。
outine: void (*)(void*)类型的函数指针,指向协程的任务函数,即启动这个协程后要完成什么样的任务。routine 类型为函数指针。
arg: void* 类型指针,传递给任务函数的参数。调用 co_create 将协程创建出来后,这时候它还没有启动,也即是说我们传递的routine 函数还没有被调用。实质上,这个函数内部仅仅是分配并初始化 stCoRoutine_t结构体、设置任务函数指针、分配一段“栈”内存,以及分配和初始化 coctx_t。为什么这里的“栈”要加个引号呢?因为这里的栈内存,无论是使用预先分配的共享栈,还是co_create 内部单独分配的栈,其实都是调用 malloc 从进程的堆内存分配出来的。对于协程而言,这就是“栈”,而对于底层的进程(线程)来说这只不过是普通的堆内存而已。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() 进行初始化
co_init_curr_thread_env();
}
//分配一个协程控制块,获取当前线程环境,传入sttr,pfn,arg,创建协程环境
stCoRoutine_t *co = co_create_env( co_get_curr_thread_env(), attr, pfn,arg );
*ppco = co;
return 0;
}
协程初始化函数:void 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;coctx_init( &self->ctx );
env->pCallStack[ env->iCallStackSize++ ] = self;
stCoEpoll_t *ev = AllocEpoll();
SetEpoll( env,ev );
}
co_init_curr_thread_env()会生成当前环境g_arrCoEnvPerThread[GetPid()]的第一个协程 env->pCallStack,其 cIsMain 标志位 1,iCallStackSize表示协程层数,目前只有1层,AllocEpoll()函数中初始化当前环境env的 pstActiveList、pstTimeoutList 这两个列表,这两个列表分别记录了活动协程和超时协程。环境初始化操作在一个线程中只会进行一次。在初始化完成之后,会调用co_create_env()创建一个新的协程,新协程的结构体中的env这个域始终指向当前协程环境g_arrCoEnvPerThread[ GetPid() ]。新协程创建之后,并没有做什么操作。
启动协程函数:void co_resume( stCoRoutine_t *co );
启动 co 指针指向的协程。libco 的协程是非对称协程,协程在让出CPU 后要恢复执行的时候,还是要再次调用co_resume 函数的去“启动”协程运行的。从语义上来讲,co_start 只有一次,而 co_resume 可以是暂停之后恢复启动,可以多次调用。讲到非对称协程,一般用“resume”与“yield”这两个术语。协程要获得 CPU 执行权用“resume”,而让出 CPU 执行权用“yield”,这两个是两个不同的(不对称的)过程,因此这种机制才被称为非对称协程(asymmetric coroutines)。所以讲到 resume 一个协程,我们一定得注意,这可能是第一次启动该协程,也可以是要准备重新运行挂起的协程。我们可以认为在 libco 里面协程只有两种状态,即running 和 pending。当创建一个协程并调用 resume 之后便进入了 running 状态,之后协程可能通过 yield 让出 CPU,这就进入了 pending 状态。不断在这两个状态间循环往复,直到协程退出(执行的任务函数 routine 返回)
co_resume() 启动一个协程的含义,不是“创建一个并发任务”。进入 co_resume() 函数后发生协程的上下文切换,协程的任务函数是立即就会被执行的,而且这个执行过程不是并发的(Concurrent)。为什么不是并发的呢?因为 co_resume() 函数内部会调用 coctx_swap() 将当前协程挂起,然后就开始执行目标协程的代码了。本质上这个过程是串行的,在一个操作系统线程(进程)上发生的,甚至可以说在一颗 CPU 核上发生的。将 coroutine 当做一种特殊的subroutine 来看:A 协程调用 co_resume(B) 启动了 B 协程,本质上是一种特殊的过程调用关系,A 调用 B 进入了 B 过程内部,这很显然是一种串行执行的关系。既然 co_resume() 调用后进入了被调协程执行控制流,那么 co_resume()函数本身何时返回?这就要等被调协程主动让出 CPU 了。
co_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;
co_swap( lpCurrRoutine, co );
}第2行是获取当前线程的协程环境env,第3行获取当前正在执行的协程,也即马上要被切换出去的协程。第 5、6 行的 if 条件分支用来判断待切换的协程co是否已经被切换过,当且仅当协程是第一次启动时才会执行到,首次启动协程过程有点特殊,需要调用 coctx_make() 为新协程准备 context上下文(为了让 co_swap() 内能跳转到协程的任务函数),并将 cStart 标志变量置 1。这里为新协程co准备的上下文,就是在coctx_make()函数里面,这个函数将函数指针CoRoutineFunc赋值给co->ctx的reg[0],将来上下文切换的时候,就能切换到reg[0]所指向的地址去执行。第 8 行将将待切换的协程co 压入 pCallStack 栈,置于协程环境env的协程栈的顶端,表明当前最新的协程是co。最后,调用co_swap(),函数将协程上下文环境切换为新协程co的上下文环境,并进入新协程co指定的函数内执行,之前被切换出去的协程被挂起,直到新协程co主动yield,让出cpu,才会恢复被切换出去的协程执行。
所有的协程都是在当前协程执行的,也就是说,所有的协程都是串行执行的,调用co_resume()之后,执行上下文就跳到新协程co的代码空间中去了。co_swap() 不会就此返回,这次 resume 的 新协程co 协程内部可能会resume新的协程继续执行下去,因此直到这次的新协程co主动yield 让出 CPU 时才会返回到co_resume() 中来所以co_swap()函数调用可能要等到很长时间才能返回。值得指出的是,这里讲 co_swap() 不会就此返回,不是说这个函数就阻塞在这里等待 co 这个协程 yield 让出 CPU,实际上co_swap() 内部已经切换了 CPU 执行上下文,奔着新协程co的代码路径去执行了。
挂起协程函数:void co_yield( stCoRoutine_t *co );
void co_yield_env( stCoRoutineEnv_t *env );
void co_yield_ct(); //ct = current thread
void co_yield( stCoRoutine_t *co )
{
co_yield_env( co->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--;
co_swap( curr, last);
}
在调用co_yield_env(),进入co_swap()之后,调用coctx_swap(),切换到上一次的last协程的上下文,那么当前协程的co_swap()函数里面的变量,都是在栈空间上面的,切换到last协程的上下文之后,那些变量依然在栈空间上面,不会被销毁?直到回到了main函数的协程,还是没有被销毁?。
其实这些变量其实不是在栈空间上面,而是在CPU的通用寄存器里面,当调用coctx_swap()之后,这些寄存器变量就会保存到当前协程的栈空间中去,其实是我们之前co_create()函数malloc出来的一片堆空间。这是因为cpu的工作寄存器数量较多,而局部变量较少,而co_swap()函数的变量都是局部变量,直接存放在cpu的工作寄存器中,而coctx_swap()的作用就是将CPU的各个通用寄存器保存到coctx_t结构的regs[1] ~ regs[6]的位置,然后将last协程的coctx_t结构的regs[1]~regs[6]的内容加载到当前的通用寄存器中,并将执行cpu的执行顺序切换到last协程中去执行。在非对称协程理论,yield 与 resume 是个相对的操作。A 协程 resume 启动了 B 协程,那么只有当 B 协程执行 yield 操作时才会返回到 A 协程。当协程正常结束的时候,会继续执行CoRoutineFunc()函数,将协程的cEnd设置为1,表示已经结束,并执行一次co_yield_env(),让出cpu,切换回上一次被让出的协程继续执行。函数 co_resume() ,内部 co_swap() 会执行被调协程的代码。只有被调协程 yield 让出 CPU,调用者协程的 co_swap() 函数才能返回到原点,即返回到原来co_resume() 内的位置。在被调协程要让出 CPU 时,会将它的 stCoRoutine_t 从pCallStack 弹出,“栈指针”iCallStackSize 减 1,然后 co_swap() 切换 CPU 上下文到原来被挂起的调用者协程恢复执行。这里“被挂起的调用者协程”,即是调用者 co_resume()中切换 CPU 上下文被挂起的那个协程。
co_resume 是有明确目的对象的,而且可以通过 resume 将 CPU 交给任意协程。但 yield 则不一样,你只能 yield 给当前协程的调用者。而当前协程的调用者,即最初 resume 当前协程的协程,是保存在 stCoRoutineEnv_t的 pCallStack 中的。因此你只能 yield 给“env”,yield 给调用者协程;而不能随意 yield给任意协程,CPU 不是你想让给谁就能让给谁的。事实上,libco 提供了一个 co_yield(stCoRoutine_t* co) 的函数。看起来你似乎可以将CPU 让给任意协程。实际上并非如此:
void co_yield( stCoRoutine_t *co )
{
co_yield_env( co->env );
}
我们知道,同一个线程上所有协程是共享一个 stCoRoutineEnv_t 结构的,因此任意协程的 co->env 指向的结构都相同。如果你调用 co_yield(co),就以为将 CPU 让给 co 协程了,那就错了。最终通过 co_yield_env() 还是会将 CPU 让给原来启动当前协程的调用者。libco 的协程是不支持线程间迁移(migration)的,同一个线程上所有协程共享 stCoRoutineEnv_t,那么co_yield() 给其他线程上的协程,程序一定会挂掉。这个 co_yield() 其实容易让人产生误解的,协程库内虽然提供了 co_yield(stCoRoutine_t* co) 函数,但是没有任何地方有调用过该函数。使用的较多的是另外一个函数co_yield_ct(),其实本质上作用都是一样的。
/*void co_yield_ct()
{
co_yield_env( co_get_curr_thread_env() );
}stCoRoutineEnv_t *co_get_curr_thread_env()
{
return gCoEnvPerThread;
}static __thread stCoRoutineEnv_t* gCoEnvPerThread = NULL;
void co_init_curr_thread_env()
{
gCoEnvPerThread = (stCoRoutineEnv_t*)calloc( 1, sizeof(stCoRoutineEnv_t) );
stCoRoutineEnv_t *env = gCoEnvPerThread;
......
}*/
切换协程函数:void co_swap(stCoRoutine_t* curr, stCoRoutine_t* pending_co){}
curr :当前协程的 coctx_t结构指针,输出参数,函数调用过程中会将当前协程的 context 保存在这个参数指向的内存里
pending:即待切入的协程的 coctx_t 指针,输入参数,coctx_swap 从这里取上次保存的 context,恢复各寄存器的值。
coctx_t 结构:用于保存各寄存器值(context)
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);
}
}//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);
}
}
}
//在co_swap()函数代码中,由于libco不是共享栈的模式,即pending_co->cIsShareStack为0,所以执行了if分支,接下来执行coctx_swap(),这是一段汇编源码,内容就是从curr的上下文跳转到pending_co的上下文中执行,通过回调CoRoutineFunc()函数实现,此时当前线程的cpu已经开始执行pending_co协程中的代码,直到pending_co主动让出cpu,才接着执行coctx_swap()下面的代码,由于update_occupy_co为NULL,下面的if语句没有执行,所以相当于coctx_swap()下面没有代码,直接返回到curr协程中
coctx_swap的汇编详解:
.globl coctx_swap
#if !defined( __APPLE__ ) && !defined( __FreeBSD__ )
.type coctx_swap, @function
#endif
coctx_swap:#if defined(__i386__)
//LEA 指令即 Load Effective Address 的缩写。这条指令把 4(%esp) 有效地址保存到 eax 寄存器,可以认为是将当前的栈顶地址保存下来(实际保存的地址比栈顶还要⾼ 4 字节,为了⽅便我们就称之为栈顶)。为什么要保存栈指针呢,因为紧接着就要进⾏栈切换了。
leal 4(%esp), %eax //sp//此时4(%esp) 内正是指向 current 协程 coctx_t 的指针,这里把它塞到 esp 寄存器。接下来将 coctx_t 指针指向的地址加上 32 个字节的内存位置加载到 esp 中。经过这么一倒腾,esp 寄存器实际上指向了当前协程 coctx_t 结构的 ss_size 成员位置,在它之下有个名为 regs 的数组,刚好是用来保存 8 个寄存器值的。注意这是第⼀次栈切换,不过是临时性的,目的只是⽅便接下来使用 push 指令保存各寄存器值。
movl 4(%esp), %esp
leal 32(%esp), %esp //parm a : ®s[7] + sizeof(void*)//eax 寄存器内容压栈。更准确的讲,是将 eax 寄存器保存到了 coctx_t->regs[7] 的位置。注意到eax 寄存器已经保存了原栈顶的地址,所以这句实际上是将当前协程栈顶保存起来,以备下次调度回来时恢复栈地址。
pushl %eax //esp ->parm a
//保存各通用寄存器的值,到 coctx_t 结构的 regs[1]~regs[6] 的位置。
pushl %ebp
pushl %esi
pushl %edi
pushl %edx
pushl %ecx
pushl %ebx
//-4(%eax) 实际上是指向原来 coctx_swap 刚进来时的栈顶,我们讲过栈顶的值是 call 指令自动压⼊的函数返回地址。这句实际上就是将 coctx_swap 的返回地址给保存起来了,放在coctx_t->regs[0] 的位置。
pushl -4(%eax)//⾄此,current 协程的各重要寄存器都已保存完成了,开始可以放⼼地交班给 pending 协程了。接下来我们需要将 pending 协程调度起来运⾏,就需要为它恢复context——恢复各通用寄存器的值以及栈指针。因此这⼀⾏将栈指针切到 pending 协程的 coctx_t 结构体开始,即 regs[0] 的位置,为恢复寄存器值做好了准备。
movl 4(%eax), %esp //parm b -> ®s[0]
//弹出 regs[0] 的值到 eax 寄存器。regs[0] 正该协程上次被切换出去时在第 19 ⾏保存的值,即 coctx_swap 的返回地址。
popl %eax //ret func addr
//从 regs[1]~regs[6] 恢复各寄存器的值(与之相应的是前面第 13~18 ⾏的压栈操作)
popl %ebx
popl %ecx
popl %edx
popl %edi
popl %esi
popl %ebp
//将 pending 协程上次切换出去时的栈指针恢复(与之对应的是第 12 ⾏压栈操作)。请思考⼀下,栈内容已经完全恢复了吗?注意到第 8 ⾏我们讲过,当时保存的“栈顶”比真正的栈顶差了⼀个 4 字节的偏移。⽽这 4 字节真正栈顶的内容,正是coctx_swap 的返回地址。如果此时程序就执⾏ ret 指令返回,那程序就不知道会跑到哪去了。
popl %esp
//为了程序能正确地返回原来的coctx_swap调用的地⽅,将eax 内容(第19⾏保存⾄regs[7],第23⾏取出来到eax)压栈。
pushl %eax //set ret func addr//清零eax 寄存器,执⾏返回指令
xorl %eax, %eax
ret//一共切换了 3 次栈指针
co_release:释放资源避免内存泄漏
void co_release( stCoRoutine_t *co )
{
if( co->cEnd )
{
free( co );
}
}
co_self:获取当前正在执行的协程,获取当前协程环境的线程栈顶的协程
stCoRoutine_t *co_self()
{
return GetCurrThreadCo();
}stCoRoutine_t *GetCurrThreadCo( )
{
stCoRoutineEnv_t *env = co_get_curr_thread_env();
if( !env ) return 0;
return GetCurrCo(env);
}stCoRoutine_t *GetCurrCo( stCoRoutineEnv_t *env )
{
return env->pCallStack[ env->iCallStackSize - 1 ];
}libco封装了系统调用,在系统调用,比如send/recv/condition_wait等函数前面加了一层hook,有了这层hook就可以在系统调用的时候不让线程阻塞而产生线程切换,co_enable_hook_sys()函数允许协程hook,当然也可以不允许hook,直接使用原生的系统调用。
co_enable_hook_sys:
void co_enable_hook_sys()
{
stCoRoutine_t *co = GetCurrThreadCo();
if( co )
{
co->cEnableSysHook = 1;
}
}