协程切换原理
使用glibc中<ucontext.h>提供的相关函数
用户态切换简单来说就是保存当前上下文,切换到新的上下文.
用户态程序的上下文一般包含如下信息:
- 栈
- 各种寄存器
- 信号掩码: linux信号掩码是基于线程的,协程也需要支持单独设置信号掩码信息
我们来看一下glibc定义的用户态上下文结构ucontext_t:
1 | typedef struct ucontext_t |
通过上述结构定义,我们也可以看出,用户态上下文主要就是寄存器和栈,另外还有信号掩码信息.
ucontext相关api实现
由于ucontext api使用汇编代码实现,因此我们先来学习一些汇编基础知识.
- x64上使用rdi, rsi, rdx, rcx, r9, r10传递参数,如果参数大于6个则使用栈
- leaq指令用于取地址,类似于c中的&
另外为了理解如何保存当前栈和指令寄存器,我们要熟悉一下x64上函数调用的相关知识:
- 当上一个函数使用call指令调用当前函数时,会将上一个函数的返回地址
prev rip
压入栈中,这样当被调用函数调用ret
指令返回时就会从栈中pop
出这个地址进行返回
- 当上一个函数使用call指令调用当前函数时,会将上一个函数的返回地址
- 当前函数执行时,会将上一级函数的
rbp
压入栈中,用于函数返回时还原,然后将rbp
设置为当前的栈底,再调用rsp
开辟当前函数的栈.
- 当前函数执行时,会将上一级函数的
- 现在我们考虑在当前函数中调用getcontext会发生什么, 通过call调用getcontext后,当前函数的返回地址
current rip
被压入栈中:
- 现在我们考虑在当前函数中调用getcontext会发生什么, 通过call调用getcontext后,当前函数的返回地址
1. 保存当前上下文
getcontext能够将当前的上下文信息保存起来,用于后面还原.我们来看下具体实现:
函数原型
1 | int getcontext(ucontext_t *ucp); |
函数详解
1 | ENTRY(__getcontext) |
2. 设置上下文:
setcontext函数能够还原之前的ucontext_t中的状态.
函数原型:
1 | int setcontext(const ucontext_t *ucp); |
实现详解:
1 | ENTRY(__setcontext) |
下面的图示详细展示了执行setcontext后的栈布局:
一个例子
下面我们通过getcontext, setcontext来实现一个示例直观理解一下:
1 |
|
执行上面代码会看到反复打印”hello the world”
1 | 安哥6@ubuntu:~$ ./a.out |
上面的两个函数只实现了简单的保存当前上下文和设置上下文的功能,要实现更复杂的协程切换,我们需要灵活地创建上下文和在两个上下文之间切换,因此makecontext
, swapcontext
就派上用场了:
3. makecontext
makecontext能够让我们设置栈的位置,要执行的函数即要传递的参数,这样就具备了创建协程运行环境的功能.
函数原型
1 | void makecontext(ucontext_t *ucp, void (*func)(), int argc, ...); |
- ucp: 上下文结构
- func: 关联的函数
- argc: 关联的函数的参数个数
- …: 关联的参数
函数详解
uc_link解释: 当我们创建的ucontext_t中的函数执行结束后,应该切换到哪里去?为了能够指明这个信息,ucontext_t中有一个uc_link指针,它指向另外一个ucontext_t结构,这就是uc_link的作用.
跳板代码: (__start_context函数)
由跳板代码完成uc_link的加载和切换,这样ucontext_t结束时就能切换到uc_link.
跳板代码放在ucontext_t函数栈的最顶端,这样ucontext_t结束时就能通过ret
弹出并执行了.
1 | __makecontext (ucontext_t *ucp, void (*func) (void), int argc, ...) |
4. swapcontext
将当前上下文保存并切换到另一个上下文中
函数原型
1 | int swapcontext(ucontext_t *oucp, const ucontext_t *ucp); |
详细实现
swapcontext的前半部分和getcontext类似保存当前上下文,后半部分和setcontext类似,因此只分析关键部分
1 | /* Load the new stack pointer and the preserved registers. */ |
swapcontext后的栈布局
最后我们看一下当切换后的ucontext_t执行完后如何通过跳板代码执行到uc_link.
跳板代码实现
1 | ENTRY(__start_context) |