人人都可以写协程:一个基于汇编的协程实现

22 篇文章 0 订阅
7 篇文章 1 订阅


本文以有栈非对称协程的实现为例。项目源码在 https://github.com/lylhw13/Coroutine-in-C

准备

基本概念

编写协程前需要明白几个概念:

有栈协程和无栈协程:

  • 有栈协程:通过保存运行时的堆栈及运行时的上下文来保存运行状态,需要改变调用栈。
  • 无栈协程:通过闭包或者状态机的方法记录程序的运行状态,不改变调用栈。

对称协程和非对称协程:

  • 对称协程:各协程之间可以进行执行权的切换。
  • 非对称协程:有一个中心化的调度器,所有协程都只和调度器进行执行权的切换。

已有的实现案例

有栈协程的实现方法

常用的方法有3种:

  • 使用 C 标准函数库的 setjmp 和 longjmp 函数
  • 使用POSIX标准中的 ucontext 库,但是这个库已被标记为 obsolete
  • 使用汇编直接对函数上下文进行操作

有栈协程的实现原理

协程原理的简单概括就是:函数可以根据需要让渡出 CPU 以供其他函数执行,而且函数可以在后续被唤起时,继续之前的状态执行。

下面一张图说明了协程的运行原理。
在这里插入图片描述

难点

协程的概念其实简单。但是当用汇编来实现,会有如下一些难题需要解决。

问题1,恢复后的协程应该在哪里继续执行?

在schedule中申请一个静态数组,静态数组是在栈上分配的,因此可以用来作为共有栈来使用。

typedef struct schedule {
    char stack[STACK_SIZE];		/* 当作共有栈来使用 */
    struct listhead head;
    struct context ctx;
}schedule_t;

问题2:怎么初始化协程的栈和上下文?

这个需要考虑函数调用栈和函数的调用约定。对于C语言的调用约定可以参考 一段程序说明C语言不同调用约定的区别。初始化一个协程,就相当于手动构建函数的调用栈。对于C语言的函数调用栈可以参考一幅图帮助理解C函数的调用栈

x86 的默认调用约定是 cdecl,函数调用汇编如下

caller:
        pushl   %ebp
        movl    %esp, %ebp
        pushl   para2
        pushl   para1
        call    callee

x86-64的函数调用汇编如下

caller:
        pushq   %rbp
        movq    %rsp, %rbp
        movl    para2, %esi
        movl    para1, %edi
        call    callee

因此仿照上面的函数调用,就可以初始化好协程的栈和上下文:

  1. 需要指定参数的位置,x86 默认的调用约定是 cdecl,参数是从右到左进行压栈;而x86-64前6个参数是通过寄存器传递。
  2. EIP (RIP, instruction pointer),即指令指针,该指针指向下一条即将执行的命令,此时应该指向代表协程执行主体的函数。
  3. 将 ESP (RSP)指向栈顶。
  4. 其他寄存器可以使用默认值。
  5. 注意,每个调用栈开始的EBP 压栈和 EBP 赋值主要用于函数的回退。这里考虑到执行到这一步的时候,协程已经执行完毕,可以将执行权交给schedule,而不用考虑协程的退出。

代码实现如下:
其中接口和 POSIX 函数 makecontext一致。

#if defined(__i386__)
void makectx(struct context *ctx, void(*fun)(struct coroutine*cor), void *para1)
{
    void *sp;
    void **para_addr;

    sp = (void *)(ctx->ss_sp + ctx->ss_size - sizeof(void *));
    /* align stack */
    sp = (char*)((unsigned long)sp & -16L);

    /* para in the stack */
    para_addr = (void**)(sp - sizeof(void *)*2);
    *para_addr = para1;

    ctx->regs[oEIP/psize] = fun;
    ctx->regs[oESP/psize] = (void*)(sp - sizeof(void *)*4);
    return;
}
#elif defined(__x86_64__)
void makectx(struct context *ctx, void(*fun)(struct coroutine*cor), void *para1)
{
    void *sp;

    sp = (void *)(ctx->ss_sp + ctx->ss_size - sizeof(void *));
    /* align stack */
    sp = (char*)((unsigned long)sp & -16L);

    ctx->regs[oRDI/psize] = para1;
    ctx->regs[oRIP/psize] = fun;
    ctx->regs[oRSP/psize] = sp;

    return;
}
#endif

问题3:怎么切换协程的上下文?

其实切换协程的上下文,原理非常简单,就是将就协程的栈和寄存器保存起来,然后替换为新协程的栈和寄存器。
也就是操作分为两步:

1. 保存栈

每个协程挂起时,申请一块动态空间用来保存栈的值。在恢复的时候,将动态空间的值复制到栈空间。

2. 保存寄存器

以 x86 为例,x86 寄存器有 EAX, EBX, ECX, EDX, EDI, ESI, EBP, ESP, EIP。在保存寄存器时,我们申请一个数组,用来存储寄存器的值。恢复时,将寄存器值一一恢复。

x86-64 的原理一致,只不过寄存器数目不一致。具体实现请参照项目中的代码。

以下为各个寄存器值在数组中的偏离量,单位为字节。

#define oEAX 0
#define oEBX 4
#define oECX 8
#define oEDX 12
#define oEDI 16
#define oESI 20
#define oEBP 24
#define oESP 28
#define oEIP 32

以下为切换寄存器的汇编代码,先将寄存器值保存到第一个参数的数组中,然后再将第二个参数数组中的值加载到寄存器上。

.globl swapctx
    .type swapctx, @function
swapctx:
    /* save registers to first parameter */
    movl 4(%esp), %eax     
    movl %ebx, oEBX(%eax)
    movl %ecx, oECX(%eax)
    movl %edx, oEDX(%eax)
    movl %edi, oEDI(%eax)
    movl %esi, oESI(%eax)
    movl %ebp, oEBP(%eax)
    movl %esp, oESP(%eax)
    
    /* save eip */
    movl (%esp), %ecx 
    movl %ecx, oEIP(%eax)

    /* setup registers from second parameter */
    movl  8(%esp), %eax 

    movl oESP(%eax), %esp
    /* setup eip */
    movl oEIP(%eax), %ecx
    movl %ecx, (%esp)  

    movl oEBX(%eax), %ebx
    movl oECX(%eax), %ecx
    movl oEDX(%eax), %edx
    movl oEDI(%eax), %edi 
    movl oESI(%eax), %esi
    movl oEBP(%eax), %ebp

    /* clear rax to indicate success */
    xorl %eax, %eax
    ret

参考:

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lylhw13_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值