1️⃣ 概述:从”上下文切换“ 到 ”用户态线程“

  最近在开发服务器相关项目时,接触到了 协程(Coroutine) 这个概念。协程在高并发服务器、网络框架、异步编程中都很常见,而在 Linux 平台上,很多早期的用户态协程实现都与 ucontext 这组 API 有关。

  虽然在项目里经常使用 ucontext,但一直没有系统地理解过:

  • 到底解决了什么问题?
  • 它和线程有什么区别?
  • 它又是如何在底层完成 “切换执行流” 的?

  所以就想借这个机会,对 ucontext 的原理做一次梳理。这篇文章不会从业务层面讨论 “协程框架怎么设计”,而是聚焦于一个更底层的问题:

ucontext 到底是如何实现用户态上下文切换的?

  在进入正式主题之前,先不急着看 API 或源码,因为如果一开始就直接面对 getcontextmakecontextswapcontext 这些接口,很容易只记住 “怎么用”,却不明白 “为什么它能这样用”

  所以先从一个生活中的例子来理解什么叫做 “上下文切换”

生活案例

  我们每个人在日常生活中,其实都在频繁地做 “上下文切换”

  比如,某个周末的下午,你正在家里认真写博客。
  这时手机突然响了,有人给你打电话。于是你停下写作,去接电话。
  电话结束后,你又重新坐回电脑前,继续把刚才那一段内容写完。

这个过程看起来很普通,但如果从程序执行的角度看,它其实就是一次非常典型的 “上下文切换”

  • 原本你处在 “写博客” 这个状态中;
  • 电话到来后,你切换到了 “接电话” 这个状态中;
  • 电话结束后,你又恢复到 “写博客” 的状态中,继续从之前中断的位置往下做。

这里最关键的不是 “你做了两件事”,而是:

你能够在中途暂停一件事,并在之后准确地回到之前的位置继续执行。

