简单协程的实现

简单协程的实现

基本原理

之前的一篇短文简单分析了Linux内核中任务切换的实现机制,其精巧的方法让人叹为观止:Linux内核源码诚然是世界范围内的IT精英的杰作,开源项目的典范。通过qemu虚拟机及gdb调试工具,对任务切换的功能可以有较为深刻的理解,不过我想可以更进一步,将内核的任务切换移植到应用层,这样也就是协程实现的简单实现了。

简单协程的实现

协程的结构体定义

struct co_thread {
    /* registers */
    struct co_context      ctx;
    struct fp_context      fpctx;
​
    /* coroutine stack */
    unsigned long          stk_end;
    unsigned long          stk_top;
    unsigned int           stk_size;
    unsigned int           co_state;
​
    struct co_thread *     master;
​
    /* for slave coroutine */
    void *                 co_arg;
    co_thread_func         co_func;
    int                    co_id;
​
    /* for input & output */
    int                    co_loop;
    void *                 inout;
    unsigned int           iolen;
    struct co_thread *     co_list[0];
};

如上图,C语言代码运行时需要一个上下文环境,即上面代码段的co_context;除此之外,还需要栈空间,即stk_end/stk_top。其中co_context与Linux内核中定义的cpu_context完全相同:

typedef unsigned long coreg_t;
struct co_context_arm64 {
    coreg_t                x19;
    coreg_t                x20;
    coreg_t                x21;
    coreg_t                x22;
    coreg_t                x23;
    coreg_t                x24;
    coreg_t                x25;
    coreg_t                x26;
    coreg_t                x27;
    coreg_t                x28;
    coreg_t                x29;
    coreg_t                sp;
    coreg_t                pc;
};

协程的任务切换实现

extern void _co_fp_store(void *); 
extern void _co_fp_restore(void *); 
extern struct co_thread * _co_switch(void *, void *); 
static struct co_thread * co_switch(struct co_thread * prev, struct co_thread * next)
{
    struct co_thread * rval;
​
    if (__builtin_expect(prev == next, 0)) {
        fputs("Fatal Error, coroutine cannot switch to itself!\n", stderr);
        fflush(stderr);
        return NULL;
    }   
​
    _co_fp_store(&(prev->fpctx));
    _co_fp_restore(&(next->fpctx));
    if ((prev->co_state & COTHREAD_STATE_DEAD) == 0)
        prev->co_state = COTHREAD_STATE_SUSPEND;
    next->co_state = COTHREAD_STATE_RUNNING;
    rval = _co_switch(prev, next);
    return rval;
}

见上面的代码段,实现了简单协程的任务切换。主要是引用了三个汇编函数:调用co_fp_store存储当前的浮点运算的上下文,并调用co_fp_restore恢复新任务的浮点运算的上下文,最后调用了_co_switch汇编函数进行任务切换。该汇编函数是参照内核源码中的cpu_switch_to,稍加修改而成的:

co_switch:
    mov x8, x0
    mov x9, sp
    stp x19, x20, [x8], #16     // store callee-saved registers
    stp x21, x22, [x8], #16
    stp x23, x24, [x8], #16
    stp x25, x26, [x8], #16
    stp x27, x28, [x8], #16
    stp x29, x9, [x8], #16
    str x30, [x8]          
    mov x8, x1             
    ldp x19, x20, [x8], #16     // restore callee-saved registers
    ldp x21, x22, [x8], #16
    ldp x23, x24, [x8], #16
    ldp x25, x26, [x8], #16
    ldp x27, x28, [x8], #16
    ldp x29, x9, [x8], #16 
    ldr x30, [x8]          
    mov sp, x9             
    mov x0, x1      
    ret

测试简单协程

编写简单的测试C文件,代码很短,如下图:

#include "coroutine.h"
    
static int test_func(struct co_thread * co, void * what)
{
    int idx; 
    
    fprintf(stdout, "Coroutine %d running, what: %p\n", co->co_id, what);
    for (idx = 0; idx < 5; ++idx) {
        fprintf(stdout, "In [%s], index: %d, errno = %d\n", __FUNCTION__, idx, errno);
        fflush(stdout); 
        co_thread_yield(co, NULL, 0, NULL);
        if (co->co_loop == 0)
            break;
    }
    return 0;
}   
    
int main(int argc, char *argv[])
{
    int idx;
    void * rval;
    struct co_thread * boss, * slave;
    
    boss = co_thread_master();
    if (boss == NULL)
        return 1;
    
    slave = co_thread_create(boss, 1, test_func,
        (void *) 0x2020ul, COROUTINE_STACK_SIZE);
    if (slave == NULL) {
        co_thread_free(boss, 1);
        return 2;
    }
    
    idx = 0; 
    do {
        fprintf(stdout, "In main cothread, index: %d\n", idx++);
        fflush(stdout); errno = idx;
        rval = co_thread_resume(slave, NULL, 0, NULL);
        if (coroutine_dead(slave))
            break;
    } while (rval != COROUTINE_ERROR);
​
    co_thread_destory(slave);
    co_thread_free(boss, 1);
    return 0;
}

协程仍属于同一进程的同一个线程,我们通过读写线程相关的全局变量errno来验证。接下来编译并运行此协程测试,其结果如下:

yejq@UNIX:~/program/coroutine$ make
aarch64-linux-gnu-gcc -Wall -O2 -fPIC -D_GNU_SOURCE -I. -c -o coroutine.o coroutine.c
aarch64-linux-gnu-gcc -Wall -O2 -fPIC -D_GNU_SOURCE -I. -c -o main.o main.c
aarch64-linux-gnu-gcc -Wall -O2 -fPIC -D_GNU_SOURCE -I. -c -o co_switch.o co_switch.S
aarch64-linux-gnu-gcc -o cor coroutine.o main.o co_switch.o
yejq@UNIX:~/program/coroutine$ ls
cor  coroutine.c  coroutine.h  coroutine.o  co_switch.o  co_switch.S  main.c  main.o  Makefile
yejq@UNIX:~/program/coroutine$ file cor
cor: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 3.7.0, BuildID[sha1]=5a1cd3636c738e6ac3dead55ad85b6a3c0972710, with debug_info, not stripped
yejq@UNIX:~/program/coroutine$ QEMU_LD_PREFIX=/opt/gcc-linaro-7.4.1-2019.02-x86_64_aarch64-linux-gnu/aarch64-linux-gnu/libc \
> qemu-aarch64 ./cor
In main cothread, index: 0
Coroutine 1 running, what: 0x2020
In [test_func], index: 0, errno = 1
In main cothread, index: 1
In [test_func], index: 1, errno = 2
In main cothread, index: 2
In [test_func], index: 2, errno = 3
In main cothread, index: 3
In [test_func], index: 3, errno = 4
In main cothread, index: 4
In [test_func], index: 4, errno = 5
In main cothread, index: 5

为了测试方便,我们使用qemu虚拟机直接运行该AArch64平台的二进制应用。实测在ARM 64位嵌入式设备也同样正常运行,输出结果相同。main函数与test_func交替运行,恰似线程。此外,可见主协程修改了全局变量errno,test_func协程能够读取到,说明二者同属一个线程。

至此,我们在ARM 64位平台就实现了简单的协程,这样也就更进一步加深了对Linux内核中任务切换的理解。该文的完整代码可在笔者的下载区域获取到。

发布了17 篇原创文章 · 获赞 9 · 访问量 1万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章