协程解析二 | 云风的coroutine
概述
云风的coroutine
是通过ucontext
来控制程序运行时上下文的,我们来根据该库提供的几个接口,和一个demo来解释协程的运行原理。如果不了解ucontext的,建议先了解ucontxt
环境
- coroutine
- Ubuntu16.04
- gcc
- make
- vscode
下载代码 & 编译
$ git clone https://github.com/cloudwu/coroutine
$ cd coroutine && make
写一个生产者和消费者的demo
//procus.c
#include "coroutine.h"
#include <stdio.h>
#include <sys/types.h>
struct args {
int n;
};
void product(struct schedule *S, void *arg)
{
struct args* a = (struct args*)arg;
a->n = 1;
while (a->n < 5)
{
a->n++;
coroutine_yield(S); //flag 3
}
}
void consumer(struct schedule *S, int co, void *arg)
{
struct args* a = (struct args*)arg;
while (coroutine_status(S,co)) {
printf("get int %d\n", a->n);
coroutine_resume(S,co); //flag 2
}
printf("stop consumer\n");
}
int main() {
struct schedule * S = coroutine_open(); //flag 1
struct args arg;
arg.n = 1;
int co = coroutine_new(S, product, &arg);
printf("co: %d\n", co);
consumer(S, co, &arg);
coroutine_close(S);
return 0;
}
在Makefile中加入
procus : procus.c coroutine.c
gcc -g -Wall -o $@ $^
编译&运行
$ make procus
$ ./procus
co: 0
get int 1
get int 2
get int 3
get int 4
get int 5
stop consumer
在vscode中调试
按下 F5, 生成lunch.json
文件, 在文中加入下列行:
"program": "${workspaceFolder}/procus",
在 flag 1
flag 2
fllag 3
这三个地方打断点。按一下F5,可以看到运行过程是 flag 1
-> flag 2
-> fllag 3
运行步骤:
- coroutine_open: 打开调度器, 分配协程共享栈,
char stack[STACK_SIZE]
大小为 1M- coroutine_new: 创建一个协程,返回协程ID- coroutine_resume: 重新启动一个协程- coroutine_yield: 挂起一个协程 详细讲讲coroutine_resume
和coroutine_yield
。
coroutine_resume
1.coroutine_new
创建一个协程后,协程的状态是COROUTINE_READY
,调用coroutine_resume
,协程状态变为COROUTINE_RUNNING
。
getcontext(&C->ctx);
C->ctx.uc_stack.ss_sp = S->stack;
C->ctx.uc_stack.ss_size = STACK_SIZE;
C->ctx.uc_link = &S->main;
S->running = id;
C->status = COROUTINE_RUNNING;
C->ud = S->co[id]->ud;
uintptr_t ptr = (uintptr_t)S;
makecontext(&C->ctx, (void (*)(void)) mainfunc, 2, (uint32_t)ptr, (uint32_t)(ptr>>32));
swapcontext(&S->main, &C->ctx);
coroutine_resume
启动状态为COROUTINE_READY
的协程:
- 获取当前
context
- 协程栈顶指向内存S->stack
,栈大小为STACK_SIZE
-C->ctx.uc_link
保存当前context
结束后继续执行的context
记录- 修改协程运行状态为COROUTINE_RUNNING
- makecontext:设置函数指针mainfunc
和堆栈到对应context保存的sp和pc寄存器中- 保存当前context
到S->main
,切换context
到C->ctx
- 此时正在运行mainfunc
函数 ,现在的上下文就是在(S->stack ,S->stack + STACK_SIZE)
运行的C->ctx
,如果不懂这一步可以看协程解析一(ucontext解析) 查看mainfunc
函数
static void mainfunc(uint32_t low32, uint32_t hi32) {
....
C->func(S,C->ud);
_co_delete(C);
S->co[id] = NULL;
--S->nco;
S->running = -1;
}
mainfunc
函数运行的是C->func
,运行完C->func
之后就把该协程的运行栈的内存空间给释放掉了。- 运行完mainfunc
函数后,context切换到S->main
。 2.调用coroutine_resume
,协程状态变为COROUTINE_RUNNING
, 运行mainfunc
函数,运行到C->func
, 实际上是在运行product
函数,product
函数调用了coroutine_yield
,此时协程状态变为COROUTINE_SUSPEND
。保存此时协程的context
->C-ctx
,切换协程上下文为S->main
。此时运行时所在函数coroutine_resume
标记1的位置。
void coroutine_resume(struct schedule * S, int id) {
...
switch(status) {
case COROUTINE_READY:
...
swapcontext(&S->main, &C->ctx); //标记1
break;
case COROUTINE_SUSPEND:
memcpy(S->stack + STACK_SIZE - C->size, C->stack, C->size);
S->running = id;
C->status = COROUTINE_RUNNING;
swapcontext(&S->main, &C->ctx); //标记2
break;
default:
assert(0);
}
}
3.此时相当于是回到了customer
函数,此时协程状态为COROUTINE_SUSPEND
, 继续循环调用coroutine_resume
。
- 拷贝协程私有运行栈
C->stack
到共享栈S->stack
,大小为C->size
- 设置此时正在运行的协程的协程ID,此时协程的状态修改为COROUTINE_RUNNING
- 保存此时context
到S->main
,切换context
到C->ctx
。C->ctx
上运行的函数是mainfunc
,也就是product
函数。
coroutine_yield
C->func
函数指针指向的是product
函数,product
函数中调用了coroutine_yield
,有以下代码:
static void _save_stack(struct coroutine *C, char *top) {
char dummy = 0;
assert(top - &dummy <= STACK_SIZE);
if (C->cap < top - &dummy) {
free(C->stack);
C->cap = top-&dummy;
C->stack = malloc(C->cap);
}
C->size = top - &dummy;
memcpy(C->stack, &dummy, C->size);
}
void coroutine_yield(struct schedule * S) {
...
_save_stack(C,S->stack + STACK_SIZE);
C->status = COROUTINE_SUSPEND;
S->running = -1;
swapcontext(&C->ctx , &S->main);
}
主要步骤
- 调用
_save_stack
函数,把协程指针和运行时栈底地址作为参数 char dummy = 0;
声明一个变量,然后取地址,这个地址就是此时栈顶的地址。 要理解这段代码,要理解的几个点:uncontext
的使用,C->ctx.uc_stack.ss_sp = S->stack;
,C->ctx.uc_stack.ss_size = STACK_SIZE;
这两段代码分配了此时协程可以使用的空间大小。 S->stack+STAACK_SIZE是栈底,S->stack是栈顶。- 栈有先入后出的特性
- 栈的地址是从高到地分配的。
char dummy = 0;
是一个分配在栈空间上的数据。此时 变量dummy的地址&dummy
是协程的栈空间的栈顶地址。栈底地址-栈顶地址=top-&dummy
,表示的是该协程所占用的空间大小。
memcpy(C->stack, &dummy, C->size)
, 保存栈的数据到C->stack -> C->stack + C->size
- 修改协程的状态为
COROUTINE_SUSPEND
, 设置此时运行的协程id
为-1
,挂起协程; - 保存当前的
context
到C->ctx
, 切换当前的context
为S->main
。此时回到了标记2
,见上文。这样就形成了闭环,直到mainfunc
运行完毕,协程退出。
至此,coroutine的整体流程就说完了。
--完--
- 原文作者: 留白
- 原文链接: https://zfunnily.github.io/2021/03/coroutinetwo/
- 更新时间:2024-04-16 01:01:05
- 本文声明:转载请标记原文作者及链接