程序也是一样:
  一个执行流如果想在中途暂停,然后未来还能从原来的位置继续运行,就必须把它当时的状态保存下来。这个 “状态”,就可以理解为它的 上下文(context

2️⃣ 寄存器相关知识

  现在要理解 ucontext,先补一下 x86-64 寄存器 的基础非常有必要,因为所谓 “上下文切换”,本质上就是:

把当前执行流的寄存器状态保存下来,再恢复另一个执行流的寄存器状态。

  所以先把寄存器搞清楚,后面再看 swapcontext 就会顺很多。

什么是寄存器?

  寄存器(Register)是 CPU 内部的一小块高速存储空间,速度比内存快得多。
CPU 执行指令时,很多数据不会直接在内存里反复读写,而是先放到寄存器里参与运算

  你可以把寄存器理解成:

  • 内存像仓库
  • 寄存器像手边的工具箱

正在处理的数据,通常都先放在寄存器里

x86-64 常见的 16 个通用寄存器

  在 x86-64 架构下,最常见的是这 1664 位通用寄存器:

1
2
3
4
%rax, %rbx, %rcx, %rdx
%rsi, %rdi, %rbp, %rsp
%r8, %r9, %r10, %r11
%r12, %r13, %r14, %r15

  接下来介绍,每个寄存器的作用:

  • %rax
    • 作用:

      • 函数返回值寄存器
      • 某些运算的默认寄存器
    • 用途:

      • 函数执行完后,返回值通常放在 %rax
    • 案例:

      1
      2
      3
      int add() {
      return 5; // 最后返回给调用者时,5 通常会放到 %rax
      }
  • %rbx
    • 作用:
      • 通常用于保存数据;
      • 属于 callee-saved(被调用者保存)寄存器;
    • 用途:
      • 如果一个函数要用 %rbx,那它在修改之前,通常要先保存原值,返回前再恢复。
  • %rcx
    • 作用:

      • 函数参数寄存器之一;
      • 某些指令的计数寄存器;
    • 用途:

      • 它通常作为:第 4 个整数/指针参数
    • 案例:

      1
      foo(a, b, c, d) // 这里 d 放在 %rcx,c 放在 %rdx,b 放在 %rsi,a 放在 %rdi
  • %rdx
    • 作用:
      • 函数参数寄存器之一;
      • 某些乘除法指令中有特殊用途;
    • 用途:
      • 它通常作为:第 3 个整数/指针参数;
  • %rsi
    • 作用: 函数参数寄存器之一;
    • 用途: 通常作为第 2 个整数/指针参数;
  • %rdi
    • 作用: 函数参数寄存器之一;
    • 用途: 通常作为第 1 个整数/指针参数;
  • %rbp
    • 作用:

      • 栈帧基址寄存器;
      • 也是 callee-saved 寄存器;
    • 用途: 在传统函数调用里,它常用来作为 当前函数栈帧的基准位置

    • 案例:

      1
      2
      3
      4
      5
      6
      7
      8
      高地址
      ------------
      函数参数
      返回地址
      旧 rbp
      局部变量
      ------------
      低地址
  • %rsp
    • 作用: 栈指针寄存器,始终指向当前栈顶附近
    • 用途:
      • 函数调用、局部变量、返回地址,本质上都和栈有关;
      • 协程切换时,只要把 %rsp 换掉,CPU 实际上就切到了另一块栈空间;
  • %r8
    • 作用: 函数参数寄存器之一,通常作为第 5 个整数/指针参数
  • %r9
    • 作用: 函数参数寄存器之一,通常作为第 6 个整数/指针参数
  • %r10
    • 作用: 临时寄存器,caller-saved(调用者保存)
  • %r11
    • 作用: 临时寄存器,caller-saved(调用者保存)
  • %r12
    • 作用: 临时寄存器,caller-saved(调用者保存)
  • %r13
    • 作用: 临时寄存器,caller-saved(调用者保存)
  • %r14
    • 作用: 临时寄存器,caller-saved(调用者保存)
  • %r15
    • 作用: 临时寄存器,caller-saved(调用者保存)

函数参数是如何放入到寄存器的?

  在 Linux x86-64 下,最常见的 System V ABI 规定:如果参数超过 6 个,多出来的通常放栈上

1
2
3
4
5
6
第1个参数 -> %rdi
第2个参数 -> %rsi
第3个参数 -> %rdx
第4个参数 -> %rcx
第5个参数 -> %r8
第6个参数 -> %r9

caller-savecallee-save

  • caller-save
    • 定义: 调用其他函数之前,如果这些寄存器里的值当前函数还想保留,那么这些寄存器里的值必须调用者自己先保存,因为针对这些寄存器,被调用的函数通常可以直接修改。

    • 常见的 caller-save

      1
      %rax, %rcx, %rdx, %rsi, %rdi, %r8, %r9, %r10, %r11
  • callee-saved
    • 定义: 被调用的函数如果要使用这些寄存器,必须先保存这些寄存器原来的值,返回前恢复这些寄存器原本的值。这些寄存器存放 “需要跨函数保持不变” 的数据。

    • 常见的 callee-save

      1
      %rbx, %rbp, %r12, %r13, %r14, %r15

3️⃣ 源码讲解

ucontext

  ucontext 是一组用于实现用户态上下文切换的底层 API,其核心作用是 保存、恢复和切换程序的执行现场
  “执行现场” 主要包括 当前的寄存器状态栈信息 以及 后续要执行的上下文 等内容。借助 ucontext,程序可以在用户空间主动控制执行流:

  • 先保存当前上下文的运行状态
  • 再恢复另一个上下文之前保存好的状态
  • 从而实现类似 “暂停—切换—恢复” 的效果。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typedef struct ucontext
{
unsigned long int uc_flags;
struct ucontext *uc_link; // 待切换上下文
__sigset_t uc_sigmask; // 信号屏蔽字掩码
stack_t uc_stack; // 上下文所使用的栈
mcontext_t uc_mcontext; // 保存的上下文的寄存器信息
long int uc_filler[5];
} ucontext_t;

//其中mcontext_t 定义如下
typedef struct
{
gregset_t __ctx(gregs); //所装载寄存器
fpregset_t __ctx(fpregs); //寄存器的类型
} mcontext_t;

//其中gregset_t 定义如下
typedef greg_t gregset_t[NGREG];//包括了所有的寄存器的信息

getcontext()

  • 函数: int getcontext(ucontext_t* ucp)
  • 作用:
    • 获取当前执行流的上下文信息,并将其保存到一个 ucontext_t 结构体中。
    • 这里保存的内容可以理解为 程序当前时刻的 “运行现场”,主要包括 寄存器状态栈信息 以及 当前代码执行到的位置 等。

整体代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ENTRY(__getcontext)
/* Save the preserved registers, the registers used for passing
args, and the return address. */
movq %rbx, oRBX(%rdi)
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)

