协程解析二 | 云风的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
 - 本文声明:转载请标记原文作者及链接