Ucontext
ucontext 是一套定义在 <ucontext.h> 头文件中的 C 语言函数库,它属于 POSIX 标准的一部分(尽管在后续标准中被标记为“过时”)。它的核心功能是允许程序员在用户态(User Space)直接、显式地控制程序的执行上下文(Execution Context)。
为了理解这一点,我们可以将程序的一次执行想象成一个“任务”。这个任务包含了它继续执行下去所需要的一切信息:
- 当前代码执行到哪一行了? (由程序计数器PC寄存器记录)
- 当前的局部变量、函数参数是什么? (存储在程序的栈上,由栈指针SP寄存器管理)
- 计算过程中的中间值是多少? (存储在各种通用CPU寄存器中)
执行上下文就是这些信息在某个瞬间的“快照”。ucontext 的本质,就是提供了一套工具,让我们能够保存(存档)、恢复(读档)和创建(新游戏)这些快照。
这套机制是实现用户级线程(User-level Threads)或协程(Coroutines)的基石,因为它使得任务切换完全在程序内部完成,无需陷入操作系统内核,从而大大降低了切换的开销。
核心组件
ucontext 主要由一个核心结构体和四个主要函数组成。
- 结构体 ucontext_t
1
2
3
4
5
6
7
8
typedef struct ucontext {
struct ucontext *uc_link; // 指向后继上下文
sigset_t uc_sigmask; // 信号屏蔽集
stack_t uc_stack; // 栈信息
mcontext_t uc_mcontext; // 机器上下文(寄存器状态)
...
} ucontext_t;
这是用来存储上下文“快照”的容器。你可以把它理解为一个“存档文件”。它的主要成员包括:
mcontext_t uc_mcontext: 这是最核心的部分,存储了所有平台相关的CPU寄存器状态(如EIP/RIP, ESP/RSP, EAX/RAX等)。这是上下文的真正“快照”数据。
stack_t uc_stack: 描述了该上下文所使用的栈空间。包含了栈的起始地址 (ss_sp) 和大小 (ss_size)。每个独立的执行流(协程)都必须有自己独立的栈。
sigset_t uc_sigmask: 保存了该上下文的信号屏蔽集。当切换到这个上下文时,进程的信号屏蔽集也会被恢复。
ucontext_t *uc_link: 指向一个“后继”上下文, 即返回后继续执行的位置。如果当前上下文的函数执行完毕并正常返回,程序会自动激活 uc_link 指向的上下文。如果 uc_link 为 NULL,则当前协程执行完毕后整个进程会退出。
- 主要函数
| 函数 | 功能比喻 | 详细解释 |
|---|---|---|
int getcontext(ucontext_t *ucp) |
存档 (Save) | 捕获当前执行上下文,将所有信息保存到
ucp 指向的结构体中。用于保存当前进度或创建新上下文。 |
int setcontext(const ucontext_t *ucp) |
读档 (Load) | 丢弃当前上下文,无条件加载
ucp
中的快照并恢复执行。成功后不会返回,因为执行流已被新上下文取代。 |
void makecontext(ucontext_t *ucp, void (*func)(), int argc, ...) |
创建新任务 (New Game) | 修改通过 getcontext 获取的
ucp,将其与新函数
func 绑定。激活时执行
func。绑定前需手动分配栈空间和设置后继上下文。 |
int swapcontext(ucontext_t *oucp, const ucontext_t *ucp) |
切换任务 (Switch) | 原子地保存当前上下文到
oucp,并加载激活
ucp
指向的新上下文。是协程调度的核心。 |
示例
下面是一个简单的示例,展示了如何使用 ucontext 实现协程之间的切换:
1 |
|
预期输出:
1 | Main: Starting coroutines |
首先, main 函数初始化了两个协程的上下文,并为它们分配了独立的栈。main 调用 swapcontext(&main_context, &coroutine1_context),保存自己的状态,然后跳转到 coroutine_func1 开始执行。
接着 coroutine_func1 打印 “ping 0”,然后调用 swapcontext,保存自己的状态(此时它在循环的第一次迭代中),并跳转到 coroutine_func2。coroutine_func2 从头开始执行,打印 “pong 0”,然后调用 swapcontext,保存自己的状态,并跳转回 coroutine_func1。
在这里, coroutine_func1 从它上次离开的地方(swapcontext 调用之后)继续执行,进入下一次循环,打印 “ping 1”,然后再次切换。
这个“乒乓”过程一直持续,直到两个协程的循环都结束。coroutine_func2 最后一次切换回 coroutine_func1,coroutine_func1 循环结束,函数返回。
由于之前设置了 coroutine1_context.uc_link = &main_context,当 coroutine_func1 返回时,程序会自动激活 main_context。main 函数从它当初调用 swapcontext 的地方继续执行,打印 “Main: Coroutines finished”,程序结束。
在某种程度上, getcontext() + makecontext()的组合类似于 Linux 下的 fork() + exec(),它们都遵循复制–修改的操作逻辑 getcontext() 获取当前线程的执行上下文“快照”, 然后程序员手动修改这个上下文结构体,为其分配一个新的栈空间,并使用 makecontext() 将其与一个全新的函数绑定。这创造了一个新的、待执行的逻辑分支(协程)。 而 fork() 则是复制当前进程的所有资源(包括内存空间、文件描述符等),然后父进程使用 exec() 加载一个全新的程序映像,开始执行一个完全不同的代码路径。