核心作用

  • 第一部分:保存“被调用者保存寄存器”

    1
    2
    3
    4
    5
    6
    movq    %rbx, oRBX(%rdi)    # 把当前 rbx 寄存器的值保存到 ucontext 中的 RBX 字段
    movq %rbp, oRBP(%rdi) # 把当前 rbp 寄存器的值保存到 ucontext 中的 RBP 字段
    movq %r12, oR12(%rdi) # 把当前 r12 寄存器的值保存到 ucontext 中的 R12 字段
    movq %r13, oR13(%rdi) # 把当前 r13 寄存器的值保存到 ucontext 中的 R13 字段
    movq %r14, oR14(%rdi) # 把当前 r14 寄存器的值保存到 ucontext 中的 R14 字段
    movq %r15, oR15(%rdi) # 把当前 r15 寄存器的值保存到 ucontext 中的 R15 字段
    • 上述寄存器都是 callee-saved 寄存器,也就是通常认为 “跨函数调用应该保持不变” 的寄存器。如果以后要恢复到这个上下文,那么这些值必须原样拿回来。
  • 第二部分:保存参数寄存器

    1
    2
    3
    4
    5
    6
    7
    movq    %rdi, oRDI(%rdi)    # 把当前 rdi 寄存器的值保存到 ucontext 中的 RDI 字段;
    # 此时 rdi 本身还存着 ucontext_t* 的地址
    movq %rsi, oRSI(%rdi) # 把当前 rsi 寄存器的值保存到 ucontext 中的 RSI 字段
    movq %rdx, oRDX(%rdi) # 把当前 rdx 寄存器的值保存到 ucontext 中的 RDX 字段
    movq %rcx, oRCX(%rdi) # 把当前 rcx 寄存器的值保存到 ucontext 中的 RCX 字段
    movq %r8, oR8(%rdi) # 把当前 r8 寄存器的值保存到 ucontext 中的 R8 字段
    movq %r9, oR9(%rdi) # 把当前 r9 寄存器的值保存到 ucontext 中的 R9 字段
    • x86-64 下,这几个寄存器通常用来传函数前 6 个参数
  • 第三部分:保存返回地址,也就是未来恢复后该从哪里继续执行

    1
    2
    movq    (%rsp), %rcx
    movq %rcx, oRIP(%rdi)
    • %rsp 是当前栈指针:函数刚被调用进来时,栈顶通常放着:调用者的返回地址;
    • (%rsp) 取出来的就是:当前这个函数执行完后,本来要返回到哪里去;
    • getcontext 把它读出来放到 %rcx
    • RIP 表示指令指针,表示 CPU 下一条该执行哪条指令,也就是说:把 “当前函数将来返回后要继续执行的位置” 保存起来;
    • 所以这里保存的不是 “当前正在执行的 getcontext 内部某一行”,而是:调用 getcontext 之后程序应该继续往下走的位置;
  • 第四部分:保存栈指针,但要跳过返回地址

    1
    2
    leaq    8(%rsp), %rcx       /* Exclude the return address.  */
    movq %rcx, oRSP(%rdi)
    • (%rsp) 存的是返回地址,8(%rsp) 就是 “返回地址上面的位置”
    • 如果将来恢复上下文,我们希望恢复时的栈状态应该是:
      • 返回地址已经被 “弹出”
      • 栈顶已经回到调用者自己的栈框架里

