簡單協程的實現
基本原理
之前的一篇短文簡單分析了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內核中任務切換的理解。該文的完整代碼可在筆者的下載區域獲取到。