简单协程的实现
基本原理
之前的一篇短文简单分析了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内核中任务切换的理解。该文的完整代码可在笔者的下载区域获取到。