Linux_libco协程库剖析

非对称协程(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。不让出(yieldCPU,不等于说它一直霸占着 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 上 运行的协程是coroutine2coroutine2 的调用者是谁呢?是谁 resumecoroutine2 呢?是 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 标志位 1iCallStackSize表示协程层数,目前只有1层,AllocEpoll()函数中初始化当前环境envpstActiveList、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->ctxreg[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_coNULL,下面的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 : &regs[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 -> &regs[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;
    }
}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值