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调用是拿不到协程的返回值的。