协程实现原理

用户空间切换

linux 的系统调用提供了在用户空间进行上下文切换的能力。go 语言中用户空间的上下文切换用的是汇编实现,怀疑可能是为了跨平台及提高效率而为之。后面用 linux 提供的系统调用来实现一个简单的用户空间上下文切换,反汇编它,看与 go 语言的汇编实现有什么异同。下面首先来看想关的四个系统调用。毕竟是系统调用,会带来用户态和内核态之间的切换开销,这可能也是 go 用汇编实现的原因之一。

struct ucontext

先来看一下关键的数据结构:

#include <ucontext.h>
typedef struct ucontext {
        struct ucontext *uc_link;
        sigset_t         uc_sigmask;
        stack_t          uc_stack;
        mcontext_t       uc_mcontext;
        ...
    } ucontext_t;

其中 uc_link 是当前上下文结束,程序继续执行的上下文。 uc_sigmask 是该上下文的信号屏蔽掩码。uc_stack 是该上下文使用的栈。 uc_mcontext 是机器相关的上下文保存内容,主要包括调用线程的寄存器。

getcontext
int getcontext(ucontext_t *ucp);

用当前活跃的用户上下文初始化 ucp 指向的结构体。 getcontext 这个函数是汇编实现的,在 getcontext.S 这个文件里。

#include "offsets.h"

/*  int _Ux86_getcontext (ucontext_t *ucp)

  Saves the machine context in UCP necessary for libunwind.
  Unlike the libc implementation, we don't save the signal mask
  and hence avoid the cost of a system call per unwind.

*/

/*    .global _Ux86_getcontext
    .type _Ux86_getcontext, @function
_Ux86_getcontext:*/
    .global getcontext
    .type getcontext, @function
getcontext:
    .cfi_startproc
    mov    4(%esp),%eax  /* ucontext_t* */

    /* EAX is not preserved. */
    movl    $0, (LINUX_UC_MCONTEXT_OFF+LINUX_SC_EAX_OFF)(%eax)

    movl    %ebx, (LINUX_UC_MCONTEXT_OFF+LINUX_SC_EBX_OFF)(%eax)
    movl    %ecx, (LINUX_UC_MCONTEXT_OFF+LINUX_SC_ECX_OFF)(%eax)
    movl    %edx, (LINUX_UC_MCONTEXT_OFF+LINUX_SC_EDX_OFF)(%eax)
    movl    %edi, (LINUX_UC_MCONTEXT_OFF+LINUX_SC_EDI_OFF)(%eax)
    movl    %esi, (LINUX_UC_MCONTEXT_OFF+LINUX_SC_ESI_OFF)(%eax)
    movl    %ebp, (LINUX_UC_MCONTEXT_OFF+LINUX_SC_EBP_OFF)(%eax)

    movl    (%esp), %ecx
    movl    %ecx, (LINUX_UC_MCONTEXT_OFF+LINUX_SC_EIP_OFF)(%eax)

    leal    4(%esp), %ecx        /* Exclude the return address.  */
    movl    %ecx, (LINUX_UC_MCONTEXT_OFF+LINUX_SC_ESP_OFF)(%eax)

    /* glibc getcontext saves FS, but not GS */
    xorl    %ecx, %ecx
    movw    %fs, %cx
    movl    %ecx, (LINUX_UC_MCONTEXT_OFF+LINUX_SC_FS_OFF)(%eax)

    leal    LINUX_UC_FPREGS_MEM_OFF(%eax), %ecx
    movl    %ecx, (LINUX_UC_MCONTEXT_OFF+LINUX_SC_FPSTATE_OFF)(%eax)
    fnstenv    (%ecx)
    fldenv    (%ecx)

    xor    %eax, %eax
    ret
    .cfi_endproc
    /*.size    _Ux86_getcontext, . - _Ux86_getcontext*/
    .size    getcontext, . - getcontext

    /* We do not need executable stack.  */
    .section        .note.GNU-stack,"",@progbits

与 go 中的汇编实现还是有点相像的。做的事情其实是差不多的,只不过这是通过系统调用实现的。主要就是把各寄存器的值保存到内存结构体中。

setcontext
#include <ucontext.h>
int
setcontext(const ucontext_t *ucp);

