企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
[TOC] ## 概述 云风的`coroutine`是通过`ucontext`来控制程序运行时上下文的,我们来根据该库提供的几个接口,和一个demo来解释协程的运行原理。如果不了解ucontext的,建议先了解`ucontxt` ## 环境 * [coroutine](https://github.com/cloudwu/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解析)](%E5%8D%8F%E7%A8%8B%E8%A7%A3%E6%9E%90%E4%B8%80(ucontext%E8%A7%A3%E6%9E%90).md) 查看`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`函数,把协程指针和运行时栈底地址作为参数 1. `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`,表示的是该协程所占用的空间大小。 2.`memcpy(C->stack, &dummy, C->size)`, 保存栈的数据到 `C->stack -> C->stack + C->size` * 修改协程的状态为`COROUTINE_SUSPEND`, 设置此时运行的协程`id`为`-1`,挂起协程; * 保存当前的`context`到`C->ctx`, 切换当前的`context`为`S->main`。此时回到了 `标记2`,见上文。这样就形成了闭环,直到`mainfunc`运行完毕,协程退出。