Greenlet切换源码分析

Greenlet

协程可以算是自定义控制切换的微线程。

栈切换的本质

1.栈

  • 栈是从高地址向低地址
  • 栈帧(stack frame),机器用栈来传递过程参数,存储返回信息,保存寄存器用于以后恢复,以及本地存储。为单个过程(函数调用)分配的那部分栈称为栈帧。栈帧其实是两个指针寄存器,寄存器%ebp为帧指针,而寄存器%esp为栈指针

2.切换

  • 切换其实是切换的执行位置(top_frame)。
  • 但是当我切换执行位置,同时要切换到目的栈,同时要保证栈内数据没有丢失,且没有被无意修改。这就需要栈数据的保存与恢复(slp_switch)。

如何进行切换?

1. C栈切换

其实协程的一个很特殊的例子,就是函数调用。下面这个例子在main中调用func

#include<stdio.h>
int func(int arg)
{
    int d=4;
    int e=5;
    int f;
    f=d+e+arg;
    return f;
}

int main()
{
    int a=1;
    int b=2;
    int c=3;
    func(c);
    c=a+b;
}

用gcc生成汇编code,建议在redhat或centos下

.file   "stackpointer.c"
.text
.globl func
.type   func, @function
func:
pushl   %ebp
movl    %esp, %ebp
subl    $16, %esp
movl    $4, -12(%ebp)
movl    $5, -8(%ebp)
movl    -8(%ebp), %eax
movl    -12(%ebp), %edx
leal    (%edx,%eax), %eax
addl    8(%ebp), %eax
movl    %eax, -4(%ebp)
movl    -4(%ebp), %eax
leave
ret
.size   func, .-func
.globl main
.type   main, @function
main:
pushl   %ebp
movl    %esp, %ebp
subl    $20, %esp
movl    $1, -12(%ebp)
movl    $2, -8(%ebp)
movl    $3, -4(%ebp)
movl    -4(%ebp), %eax
movl    %eax, (%esp)  
call    func
movl    -8(%ebp), %eax
movl    -12(%ebp), %edx
leal    (%edx,%eax), %eax
movl    %eax, -4(%ebp)
leave 
ret
.size   main, .-main
.ident  "GCC: (GNU) 4.4.7 20120313 (Red Hat 4.4.7-11)"
.section        .note.GNU-stack,"",@progbits

call调用完成保存ip寄存器以及jump的作用,进入func后保存之前的bp,设置新的栈顶和栈底(在leave时恢复)
。然后将临时、本地数据保存以新的bp进行偏移保存,最后恢复ebp和esp,返回到caller继续执行。

call func作用
  • push ip,保存下一条指令的地址
  • jump func,修改ip跳转到func执行函数
func作用
  • push ebp,保存bp
  • mov esp,ebp,设置新的栈底。
  • 以新的bp进行偏移,保存临时、本地变量,完成函数功能
  • leave(等价与mov ebp,esp;pop ebp)恢复esp和ebp
  • ret 恢复ip,回到call的下一条指令继续执行。

2. Python栈切换

我们进行的切换方式与此类似,但是python的栈和c栈不同,python栈建立在虚拟机上。
总体上说,就是先进行c栈切换,关于ip设置跳转到下条指令执行(即执行位置的切换,如何跳到函数位置开始执行,如何从函数返回原来位置执行),需要在python上实现top_frame的设置。
具体细节参考:

switch具体实现

几个注意点:

  • 导入greenlet会初始化一个main_greenlet,并设置current为main_greenlet
  • greenlet运行结束,会返回到父greenlet执行
from greenlet import greenlet  

def func1(arg):  
    print (arg)  
    gr2.switch()  
    print ("func1 end")  

def func2():  
    print ("fun2 come")  

#设置parent为main_greenlet
gr1 = greenlet(func1)  
gr2 = greenlet(func2)  
value = gr1.switch("fun1 come")  
print (value)  

