协程
- 协程:编程语言层面的概念,又叫做用户态线程,由用户进行切换,因为不用陷入内核态,所以切换效率更高。
- 协程的分类:
- 非对称式协程:协程主动挂起时,只能返回最初调用它的协程继续执行。在非对称协程中,存在着调用与被调用的关系。比如腾讯的libco
- 对称协程:运行的协程和挂起的协程之间没有关系。
与协程相关的四大系统函数
在Linux上,与协程相关的四大函数分别是:getcontext、setcontext、makecontext、swapcontext
1.信号栈
- 信号栈定义如下所示:
/* Structure describing a signal stack. */
typedef struct
{
void *ss_sp;
int ss_flags;
size_t ss_size;
} stack_t;
2.上下文
- 与平台相关的上下文数据结构如下所示:x86_x64架构
typedef long long int greg_t;
#define __NGREG 23
/* Container for all general registers. */
typedef greg_t gregset_t[__NGREG];
/* Structure to describe FPU registers. */
typedef struct _libc_fpstate *fpregset_t;
/* Context to describe whole processor state. */
typedef struct
{
// long long int[23],保存23个寄存器的信息
gregset_t __ctx(gregs);
/* Note that fpregs is a pointer. */
fpregset_t __ctx(fpregs);
__extension__ unsigned long long __reserved1 [8];
} mcontext_t;
3.函数介绍
以下四个函数都定义在ucontext.h头文件中
- getcontext:get the user context,将上下文信息保存在传入的实参中,主要做的事情是将相关的寄存器值保存。保存现场
int getcontext(ucontext_t *ucp);
// Userlevel context
typedef struct ucontext_t {
struct ucontext_t *uc_link; // 指向将要恢复的上下文(当前上下文执行结束后需要执行的上下文)
sigset_t uc_sigmask;// 当前上下文阻塞(屏蔽)的信号集
stack_t uc_stack; // 当前上下文执行需要使用的信号栈
mcontext_t uc_mcontext; // 保存的上下文信息,包括寄存器(比如说pc值)等
struct _libc_fpstate __fpregs_mem; // FPU寄存器信息
__extension__ unsigned long long int __ssp[4];
} ucontext_t;
函数形参:
ucp:传出参数,保存上下文信息
函数返回值:
成功则返回0失败返回-1
- setcontext:set the user context,这个函数根据传入的实参恢复上下文。恢复现场
int setcontext(const ucontext_t *ucp);
- makecontext:manipulate user context,修改第一参数指向的上下文信息,这个上下文信息可以由getcontext函数获得。
void makecontext(ucontext_t *ucp, void (*func)(), int argc, ...);
函数形参:
argc:传递给func的参数的个数
- swapcontext:manipulate user context,保存第一参数指向的上下文信息,恢复第二参数指向的上下文信息。恢复现场
int swapcontext(ucontext_t *oucp, const ucontext_t *ucp);
4.四大函数的基本使用
- 示例如下:来自官方文档
#include <ucontext.h>
#include <stdio.h>
#include <stdlib.h>
static ucontext_t uctx_main, uctx_func1, uctx_func2;
#define handle_error(msg) \
do { perror(msg); exit(EXIT_FAILURE); } while (0)
static void
func1(void)
{
printf("func1: started\n");
printf("func1: swapcontext(&uctx_func1, &uctx_func2)\n");
// 切换至上下文uctx_func2
if (swapcontext(&uctx_func1, &uctx_func2) == -1)
handle_error("swapcontext");
printf("func1: returning\n");
}
static void
func2(void)
{
printf("func2: started\n");
printf("func2: swapcontext(&uctx_func2, &uctx_func1)\n");
// 切换至上下文uctx_func1
if (swapcontext(&uctx_func2, &uctx_func1) == -1)
handle_error("swapcontext");
printf("func2: returning\n");
}
int
main(int argc, char *argv[])
{
char func1_stack[16384];
char func2_stack[16384];
if (getcontext(&uctx_func1) == -1)
handle_error("getcontext");
uctx_func1.uc_stack.ss_sp = func1_stack;
uctx_func1.uc_stack.ss_size = sizeof(func1_stack);
uctx_func1.uc_link = &uctx_main;
makecontext(&uctx_func1, func1, 0);
if (getcontext(&uctx_func2) == -1)
handle_error("getcontext");
uctx_func2.uc_stack.ss_sp = func2_stack;
uctx_func2.uc_stack.ss_size = sizeof(func2_stack);
/* Successor context is f1(), unless argc > 1 */
uctx_func2.uc_link = (argc > 1) ? NULL : &uctx_func1;
makecontext(&uctx_func2, func2, 0);
printf("main: swapcontext(&uctx_main, &uctx_func2)\n");
// 切换至上下文uctx_func2
if (swapcontext(&uctx_main, &uctx_func2) == -1)
handle_error("swapcontext");
printf("main: exiting\n");
exit(EXIT_SUCCESS);
}
//main: swapcontext(&uctx_main, &uctx_func2)
//func2: started
//func2: swapcontext(&uctx_func2, &uctx_func1)
//func1: started
//func1: swapcontext(&uctx_func1, &uctx_func2)
//func2: returning
//func1: returning
//main: exiting
5.原理剖析
- getcontext函数:参见
// 宏定义中两个##表示将两个token连接成为一个token
#define ORBP offsetof(ucontext_t,uc_mcontext.gregs[REG_##RBP])
// gregs类型为long long int[23]
// 每个寄存器名称对应gregs数组的一个下标
enum
{
REG_R8 = 0,
REG_R9,
REG_R10,
REG_R11,
REG_R12,
REG_R13,
REG_R14,
REG_R15,
REG_RDI,
REG_RSI,
REG_RBP,
REG_RBX,
REG_RDX,
REG_RAX,
REG_RCX,
REG_RSP,
REG_RIP,
REG_EFL,
REG_CSGSFS, /* Actually short cs, gs, fs, __pad0. */
REG_ERR,
REG_TRAPNO,
REG_OLDMASK,
REG_CR2
};
// rdi寄存器一般存放的是函数的第一个参数,因此在调用getcontext函数的时候,
// rdi寄存器存放的是调用者传入的实参地址,这个是实参用于保存上下文信息
ENTRY(__getcontext)
/* Save the preserved registers, the registers used for passing
args, and the return address. */
movq %rbx, oRBX(%rdi) // ====>等同于movq %rbx, n(%rdi) ===> movq %rbx, ucp->uc_mcontext.gregs[n]
movq %rbp, oRBP(%rdi)
movq %r12, oR12(%rdi)
movq %r13, oR13(%rdi)
movq %r14, oR14(%rdi)
movq %r15, oR15(%rdi)
movq %rdi, oRDI(%rdi)
movq %rsi, oRSI(%rdi)
movq %rdx, oRDX(%rdi)
movq %rcx, oRCX(%rdi)
movq %r8, oR8(%rdi)
movq %r9, oR9(%rdi)
movq (%rsp), %rcx
movq %rcx, oRIP(%rdi)
leaq 8(%rsp), %rcx /* Exclude the return address. */
movq %rcx, oRSP(%rdi)
#if SHSTK_ENABLED
/* Check if shadow stack is enabled. */
testl $X86_FEATURE_1_SHSTK, %fs:FEATURE_1_OFFSET
jz L(no_shstk)
/* Save RDI in RDX which won't be clobbered by syscall. */
movq %rdi, %rdx
xorl %eax, %eax
cmpq %fs:SSP_BASE_OFFSET, %rax
jnz L(shadow_stack_bound_recorded)
/* Get the base address and size of the default shadow stack
which must be the current shadow stack since nothing has
been recorded yet. */
sub $24, %RSP_LP
mov %RSP_LP, %RSI_LP
movl $ARCH_CET_STATUS, %edi
movl $__NR_arch_prctl, %eax
syscall
testq %rax, %rax
jz L(continue_no_err)
/* This should never happen. */
hlt
L(continue_no_err):
/* Record the base of the current shadow stack. */
movq 8(%rsp), %rax
movq %rax, %fs:SSP_BASE_OFFSET
add $24, %RSP_LP
/* Restore RDI. */
movq %rdx, %rdi
L(shadow_stack_bound_recorded):
/* Get the current shadow stack pointer. */
rdsspq %rax
/* NB: Save the caller's shadow stack so that we can jump back
to the caller directly. */
addq $8, %rax
movq %rax, oSSP(%rdx)
/* Save the current shadow stack base in ucontext. */
movq %fs:SSP_BASE_OFFSET, %rax
movq %rax, (oSSP + 8)(%rdi)
L(no_shstk):
#endif
/* We have separate floating-point register content memory on the
stack. We use the __fpregs_mem block in the context. Set the
links up correctly. */
leaq oFPREGSMEM(%rdi), %rcx
movq %rcx, oFPREGS(%rdi)
/* Save the floating-point environment. */
fnstenv (%rcx)
fldenv (%rcx)
stmxcsr oMXCSR(%rdi)
/* Save the current signal mask with
rt_sigprocmask (SIG_BLOCK, NULL, set,_NSIG/8). */
leaq oSIGMASK(%rdi), %rdx
xorl %esi,%esi
#if SIG_BLOCK == 0
xorl %edi, %edi
#else
movl $SIG_BLOCK, %edi
#endif
movl $_NSIG8,%r10d
movl $__NR_rt_sigprocmask, %eax
syscall
cmpq $-4095, %rax /* Check %rax for error. */
jae SYSCALL_ERROR_LABEL /* Jump to error handler if error. */
/* All done, return 0 for success. */
xorl %eax, %eax
ret
PSEUDO_END(__getcontext)
weak_alias (__getcontext, getcontext)
一些开源协程库分析
1.云风的协程库
地址:https://github.com/cloudwu/coroutine/tree/master
,这是一个非对称协程库
- 定义协程的状态有以下四种:
#define COROUTINE_DEAD 0
#define COROUTINE_READY 1 // 新创建的协程的状态为这个
#define COROUTINE_RUNNING 2
#define COROUTINE_SUSPEND 3
- 核心成员
// 管理一个线程中的客户端创建的各个协程,包括统计数量、记录运行协程的id等
struct schedule {
char stack[STACK_SIZE]; // 一个线程中的各个协程运行所需要的公用的栈空间
ucontext_t main; // 用于保存上下文信息
int nco; // 使用coroutine_new接口创建的协程的数量
int cap; // 容纳协程个数的容量
int running; // 记录正在运行的协程的id
struct coroutine **co; // 维护着一个动态分配的协程数组
};
// 协程信息
struct coroutine {
coroutine_func func; // 协程执行体
void *ud; // 协程执行的函数参数
ucontext_t ctx; // 用于保存上下文信息
struct schedule * sch; // 当前协程由谁维护
ptrdiff_t cap; // 堆栈的容量
ptrdiff_t size; // 用于表示堆栈的大小
int status; // 协程的状态
char *stack; // 保存堆栈信息的起始位置,当协程主动挂起时需要将堆栈信息保存到这里,以便再次运行时使用。
};
- 核心接口:
- coroutine_open:初始化一个schedule,这个时候schedule内部的running字段的值为-1,表示没有一个客户端创建的协程在运行
- coroutine_new:新建协程,执行一些初始化动作。这个时候新创建的协程的状态为COROUTINE_READY
- coroutine_resume:要么启动新创建协程的运行,此时需要进行context的初始化,然后切换至该上下文运行;要么将处于挂起态的协程恢复执行,此时需要从stack字段中恢复堆栈信息
- coroutine_yield:主动将协程挂起,切换至另一个上下文运行。在切换至另一个上下文运行之前需要先将当前上下文信息保存到coroutine结构体的stack字段中。
- _save_stack:协程挂起时调用这个函数,将当前协程的上下文信息从schedule的栈中拷贝至coroutine的stack字段中
- coroutine_open:初始化一个schedule,这个时候schedule内部的running字段的值为-1,表示没有一个客户端创建的协程在运行