Coroutine-Switching
Posted 2017-04-26 16:23 +0800 by ZhangJie ‐ 3 min read
分享:
1. 协程Coroutine
本文提供了一个模拟协程上下文切换过程的测试程序,基本思路是每当希望创建一个coroutine时,就在堆里申请一块内存,然后对内存进行整理,在其中保存预设的硬件上下文等信息(重点设置rbp、rsp、rip(callback)、rdi(callback-arg))。启动协程并进行切换时,通过__SwitchCoroutine__(cur,next)来完成cur向next的切换,切换过程中保存当前上下文信息到cur对应的堆内存中,并提取next对应的堆内存信息还原上下文完成切换。 测试程序中创建了4个coroutine,第1个coroutine只是为了用来启动其他3个,其他3个没有继续调度第一个,原因是第一个coroutine中rip保存的是main.c中__SwitchCoroutine__之后的指令地址,如果让第一个coroutine参与context-switch的话会使得进程结束执行。
在看下面的程序之前,需要先了解栈帧的构成、栈帧创建之前caller的动作&创建之后callee的动作、函数返回之前callee的动作&函数返回之后caller的动作,之前看过一篇整理的很不错的文章以供参考C Function Call Conventions and the Stack。
ps: 实际上直接使用linux下的ucontext_t实现会更简单,在我的介绍libmill的电子书中有详细介绍。
1.1. 协程coroutine声明
file: coroutine.h
#include <stdint.h>
typedef int64_t (*EntryCallback)(void*);
//硬件上下文信息
struct stRegister
{
uint64_t rax;
uint64_t rbx;
uint64_t rcx;
uint64_t rdx;
uint64_t rsi;
uint64_t rdi;
uint64_t r8;
uint64_t r9;
uint64_t r10;
uint64_t r11;
uint64_t r12;
uint64_t r13;
uint64_t r14;
uint64_t r15;
uint64_t rbp;
uint64_t rsp;
uint64_t rip;
};
//协程上下文
struct stContext
{
struct stRegister cpu_register;
void *arg;
uint8_t *stack;
};
typedef struct stContext Coroutine;
//创建协程
Coroutine* CreateCoroutine(EntryCallback entry, void *arg);
//删除协程
void DeleteCoroutine(Coroutine *ptr);
//设置协程栈尺寸
void SetStackSize(uint32_t size);
//协程切换
void __SwitchCoroutine__(Coroutine *cur, const Coroutine *next);
1.2. 协程Coroutine实现
file: coroutine.c
#include "coroutine.h"
#include <stdlib.h>
#define OFFSET(t, m) (&(((t*)0)->m))
uint32_t g_stack_size = 100 * 1024;
Coroutine* CreateCoroutine(EntryCallback entry, void *arg)
{
int size = g_stack_size + sizeof(Coroutine);
Coroutine *c = (Coroutine *)calloc(size, 1);
if (NULL == c)
{
return NULL;
}
uint8_t *start = (uint8_t*)c;
c->arg = arg;
//函数入口
c->cpu_register.rip = (uint64_t)entry;
//第一个参数
c->cpu_register.rdi = (uint64_t)arg;
//rbp 栈底
c->cpu_register.rbp = (uint64_t)(start + size);
//rsp 当前栈顶
c->cpu_register.rsp = c->cpu_register.rbp;
return c;
}
void DeleteCoroutine(Coroutine *ptr)
{
free(ptr);
}
void SetStackSize(uint32_t size)
{
g_stack_size = size;
}
2. 协程Coroutine上下文切换
file: switch.s
//这里协程库是基于有栈协程的设计来实现,协程硬件上下文信息需通过%rsp来计算访问地址
//__SwitchCoroutine__(current_coroutine, next_coroutine)
//- rdi, current_coroutine
//- rsi, next_coroutine
.globl __SwitchCoroutine__
__SwitchCoroutine__:
//save rsp of calling function, here %rsp equals to return address
mov %rsp, %rax
//set rsp to end of coroutine.stRegister, to push rip,
//when rdi coroutine return, it will return the rip to continue exec
mov %rdi, %rsp
add $136, %rsp
push (%rax)
//+8 to skip return address to get end address of calling function's %rsp
add $8, %rax
push %rax
//store the current_coroutine's state(stRegister)
push %rbp
push %r15
push %r14
push %r13
push %r12
push %r11
push %r10
push %r9
push %r8
push %rdi
push %rsi
push %rdx
push %rcx
push %rbx
push %rax
//ready switch to next_coroutine
mov %rsi, %rsp
//restore the next_coroutine's stRegister to cpu
pop %rax
pop %rbx
pop %rcx
pop %rdx
pop %rsi
pop %rdi
pop %r8
pop %r9
pop %r10
pop %r11
pop %r12
pop %r13
pop %r14
pop %r15
pop %rbp
//move return address to %rax
mov 8(%rsp), %rax
pop %rsp
//jmp to next_coroutine, ram indirect access to fetch the target address
jmp *%rax
3. Coroutine使用 & 测试
3.1. 测试程序
file: main.c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "coroutine.h"
Coroutine *coroutines[3];
int64_t callback(void *arg)
{
while(1) {
if(strcmp((char *)arg, "coroutine-a")==0) {
printf("[%s] ready to switch to coroutine-b\n", (char *)arg);
__SwitchCoroutine__(coroutines[0], coroutines[1]);
}
else if(strcmp((char *)arg, "coroutine-b")==0) {
printf("[%s] ready to switch to coroutine-c\n", (char *)arg);
__SwitchCoroutine__(coroutines[1], coroutines[2]);
}
else if(strcmp((char *)arg, "coroutine-c")==0) {
printf("[%s] ready to switch to coroutine-a\n", (char *)arg);
__SwitchCoroutine__(coroutines[2], coroutines[0]);
}
sleep(1);
}
return 0;
}
int main()
{
printf("initialize coroutine's callback\n");
EntryCallback cb = callback;
printf("create 3 coroutines\n");
Coroutine *coo = CreateCoroutine(cb, (void *)"coroutine-o");
Coroutine *coa = CreateCoroutine(cb, (void *)"coroutine-a");
Coroutine *cob = CreateCoroutine(cb, (void *)"coroutine-b");
Coroutine *coc = CreateCoroutine(cb, (void *)"coroutine-c");
coroutines[0] = coa;
coroutines[1] = cob;
coroutines[2] = coc;
printf("ready to start coroutine switching\n");
__SwitchCoroutine__(coo, coa);
printf("ready to exit\n");
return 0;
}
3.2. 测试程序build
file: Makefile
all: *.c *.h *.s
@echo "==> build the coroutine test module"
gcc -g -o main *.c *.h *.s
@echo "==> build successful"
test: all
@echo "==> run the coroutine test module"
./main
clean:
@echo "==> delete the build file 'main'"
rm main
3.3. 测试结果
make
make test
==> build the coroutine test module
gcc -g -o main *.c *.h *.s
==> build successful
==> run the coroutine test module
./main
initialize coroutine's callback
create 3 coroutines
ready to start coroutine switching
[coroutine-a] ready to switch to coroutine-b
[coroutine-b] ready to switch to coroutine-c
[coroutine-c] ready to switch to coroutine-a
[coroutine-a] ready to switch to coroutine-b
[coroutine-b] ready to switch to coroutine-c
[coroutine-c] ready to switch to coroutine-a
[coroutine-a] ready to switch to coroutine-b