ucontext学习

最近在看C++协程相关的代码, 发现很多协程的实现都用到了ucontext库, 因此这里记录下ucontext库的学习笔记。

1. 概述

ucontext 是 POSIX 标准中定义的一组函数,用于实现用户级上下文切换。它允许程序保存和恢复执行上下文(如寄存器、程序计数器、栈等),常用于实现协程、轻量级线程或任务调度等功能。以下是对 ucontext 的分章节详细介绍,并结合示例代码进行说明。

因此, ucontext 是实现协程的一种方式吗因为协程本质上就是一种用户级的线程, 既然是一种特殊的线程, 其上下文切换的实现方式应该和线程的上下文切换类似, 也需要保存必要的寄存器、程序计数器、栈等信息。故ucontext 是实现协程库的的一种手段, 例如云风的coroutine库就是基于ucontext实现的: https://github.com/cloudwu/coroutine/

ucontext 提供了一种在用户空间管理执行上下文的方式,避免了内核级线程切换的开销。它的核心功能包括:

  • 保存当前执行上下文。
  • 恢复之前保存的上下文。
  • 创建新的执行上下文。
  • 在上下文之间切换。

ucontext 的主要数据结构是 ucontext_t,它保存了上下文的所有信息。

2. 核心数据结构:ucontext_t

ucontext_tucontext 的核心数据结构,定义如下:

1
2
3
4
5
6
7
typedef struct ucontext {
struct ucontext *uc_link; // 指向下一个上下文
sigset_t uc_sigmask; // 信号掩码
stack_t uc_stack; // 栈信息
mcontext_t uc_mcontext; // 机器上下文(寄存器等)
// 其他实现相关的字段
} ucontext_t;
  • uc_link: 当前上下文结束时,切换到哪个上下文。如果为 NULL,则程序终止。
  • uc_sigmask: 上下文中的信号掩码,用于控制信号处理。
  • uc_stack: 当前上下文使用的栈信息。
  • uc_mcontext: 保存机器相关的上下文信息(如寄存器状态)。

3. 核心函数

3.1 getcontext:保存当前上下文

1
int getcontext(ucontext_t *ucp);
  • 功能: 将当前执行上下文保存到 ucp 指向的 ucontext_t 结构体中。
  • 返回值: 成功返回 0,失败返回 -1
  • 用途: 通常用于保存当前状态,以便后续恢复。

3.2 setcontext:恢复上下文

1
int setcontext(const ucontext_t *ucp);
  • 功能: 从 ucp 指向的 ucontext_t 结构体恢复上下文。
  • 返回值: 如果成功,不会返回;如果失败,返回 -1
  • 用途: 用于跳转到之前保存的上下文。

注意:
setcontext类似fork, 调用后会更改当前的执行流, 但区别是fork后会创建一个执行流(新的进程), 而setcontext不会创建新的进程, 而是直接切换到之前的上下文。

3.3 makecontext:创建新上下文

1
void makecontext(ucontext_t *ucp, void (*func)(), int argc, ...);
  • 功能: 为 ucp 设置一个新的上下文,指定入口函数 func 和参数。
  • 参数:
    • ucp: 需要初始化的上下文。
    • func: 新上下文的入口函数。
    • argc: 传递给 func 的参数个数。
    • ...: 具体的参数值。
  • 用途: 用于创建一个新的执行上下文。

makecontext 只负责创建一个新的上下文,但不会立即进入或切换到该上下文

3.4 swapcontext:切换上下文

1
int swapcontext(ucontext_t *oucp, const ucontext_t *ucp);
  • 功能: 保存当前上下文到 oucp,并切换到 ucp 指定的上下文。
  • 返回值: 如果成功,不会返回;如果失败,返回 -1
  • 用途: 用于在两个上下文之间切换。

4. 示例代码

以下是一个完整的示例,展示如何使用 ucontext 实现上下文切换。

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
#include <stdio.h>
#include <ucontext.h>
#include <unistd.h>

// 定义两个上下文
ucontext_t ctx[2]; // 0: main, 1: func1

// 示例函数 1
void func1() {
printf("func1: started\n");
printf("func1: switching to main\n");
swapcontext(&ctx[1], &ctx[0]); // 切换到 main 上下文
printf("func1: ended\n");
}

int main() {
char stack1[8192]; // 为 func1 分配栈空间

// 初始化上下文 ctx[1]
getcontext(&ctx[1]);
ctx[1].uc_stack.ss_sp = stack1; // 设置栈指针
ctx[1].uc_stack.ss_size = sizeof(stack1); // 设置栈大小
ctx[1].uc_link = &ctx[0]; // 结束后返回到 main 上下文
makecontext(&ctx[1], func1, 0); // 设置入口函数为 func1

// 切换到 func1 上下文
printf("main: switching to func1\n");
swapcontext(&ctx[0], &ctx[1]);
printf("main: back from func1\n");

// 再次切换到 func1 上下文
printf("main: switching to func1 again\n");
swapcontext(&ctx[0], &ctx[1]);
printf("main: end\n");

return 0;
}

这段代码的执行流程如下:

  1. 初始化上下文:

    • 使用 getcontext 初始化 ctx[1]
    • 设置 ctx[1] 的栈空间和大小。
    • 使用 makecontextctx[1] 的入口函数设置为 func1
  2. 第一次切换:

    • 调用 swapcontext 切换到 ctx[1],执行 func1
    • func1 打印消息后,切换回 ctx[0](即 main)。
  3. 第二次切换:

    • main 再次调用 swapcontext,切换到 ctx[1]
    • func1 继续执行,打印剩余消息后结束。

输出结果

1
2
3
4
5
6
7
8
$ ./a.out 
main: switching to func1
func1: started
func1: switching to main
main: back from func1
main: switching to func1 again
func1: ended
main: end