协程(coroutine)与subroutine同样作为程序执行单元的抽象被一些语言当作基 础实现,两者的抽象方式大致区别在于:
- 多个执行单元之间的关系:
对于subroutine来说,存在一个调用与被调用的关系,比如在a-subroutine里调用 b-subroutine, 那么a-subroutine就是调用者,b-subroutine是被调用者,它们共 享一个线程的堆栈.
而多个coroutine之间的关系是对等的,即便在a-coroutine里创建了 b-coroutine,他们之间也不会有任何层级关系.
subroutine作为一种通用的抽象比较容易实现,而要实现coroutine至少需要两个 条件:
-
要有一个全局的调度器并且每个coroutine得有一个堆栈空间.
-
调度器用来在多个对等的coroutine之间做切换操作,每个coroutine的堆栈 用于存储各自的上下文内容.
- 执行单元的入口与出口:
在一个典型的subroutine实现里,执行单元的入口和出口只能有一个, 这是共享 调用栈带来的局限性, 比如(x86-64平台)我们在a-subroutine里调用 b-subroutine,那么会先把前6个参数依次写入寄存器:rdi,rsi,rdx,rcx,r8,r9,6 个以上的参数从右至左压栈,rsp不断上移指向栈顶,然后把b-subroutine调用之 后的那条指令地址压栈,rsp上移,然后rbp压栈,把rsp指向rbp的栈地址,最后把 b-subroutine里的局部变量依次压栈,rsp继续上移.这就是调用时的入口过程, 当执行是b-subroutine时,就得依次出栈,然后退出,执行a-subroutine里的下一 条指令,这是出口过程.很明显,这里b-subroutine只有一个入口和一个出口,因为 必须要等b-subroutine执行完成后(出栈)才能继续执行a-subroutine.
而一个典型的coroutine实现,执行单元可以有多个入口和出口, 因为栈不共享, 一个通用的实现模式是用堆来表示coroutine的调用栈, 当我们在a-coroutine里 创建b-coroutine, 在堆上分配一块空间用于表示b-coroutine的堆栈, 在执行 b-coroutine时,我们可以在任意点把当前的执行信息写回这块在堆上分配的空 间,然后把rsp指向a-coroutine的栈顶,rbp指向a-coroutine的栈frame,rip指向 a-coroutine的需要继续执行的指令的地址, 这也就是所谓的用户态的上下文切 换,当下一次需要继续执行b-coroutine的时候,保存当前coroutine的上下文, 恢复b-coroutine的上下文就行了.
上面描述的上下文切换是在用户态进行的,unix-like的环境下, glibc库通常都 会有一个描述上下文结构的定义ucontext_t在ucontext.h文件里,并有四个函 数:getcontext,setcontext,makecontext,swapcontext分别用于在用户态保存上 下文,恢复上下文,创建上下文和保存且恢复上下文,使用它们可以实现一个基本 的协程系统, 比如libtask就是一个运行在unix平台上的基础协程库, 能在用户 态实现多个执行流轮换执行,libtask对glibc的 getcontext,setcontext,makecontext,swapcontext做了简单的封装,因为这四个 函数是libtask实现的基础,所以要研究libtask前,先得了解这4个函数的作用与 实现机制.
这里有一个关键的数据结构,即user level context:
typedef struct ucontext
{
unsigned long int uc_flags;
// 另一个执行流的上下文地址,在x86x64平台下也就是rbx寄存器里的内容
struct ucontext *uc_link;
//用于此上下文结构的堆栈,存储在堆区
stack_t uc_stack;
// mcontext_t结构体用于存储完整的进程状态信息
mcontext_t uc_mcontext;
// 需要block的信号掩码
__sigset_t uc_sigmask;
// fpu寄存器结构
struct _libc_fpstate __fpregs_mem;
} ucontext_t;
ucontext结构体里的uc_stack是在堆上分配的,并做为堆栈用于此上下文, makecontext函数将会设置uc_stack与相应寄存器里的值uc_stack的结构大致 是这样的:
---------------------------------------
| 下一个上下文的地址 |
---------------------------------------
| 参数 7-n(假如回调函数的参数大于7个) |
---------------------------------------
| 返回地址 | %rsp -> ---------------------------------------
另外寄存器里的内容:
%rdi,%rsi,%rdx,%rcx,%r8,%r9: 分别存储参数1-6
%rbx : 下一个上下文的地址
%rsp : 指向栈顶
当我