Greenlet理解要點

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章