概述

云风的coroutine是通过ucontext来控制程序运行时上下文的,我们来根据该库提供的几个接口,和一个demo来解释协程的运行原理。如果不了解ucontext的,建议先了解ucontxt

环境

下载代码 & 编译

$ 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_resumecoroutine_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寄存器中- 保存当前contextS->main,切换contextC->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- 保存此时contextS->main,切换contextC->ctxC->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; 声明一个变量,然后取地址,这个地址就是此时栈顶的地址。 要理解这段代码,要理解的几个点:
    1. uncontext的使用,C->ctx.uc_stack.ss_sp = S->stack;C->ctx.uc_stack.ss_size = STACK_SIZE;这两段代码分配了此时协程可以使用的空间大小。 S->stack+STAACK_SIZE是栈底,S->stack是栈顶。
    2. 栈有先入后出的特性
    3. 栈的地址是从高到地分配的。
    4. char dummy = 0; 是一个分配在栈空间上的数据。此时 变量dummy的地址&dummy是协程的栈空间的栈顶地址。栈底地址-栈顶地址=top-&dummy,表示的是该协程所占用的空间大小。
  • memcpy(C->stack, &dummy, C->size), 保存栈的数据到 C->stack -> C->stack + C->size
  • 修改协程的状态为COROUTINE_SUSPEND, 设置此时运行的协程id-1,挂起协程;
  • 保存当前的contextC->ctx, 切换当前的contextS->main。此时回到了 标记2,见上文。这样就形成了闭环,直到mainfunc运行完毕,协程退出。

至此,coroutine的整体流程就说完了。

--完--