makecontext()

  • 函数: void makecontext(ucontext_t* ucp, void (*func)(), int argc, ...)
  • 作用:
    • 基于一个已有的 ucontext_t 上下文,构造一个 “新的可执行上下文”,使其在后续被恢复时,能够从指定函数 func 开始执行。
    • 它可以理解为:手动为一个新的执行流搭建运行入口,主要会设置该上下文的入口函数、栈信息以及函数参数等内容。
    • 调用 makecontext 后,这个上下文就不再表示 “当前程序运行到哪里”,而是表示:当该上下文将来被 setcontext()swapcontext() 恢复时,程序应该从 func 开始执行。
    • 因此,makecontext 的本质不是 “切换上下文”,而是初始化一个新的上下文执行现场,为后续的用户态调度和协程切换做好准备。

整体代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
void __makecontext (ucontext_t *ucp, void (*func) (void), int argc, ...)
{
extern void __start_context (void); // 声明一个外部函数。它是协程/上下文函数执行完毕后的 “收尾函数”
greg_t *sp; // 用来操作新上下文栈空间的指针
unsigned int idx_uc_link; // 栈上保存 uc_link 的下标位置
va_list ap; // 用于读取可变参数
int i;

/* Generate room on stack for parameter if needed and uc_link. */
sp = (greg_t *) ((uintptr_t) ucp->uc_stack.ss_sp
+ ucp->uc_stack.ss_size); // 先让 sp 指向这块栈空间的“栈顶”(高地址)
// 因为 x86-64 的栈通常是从高地址向低地址增长的

sp -= (argc > 6 ? argc - 6 : 0) + 1; // 如果参数超过 6 个,多出来的参数需要放到栈上
// 这里额外预留:
// 1) 超过 6 个的参数空间
// 2) 一个 uc_link 的空间

/* Align stack and make space for trampoline address. */
sp = (greg_t *) ((((uintptr_t) sp) & -16L) - 8);
// 按照 x86-64 ABI 要求把栈地址对齐到 16 字节
// 然后再减 8 字节,给“返回地址/跳板地址”预留空间
// 后面这里会放 __start_context

idx_uc_link = (argc > 6 ? argc - 6 : 0) + 1;
// 记录 uc_link 在栈上的位置
// 如果参数超过 6 个,uc_link 要放在这些“栈上传参”之后

/* Setup context ucp. */
/* Address to jump to. */
ucp->uc_mcontext.gregs[REG_RIP] = (uintptr_t) func;
// 设置新上下文恢复后要跳转到的指令地址
// 也就是:将来切到这个上下文时,从 func 开始执行

/* Setup rbx.*/
ucp->uc_mcontext.gregs[REG_RBX] = (uintptr_t) &sp[idx_uc_link];
// 把 RBX 设置为指向 uc_link 所在位置的地址
// 后续 func 执行结束后,__start_context 会利用这个地址找到 uc_link

ucp->uc_mcontext.gregs[REG_RSP] = (uintptr_t) sp;
// 设置新上下文的栈指针 RSP
// 将来恢复该上下文时,CPU 会使用这块新栈

/* Setup stack. */
sp[0] = (uintptr_t) &__start_context; // 在栈顶放入一个“伪返回地址”
// 这样当 func 执行完毕后,ret 不会回到真实调用者
// 而是跳到 __start_context 去做收尾处理

sp[idx_uc_link] = (uintptr_t) ucp->uc_link;
// 把 ucp->uc_link 保存到栈上预留的位置
// 当 func 结束后,__start_context 会取出这个 uc_link
// 决定是否切换到后继上下文

va_start (ap, argc); // 开始读取可变参数

/* Handle arguments.

The standard says the parameters must all be int values. This is
an historic accident and would be done differently today. For
x86-64 all integer values are passed as 64-bit values and
therefore extending the API to copy 64-bit values instead of
32-bit ints makes sense. It does not break existing
functionality and it does not violate the standard which says
that passing non-int values means undefined behavior. */
// 处理传给 func 的参数
// 在 x86-64 下,前 6 个整数/指针参数通常走寄存器:
// rdi, rsi, rdx, rcx, r8, r9
// 超过 6 个的参数才放栈上

for (i = 0; i < argc; ++i)
switch (i)
{
case 0:
ucp->uc_mcontext.gregs[REG_RDI] = va_arg (ap, greg_t);
// 第 1 个参数放入 RDI
break;

case 1:
ucp->uc_mcontext.gregs[REG_RSI] = va_arg (ap, greg_t);
// 第 2 个参数放入 RSI
break;

case 2:
ucp->uc_mcontext.gregs[REG_RDX] = va_arg (ap, greg_t);
// 第 3 个参数放入 RDX
break;

case 3:
ucp->uc_mcontext.gregs[REG_RCX] = va_arg (ap, greg_t);
// 第 4 个参数放入 RCX
break;

case 4:
ucp->uc_mcontext.gregs[REG_R8] = va_arg (ap, greg_t);
// 第 5 个参数放入 R8
break;

case 5:
ucp->uc_mcontext.gregs[REG_R9] = va_arg (ap, greg_t);
// 第 6 个参数放入 R9
break;

default:
/* Put value on stack. */
sp[i - 5] = va_arg (ap, greg_t);
// 第 7 个及之后的参数不能再放寄存器了
// 需要按照调用约定放到新栈上
// 这里写到栈中对应位置,供 func 启动后读取
break;
}

va_end (ap); // 可变参数读取结束
}

