Greenlet是给python使用的协程,evenlet就是使用的这个库。greenlet真正实现了协程之间的切换。python协程的实现(greenlet源码分析)这篇博文非常精彩的讲解了greenlet。整个代码一共就两千来行,因为涉及到上下文切换,读起来还是有点困难的。本文主要讲讲理解greenlet的要点。


A. 数据结构

/**
States:
  stack_stop == NULL && stack_start == NULL:  did not start yet
  stack_stop != NULL && stack_start == NULL:  already finished
  stack_stop != NULL && stack_start != NULL:  active
**/
//greenlet对象最终对应的数据的C结构体,这里可以理解为python对象的属性
typedef struct _greenlet {
PyObject_HEAD
char* stack_start;   //栈的顶部  将这里弄成null,标示已经结束了
char* stack_stop;    //栈的底部
char* stack_copy;     //栈保存到的内存地址
intptr_t stack_saved;   //栈保存在外面的大小
struct _greenlet* stack_prev;  //栈之间的上下层关系
struct _greenlet* parent;    //父对象
PyObject* run_info;   //其实也就是run对象
struct _frame* top_frame;   //这里可以理解为主要是控制python程序计数器
int recursion_depth;   //栈深度
PyObject* weakreflist;
PyObject* exc_type;
PyObject* exc_value;
PyObject* exc_traceback;
PyObject* dict;
} PyGreenlet;

    每个协程是一个greenlet。run_info是协程的执行体,也就是eventlet传入的run方法。

    stack_start记录的是该greenlet从当前上下文切换出去的栈指针(寄存器esp)。对于没有经历过换出的greenlet,stack_start记录的是1.

    stack_stop记录的是该greenlet堆栈段的栈底,内容是初次创建时候程序的一个局部变量dummymarker,在g_switch函数的while循环里面声明,所以也是在整个程序的栈空间里面。

    stack_copy记录是栈的副本,防止协程切换的时候这部分数据被新协程冲掉。

    stack_parent记录协程创建时期的父协程(不是切换时候的原协程!)。模块第一次加载,会自动调用green_create_main创建一个名为gmain的协程。python程序创建协程时候没有在协程上下文里面的话,stack_parent将会记录为gmain。

    stack_prev,greenlet层次。大概便是协程切换的轨迹。每个stack_prev指向的协程的stack_stop都要比自己的大。

    注意,栈总是从高地址向低地址方向生长。


B. 协程切换

    首先关注最精彩的部分。

static int
slp_switch(void)
{
    int err;
#ifdef _WIN32
    void *seh;
#endif
    void *ebp, *ebx;
    unsigned short cw;
    register int *stackref, stsizediff;
    __asm__ volatile ("" : : : "esi", "edi");
    __asm__ volatile ("fstcw %0" : "=m" (cw));
    __asm__ volatile ("movl %%ebp, %0" : "=m" (ebp));
    __asm__ volatile ("movl %%ebx, %0" : "=m" (ebx));
#ifdef _WIN32
    __asm__ volatile (
        "movl %%fs:0x0, %%eax\n"
        "movl %%eax, %0\n"
        : "=m" (seh)
        :
        : "eax");
#endif
    __asm__ ("movl %%esp, %0" : "=g" (stackref));
    {
        SLP_SAVE_STATE(stackref, stsizediff);
        __asm__ volatile (
            "addl %0, %%esp\n"
            "addl %0, %%ebp\n"
            :
            : "r" (stsizediff)
            );
        SLP_RESTORE_STATE();
        __asm__ volatile ("xorl %%eax, %%eax" : "=a" (err));
    }
#ifdef _WIN32
    __asm__ volatile (
        "movl %0, %%eax\n"
        "movl %%eax, %%fs:0x0\n"
        :
        : "m" (seh)
        : "eax");
#endif
    __asm__ volatile ("movl %0, %%ebx" : : "m" (ebx));
    __asm__ volatile ("movl %0, %%ebp" : : "m" (ebp));
    __asm__ volatile ("fldcw %0" : : "m" (cw));
    __asm__ volatile ("" : : : "esi", "edi");
    return err;
}
#endif

    针对不同平台,slp_switch有不同的实现。上面给出的是x86体系架构下,unix类系统的实现。其中,中间那个大括号实现了真正的切换。主要是栈的切换。

    esp:栈顶寄存器;ebp:栈帧寄存器。经过SLP_SAVE_STATE之后,被换出的协程(ts_origin)的stack_start被设置为esp。对换出协程上下文做备份。如果被换入的协程为新协程,直接返回1;否则,要做一些换入协程上下文恢复工作之后返回错误码,也就是0。stsizediff是换入协程的esp与换出协程esp的差值,通过对esp、ebp加上这个差值,栈空间变换成了目标协程的栈空间,从而目标协程能够继续原来的代码执行。特别要注意,从SLP_RESTORE_STATE这句开始,就已经在换出协程的栈空间里面。(寄存器变量和普通局部变量的区别凸显出来了。)

    由于dummymarker并不在栈顶,所以切换前后的栈可能会重合一部分。重合的部分需要备份,否则新的协程栈空间的生长会冲掉这部分数据。新旧协程的栈空间关系重合可以分为上下两种情况。

             origin         target              
                                                
 stack_stop  -------                           
                |                               
                V                               
             ---------------------- stack_stop  
             xxxxxxxx     xxxxxxxxx             
 stack_start ----------------------             
                                                
                               |                
                               V                
                            ------- stack_start
               origin                   target
                                       ---------- stack_stop
 stack_stop ------------------------------------- 
             xxxxxxxxxxxx              xxxxxxxxxx
             xxxxxxxxxxxx              xxxxxxxxxx
             xxxxxxxxxxxx              xxxxxxxxxx
             xxxxxxxxxxxx              xxxxxxxxxx
stack_start -------------              ----------
                                           |
                                           V
                                       ---------- stack_start

    由于换入线程的stack_start总是可以生长,因此认为总要比换出线程的小,这样才安全。其中,画叉的部分是需要备份到堆空间的,即stack_copy属性。

C. g_initialstub

    除了gmain,其他协程都是在这里创建的。这里理解的难点在于g_switchstack函数调用一次却返回两次。

    其实了解协程切换的栈备份恢复过程,就不难理解了。换入新的协程时候,备份栈空间之后就返回了1.当新的协程运行(PyEval_CallObjectWithKeywords)结束之后,换入parent协程,经过一段时间,先前换出的协程得到换入,SLP_RESTORE_STATE把栈空间恢复回来,ebp的变更使得调用栈切换回换出时候的上下文,g_switchstack还在栈空间中,被返回。结果是"xorl %%eax, %%eax" : "=a" (err)的执行结果,也就是0.

D. 参数与返回值

    协程调用时候的参数是通过全局变量传入的,可以理解为通过数据段传参。返回值是返回到parent环境中。python的switch调用是拿不到协程的返回值的。