setcontext 将之前保存的 ucp 指针指向的 context 恢复到当前线程的上下文中,即恢复各种寄存器。
其中 ucp 指向的 context 要不来自 getcontext 要不来自 makecontext。如果来自 getcontext,则跟什么都没发生过一样。如果来自 makecontext,则执行 makecontext 里指定的函数,如果执行完毕则继续执行 uc_link 指向的 context,如果 uc_link 是 null,程序结束。
setcontext 也是汇编实现的,基本上就是 getcontext 的逆操作。

makecontext switchcontext
#include <ucontext.h>

     void
     makecontext(ucontext_t *ucp, void (*func)(), int argc, ...);

     int
     swapcontext(ucontext_t *oucp, const ucontext_t *ucp);

makecontext 修改 ucp 指向的 context。the caller must allocate a new stack for this context and assign its address to ucp->uc_stack, and define a successor context and assign its address to ucp->uc_link. Also the func.

When this context is later activated (using setcontext(3) or swapcontext()) the function func is called, and passed the series of integer (int) arguments that follow argc; the caller must specify the number of these arguments in argc. When this function returns, the successor context is activated. If the successor context pointer is NULL, the thread exits.

The swapcontext() function saves the current context in the structure pointed to by oucp, and then activates the context pointed to by ucp.

其中 makecontext 是 c 实现的,因为不涉及寄存器的操作,switchcontext 是汇编实现的,可以看做是 getcontext 和 setcontext 的结合。

一个例子
#include <ucontext.h>
#include <stdlib.h>
#include <stdio.h>

ucontext_t ctx[3];
ucontext_t* running;

int c = 80;

void shedule() {
    swapcontext(running, &ctx[0]);
}

void foo1(int a, int b) {
    printf("foo1 %d %d\n", a, b);
    printf("c: %d\n", c);
    printf("foo1 yield....\n");
    shedule();
    printf("foo1 resume and exit\n");
}

void foo2() {
    printf("foo2...\n");
    printf("foo2 yield....\n");
    shedule();
    printf("foo2 resume and exit\n");
}

int main() {
    char st1[8192];
    char st2[8192];

    // shedule g foo1
    getcontext(&ctx[1]);
    ctx[1].uc_stack.ss_sp = st1;
    ctx[1].uc_stack.ss_size = sizeof(st1);
    ctx[1].uc_link = &ctx[0];
    makecontext(&ctx[1], foo1, 2, 7, 8);
    running = &ctx[1];
    swapcontext(&ctx[0], &ctx[1]);

    // shedule g foo2
    getcontext(&ctx[2]);
    ctx[2].uc_stack.ss_sp = st2;
    ctx[2].uc_stack.ss_size = sizeof(st2);
    ctx[2].uc_link = &ctx[0];
    makecontext(&ctx[2], foo2, 0);
    running = &ctx[2];
    swapcontext(&ctx[0], &ctx[2]);

    // shedule g foo2
    swapcontext(&ctx[0], &ctx[2]);
    // shedule g foo1
    swapcontext(&ctx[0], &ctx[1]);

    // m over
    printf("m over...\n");

    return 0;
}

上面的小程序简单的揭示了协程实现的底层原理。在 go 协程实现中,有三个概念 G,P,M。G 是用户级协程,M 是系统级线程。每个 M 只有绑定了 P,才可以运行 G。P 的个数是有限的,通常与系统的 CPU 核心数相同,用来限制并发数。三者具体的关系将在另一篇文章中描述。
在上面的程序中,M 对应的就是 main 主线程。而 foo1 和 foo2 分别对应两个 G。M 先是调度运行了 foo1,在 foo1 里调用 schedule 函数主动让出 cpu ,schedule 有点类似 python 中的 yield。foo1 让出 cpu 之后,M 继续进行调度,继续调度 foo2 ,在 foo2 里同样利用 schedule 让出 cpu。回到 M 进行调度,M 调度 foo2, foo2 从上次切换之后的位置继续运行直到退出,回到 M。然后调度 foo1,同样退出。M 退出。
在实际的应用中,G 通常会组成一个队列。M 循环从队列里取出 G 进行运行,中途切换出去的 G 也会放会队列中,等待 M 下次调度。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,287评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,346评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,277评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,132评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,147评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,106评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,019评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,862评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,301评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,521评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,682评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,405评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,996评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,651评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,803评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,674评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,563评论 2 352

推荐阅读更多精彩内容