协程

  1. 协程:编程语言层面的概念,又叫做用户态线程,由用户进行切换,因为不用陷入内核态,所以切换效率更高。
  2. 协程的分类:
    1. 非对称式协程:协程主动挂起时,只能返回最初调用它的协程继续执行。在非对称协程中,存在着调用与被调用的关系。比如腾讯的libco
    2. 对称协程:运行的协程和挂起的协程之间没有关系。

与协程相关的四大系统函数

在Linux上,与协程相关的四大函数分别是:getcontext、setcontext、makecontext、swapcontext

1.信号栈
  1. 信号栈定义如下所示:
/* Structure describing a signal stack.  */
typedef struct
{
    void *ss_sp;
    int ss_flags;
    size_t ss_size;
} stack_t;
2.上下文
  1. 与平台相关的上下文数据结构如下所示: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头文件中

  1. 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
  1. setcontext:set the user context,这个函数根据传入的实参恢复上下文。恢复现场
int setcontext(const ucontext_t *ucp);
  1. makecontext:manipulate user context,修改第一参数指向的上下文信息,这个上下文信息可以由getcontext函数获得。
void makecontext(ucontext_t *ucp, void (*func)(), int argc, ...);
函数形参:
    argc:传递给func的参数的个数
  1. swapcontext:manipulate user context,保存第一参数指向的上下文信息,恢复第二参数指向的上下文信息。恢复现场
int swapcontext(ucontext_t *oucp, const ucontext_t *ucp);
4.四大函数的基本使用
  1. 示例如下:来自官方文档
#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.原理剖析
  1. 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,这是一个非对称协程库

  1. 定义协程的状态有以下四种:
#define COROUTINE_DEAD 0
#define COROUTINE_READY 1 // 新创建的协程的状态为这个
#define COROUTINE_RUNNING 2
#define COROUTINE_SUSPEND 3
  1. 核心成员
// 管理一个线程中的客户端创建的各个协程,包括统计数量、记录运行协程的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; // 保存堆栈信息的起始位置,当协程主动挂起时需要将堆栈信息保存到这里,以便再次运行时使用。
};
  1. 核心接口:
    1. coroutine_open:初始化一个schedule,这个时候schedule内部的running字段的值为-1,表示没有一个客户端创建的协程在运行
    2. coroutine_new:新建协程,执行一些初始化动作。这个时候新创建的协程的状态为COROUTINE_READY
    3. coroutine_resume:要么启动新创建协程的运行,此时需要进行context的初始化,然后切换至该上下文运行;要么将处于挂起态的协程恢复执行,此时需要从stack字段中恢复堆栈信息
    4. coroutine_yield:主动将协程挂起,切换至另一个上下文运行。在切换至另一个上下文运行之前需要先将当前上下文信息保存到coroutine结构体的stack字段中。
    5. _save_stack:协程挂起时调用这个函数,将当前协程的上下文信息从schedule的栈中拷贝至coroutine的stack字段中