首先:

gr1.switch("func1")

会调用g_switch函数,其中target=gr1,args=('func1')

static PyObject *
g_switch(PyGreenlet* target, PyObject* args, PyObject* kwargs)
{
  ...
  while (target) {
  if (PyGreenlet_ACTIVE(target)) {
    ts_target = target;
    err = g_switchstack();
    break;
  }
  if (!PyGreenlet_STARTED(target)) {
    void* dummymarker;
    ts_target = target;
    err = g_initialstub(&dummymarker);
    if (err == 1) {
      continue; /* retry the switch */
    }
    break;
  }
  target = target->parent;
  }
  ...
}
  • gr1(new_greenlet),默认stack_start = NULL(没有运行),stack_stop = NULL(没有启动),因而执行g_initialstub()
  • dummymarker设置为栈底
  • 为什么要将dummymarker栈底设置于此处?
    g_initialstub的栈中包含函数需要的参数等数据,然而&dummymarker的位置恰为g_initialstub栈的ebp。

g_initialstub

代码已简化

static int GREENLET_NOINLINE(g_initialstub)(void* mark))
{
  ...
  /* 设置stack_stop,表明start该greenlet */
  self->stack_start = NULL;
  self->stack_stop = (char*) mark;

  /* 设置target的上一个活动栈 */
  /* Example:g1_greenlet.stack_prev=main_greenlet */
  if (ts_current->stack_start == NULL) {
    /* ts_current is dying */
    self->stack_prev = ts_current->stack_prev;
  }
  else {
    self->stack_prev = ts_current;
  }
  /* 核心代码,进行栈切换 */
  err = g_switchstack();

  /* 标志greenlet正在运行,将要运行PyEval_CallObjectWithKeywords */
  self->stack_start = (char*) 1;  /* running

  /* 设置当前运行参数为parent参数 */
  self->run_info = green_statedict(self->parent);
​
  /* 开始执行函数 */
  /* 注意:可能在该函数运行过程中,存在switch其他的greenlet,否则运行到函数结束 */
  result = PyEval_CallObjectWithKeywords(
    run, args, kwargs);

  /* 标志函数结束 */
  self->stack_start = NULL;  /* dead */

  /* 函数结束切换到parent运行 */
  for (parent = self->parent; parent != NULL; parent = parent->parent) {
    result = g_switch(parent, result, NULL);
}

  • 设置当前greenlet的stack_prev为ts_current,即上一个正在运行的栈
  • PyEval_CallObjectWithKeywords过程中可能会切换另一个greenlet,否则函数运行到结束

g_switchstack

static int g_switchstack(void)
{
    int err;
    {   /* save state */
        /* 保存线程状态或者说EIP */
        PyGreenlet* current = ts_current;
        PyThreadState* tstate = PyThreadState_GET();
        current->recursion_depth = tstate->recursion_depth;
        current->top_frame = tstate->frame;
        current->exc_type = tstate->exc_type;
        current->exc_value = tstate->exc_value;
        current->exc_traceback = tstate->exc_traceback;
    }
    /* 汇编实现栈切换,分不同平台 */
    err = slp_switch();
    if (err < 0) {   /* error */
        PyGreenlet* current = ts_current;
        current->top_frame = NULL;
        current->exc_type = NULL;
        current->exc_value = NULL;
        current->exc_traceback = NULL;

        assert(ts_origin == NULL);
        ts_target = NULL;
    }
    else {
        /* 恢复线程状态,或者说EIP,即跳转执行位置 */
        PyGreenlet* target = ts_target;
        PyGreenlet* origin = ts_current;
        PyThreadState* tstate = PyThreadState_GET();
        tstate->recursion_depth = target->recursion_depth;
        tstate->frame = target->top_frame;
        target->top_frame = NULL;
        tstate->exc_type = target->exc_type;
        target->exc_type = NULL;
        tstate->exc_value = target->exc_value;
        target->exc_value = NULL;
        tstate->exc_traceback = target->exc_traceback;
        target->exc_traceback = NULL;

        assert(ts_origin == NULL);
        Py_INCREF(target);
        ts_current = target;
        ts_origin = origin;
        ts_target = NULL;
    }
    return err;
}
  • 保存线程状态,即EIP
  • 进行C栈切换,汇编实现
  • 恢复目标线程状态,即跳转执行位置

slp_switch(核心代码)

static int
slp_switch(void)
{
    /* 下面变量保存在栈(current)中 */
    int err;
    void* rbp;
    void* rbx;
    unsigned int csr;
    unsigned short cw;
    register long *stackref, stsizediff;
    /* 这里save的是current线程的状态,变量保存在栈中 */
    __asm__ volatile ("" : : : REGS_TO_SAVE);
    __asm__ volatile ("fstcw %0" : "=m" (cw));
    __asm__ volatile ("stmxcsr %0" : "=m" (csr));
    __asm__ volatile ("movq %%rbp, %0" : "=m" (rbp));
    __asm__ volatile ("movq %%rbx, %0" : "=m" (rbx));
    __asm__ ("movq %%rsp, %0" : "=g" (stackref));
    {
        /* 保存当前线程的数据,包括上面的那些寄存器等等数据 */
        /* 当为new_greenlet直接返回1,无栈可切换 */
        SLP_SAVE_STATE(stackref, stsizediff);
        
        /* 重要!current在此暂停,target从此处继续之前的状态之前 */
        __asm__ volatile (
            "addq %0, %%rsp\n"
            "addq %0, %%rbp\n"
            :
            : "r" (stsizediff)
            );
        /* 恢复栈(target)中数据 */
        SLP_RESTORE_STATE();
        __asm__ volatile ("xorq %%rax, %%rax" : "=a" (err));
    }
    /* 恢复寄存器变量,这里恢复的是之前保存在target栈中的变量 */
    /* 恢复了target的esp和ebp,因为变量的保存是以ebp进行偏移寻址中,所以当进行恢复时,进行相同偏移,但是因为ebp为已变为之前的target栈,因而恢复的寄存器也仍为之前的状态。 */
    __asm__ volatile ("movq %0, %%rbx" : : "m" (rbx));
    __asm__ volatile ("movq %0, %%rbp" : : "m" (rbp));
    __asm__ volatile ("ldmxcsr %0" : : "m" (csr));
    __asm__ volatile ("fldcw %0" : : "m" (cw));
    __asm__ volatile ("" : : : REGS_TO_SAVE);
    return err;
}
  • 很重要的一点,当从恢复ebp和esp开始,current暂停,target继续之前运行,恢复之前数据,恢复的寄存器也仍为之前保存的状态,因为他们是基于ebp的偏移寻址,寻址方式不变,只受ebp的控制。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容

  • 原文地址:C语言函数调用栈(一)C语言函数调用栈(二) 0 引言 程序的执行过程可看作连续的函数调用。当一个函数执...
    小猪啊呜阅读 4,585评论 1 19
  • 首先寄存器使用惯例:eip :指令地址寄存器,保存程序计数器的值,当前执行的指令的下一条指令的地址值,16位中为i...
    扎Zn了老Fe阅读 1,955评论 0 0
  • 站在巨人的肩膀上——IDA PRO权威指南阅读笔记 一,窗口 view->open subviews 打开/关闭各...
    SueLyon阅读 14,309评论 0 6
  • 一、温故而知新 1. 内存不够怎么办 内存简单分配策略的问题地址空间不隔离内存使用效率低程序运行的地址不确定 关于...
    SeanCST阅读 7,774评论 0 27
  • 堆栈是连续的地址空间,且向低地址端生长。 esp 是堆栈指针ebp 是基址指针那两条指令的意思是将栈顶指向ebp的...
    wyrover阅读 1,062评论 0 1