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