代码逻辑

  • 首先声明外部函数 __start_context ,用来执行协程上下文的收尾工作;
  • 创建一个指向栈空间的栈指针 sp ,并且让 sp 指向程序员在堆区提前分配好的内存;
  • sp 额外预留指定大小的空间:
    • 函数返回地址(指定函数 __start_context
    • 多余函数参数(ABI 规定当函数参数超过 6 个,之后的参数需要被保存在
    • uc_link(需要被切换的上下文)
  • ucontext中的寄存器 uc_mcontext 指定保存内容
    • RIP:恢复新上下文后,需要执行的指令地址 func
    • RBX:保存指向 uc_link 所在位置的地址;
    • RSP:保存新上下文的栈指针;
    • 函数的前 6 个参数保存在寄存器中,后续的函数参数保存在栈中;

swapcontext

  • 函数: int swapcontext(ucontext_t* oucp, const ucontext_t* ucp)
  • 作用:
    • 先保存当前执行流的上下文信息 到 oucp 中,再恢复并切换到 ucp 所表示的上下文。
    • 它可以理解为一次完整的 “保存当前现场 + 切换到新现场” 的操作,因此常用于用户态协程或 Fiber 之间的切换。
    • 这里保存和恢复的内容同样包括 当前执行流寄存器状态栈信息 以及 后续代码执行位置 等关键现场。
    • 当未来再次切回 oucp 时,程序会表现得像 swapcontext() 这次调用刚刚返回一样,从原来中断的位置继续往下执行。
    • 因此,swapcontext 的本质不是单纯调用另一个函数,而是在用户态主动完成两个执行流之间的上下文切换。

整体代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
ENTRY(__swapcontext)
/* Save the preserved registers, the registers used for passing args,
and the return address. */

movq %rbx, oRBX(%rdi) # 将当前 rbx 保存到旧上下文 oucp 中,对应 RBX 字段
movq %rbp, oRBP(%rdi) # 将当前 rbp 保存到旧上下文 oucp 中,对应 RBP 字段
movq %r12, oR12(%rdi) # 将当前 r12 保存到旧上下文 oucp 中,对应 R12 字段
movq %r13, oR13(%rdi) # 将当前 r13 保存到旧上下文 oucp 中,对应 R13 字段
movq %r14, oR14(%rdi) # 将当前 r14 保存到旧上下文 oucp 中,对应 R14 字段
movq %r15, oR15(%rdi) # 将当前 r15 保存到旧上下文 oucp 中,对应 R15 字段

movq %rdi, oRDI(%rdi) # 将当前 rdi 保存到旧上下文 oucp 中,对应 RDI 字段;此时 rdi 本身存的是 oucp 地址
movq %rsi, oRSI(%rdi) # 将当前 rsi 保存到旧上下文 oucp 中,对应 RSI 字段;rsi 此时存的是将要切换到的新上下文 ucp 地址
movq %rdx, oRDX(%rdi) # 将当前 rdx 保存到旧上下文 oucp 中,对应 RDX 字段
movq %rcx, oRCX(%rdi) # 将当前 rcx 保存到旧上下文 oucp 中,对应 RCX 字段
movq %r8, oR8(%rdi) # 将当前 r8 保存到旧上下文 oucp 中,对应 R8 字段
movq %r9, oR9(%rdi) # 将当前 r9 保存到旧上下文 oucp 中,对应 R9 字段

movq (%rsp), %rcx # 取出当前栈顶保存的返回地址,即 swapcontext 将来“返回后”应该继续执行的位置
movq %rcx, oRIP(%rdi) # 将这个返回地址保存到旧上下文 oucp 的 RIP 字段中
leaq 8(%rsp), %rcx # 计算“弹出返回地址之后”的栈顶位置,也就是函数返回后的 rsp
movq %rcx, oRSP(%rdi) # 将这个新的栈顶位置保存到旧上下文 oucp 的 RSP 字段中



/* Load the new stack pointer and the preserved registers. */
movq oRSP(%rsi), %rsp # 从新上下文 ucp 中恢复栈指针 rsp,切换到新上下文所使用的栈
movq oRBX(%rsi), %rbx # 从新上下文 ucp 中恢复 rbx
movq oRBP(%rsi), %rbp # 从新上下文 ucp 中恢复 rbp
movq oR12(%rsi), %r12 # 从新上下文 ucp 中恢复 r12
movq oR13(%rsi), %r13 # 从新上下文 ucp 中恢复 r13
movq oR14(%rsi), %r14 # 从新上下文 ucp 中恢复 r14
movq oR15(%rsi), %r15 # 从新上下文 ucp 中恢复 r15

/* The following ret should return to the address set with
getcontext. Therefore push the address on the stack. */
movq oRIP(%rsi), %rcx # 取出新上下文中保存的 RIP,也就是恢复后应该继续执行的指令地址
pushq %rcx # 将这个地址压入当前新栈顶,作为接下来 ret 指令要跳转到的“返回地址”

/* Setup registers used for passing args. */
movq oRDI(%rsi), %rdi # 从新上下文 ucp 中恢复 rdi
movq oRDX(%rsi), %rdx # 从新上下文 ucp 中恢复 rdx
movq oRCX(%rsi), %rcx # 从新上下文 ucp 中恢复 rcx
movq oR8(%rsi), %r8 # 从新上下文 ucp 中恢复 r8
movq oR9(%rsi), %r9 # 从新上下文 ucp 中恢复 r9
movq oRSI(%rsi), %rsi

代码逻辑

  • 首先将当前执行流的上下文信息保存到旧上下文 oucp 中,保存内容主要包括:
    • 被调用者保存寄存器:RBXRBPR12 ~ R15
    • 参数寄存器:RDIRSIRDXRCXR8R9
    • 当前函数返回后应该继续执行的位置,即保存到 RIP 中;
    • 当前函数返回后的栈顶位置,即保存到 RSP 中;
  • 其中,(%rsp) 取出的并不是 swapcontext 内部当前正在执行的指令地址,而是调用 swapcontext 结束后应该返回的位置,因此它被保存到旧上下文的 RIP 中;
  • 同时,8(%rsp) 表示弹出返回地址后的栈顶位置,因此它被保存到旧上下文的 RSP 中,用来保证未来恢复该上下文时,程序能够像 swapcontext 已经正常返回一样继续执行;
  • 完成旧上下文保存后,开始从新上下文 ucp 中恢复执行现场,主要包括:
    • 恢复新上下文的栈指针 RSP,切换到新上下文自己的栈;
    • 恢复被调用者保存寄存器:RBXRBPR12 ~ R15
    • 恢复参数寄存器:RDIRSIRDXRCXR8R9
  • 随后将新上下文中保存的 RIP 压入当前新栈中,作为接下来 ret 指令要跳转到的目标地址;
  • 最终通过 ret 完成控制流跳转,使 CPU 从新上下文原来保存的位置继续执行;

因此,swapcontext 的本质就是:

  • 先保存当前执行流的运行现场
  • 再恢复另一个执行流的运行现场
  • 从而在用户态完成一次完整的上下文切换。