簡單協程的實現

簡單協程的實現

基本原理

之前的一篇短文簡單分析了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萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章