在 C 语言及系统编程中,程序的执行流通常是自上而下、函数调用有严格的栈结构。但有时我们希望打破函数调用的正常顺序,实现从一个函数跳转到另一个函数,甚至在不同栈上下文之间切换。
常见的手段有:
-
setjmp/longjmp
------ 非局部跳转。 -
ucontext
------ 用户级上下文切换。 -
内联汇编 ------ 直接操作寄存器和栈。
一、setjmp / longjmp ------ 非局部跳转
1、基本原理
-
setjmp
会保存当前的 CPU 寄存器环境(如PC
程序计数器、SP
栈指针、通用寄存器等),保存到一个jmp_buf
类型的结构里。 -
longjmp
可以恢复之前保存的环境,相当于把寄存器恢复到setjmp
调用时的状态。于是程序会重新从 setjmp****返回。
这就像"存档 & 读档":
-
setjmp
就是存档。 -
longjmp
就是读档。
2、示例代码
cpp
#include <stdio.h>
#include <setjmp.h>
jmp_buf env;
void func(int arg) {
printf("in func, arg=%d\n", arg);
longjmp(env, ++arg); // 跳回 setjmp
}
int main() {
int ret = setjmp(env);
if (ret == 0) {
printf("first time setjmp, ret=%d\n", ret);
func(ret);
} else {
printf("after longjmp, ret=%d\n", ret);
}
return 0;
}
3、运行过程
-
第一次调用
setjmp
→ 返回0
,进入if(ret == 0)
分支。 -
调用
func(0)
→longjmp(env, 1)
。 -
程序直接「跳回」到
setjmp
那一行,但这次返回值是1
,所以走到else
分支。
输出大概是:
cpp
first time setjmp, ret=0
in func, arg=0
after longjmp, ret=1
4、应用场景
-
错误恢复机制(比如
libc
的setjmp/longjmp
常用于异常处理)。 -
简单的流程控制,模拟"非局部跳转"。
二、ucontext ------ 用户态上下文切换
1、背景:为什么会有 ucontext
在 Unix/Linux 世界里,一个老大难问题就是 "程序如何假装同时做很多事"。
一台电脑的 CPU 在任意时刻,其实只能执行 一条指令流 。
换句话说:
👉 本质上它一次只能干一件事。
但是我们每天看到的电脑:
-
播着音乐;
-
同时你在打字;
-
浏览器还在刷网页。
这就是"假装同时做很多事"。
它是怎么做到的呢?这就涉及 调度:
-
内核线程 (kernel thread)
由内核负责调度。切换时需要系统调用,涉及内核态 ↔ 用户态的切换 → 开销比较大。
(好比你每次要切换任务,都得去找"国家公务员"帮你换身份证 → 太慢。)
-
用户线程 (user thread)
由用户空间自己管理,不经过内核。切换只是在用户态保存和恢复上下文 → 开销小很多。
(就像你自己用抽屉里的身份证副本换着用 → 快!)
👉 早期的 协程 (coroutine) 、绿色线程 (green thread) 、纤程 (fiber) 都是用户线程的一种。它们需要解决两个核心问题:
-
存档:保存当前函数执行到哪里(包括 CPU 寄存器、栈指针、程序计数器等)。
-
读档:切换到另一个函数继续执行,并且能回到原来的点。
于是,系统在 POSIX.1-2001 里引入了 ucontext API,提供了一个简单的接口来实现"存档/读档"。
不过后来,ucontext
在 glibc 2.8 之后被标记为 弃用(deprecated),因为:
-
接口设计比较古老;
-
跨平台兼容性不好;
-
有更现代的替代方案(如 pthread、现代协程库)。
但------
👉 它仍然是理解 协程 和 用户态任务切换 的经典入门工具。
主要函数:
-
getcontext(ucontext_t *ucp)
:获取当前上下文(寄存器、栈等)。 -
setcontext(const ucontext_t *ucp)
:恢复上下文。 -
makecontext(ucontext_t *ucp, void (*func)(), int argc, ...)
:修改上下文,使其执行某个函数。 -
swapcontext(ucontext_t *oucp, const ucontext_t *ucp)
:保存当前上下文到oucp
,切换到ucp
。
2、原理
当你调用 swapcontext(&A, &B)
时,系统做了两件事:
-
保存 A :把当前寄存器状态(PC、SP、通用寄存器)写入
A->uc_mcontext
。 -
恢复 B :把
B->uc_mcontext
的内容恢复到寄存器,然后PC
指向B
的位置。
换句话说:
👉 调用 swapcontext
后,CPU 就好像"穿越"到了另一个函数里继续跑。
3、示例代码
cpp
#include <stdio.h>
#include <ucontext.h>
ucontext_t ctx[2], main_ctx;
int count = 0;
void func1(void) {
while (count++ < 3) {
printf("func1: step\n");
swapcontext(&ctx[0], &ctx[1]);
}
}
void func2(void) {
while (count++ < 6) {
printf("func2: step\n");
swapcontext(&ctx[1], &ctx[0]);
}
}
int main() {
char stack1[8192];
char stack2[8192];
// 设置 ctx[0]
getcontext(&ctx[0]);
ctx[0].uc_stack.ss_sp = stack1;
ctx[0].uc_stack.ss_size = sizeof(stack1);
ctx[0].uc_link = &main_ctx;
makecontext(&ctx[0], func1, 0);
// 设置 ctx[1]
getcontext(&ctx[1]);
ctx[1].uc_stack.ss_sp = stack2;
ctx[1].uc_stack.ss_size = sizeof(stack2);
ctx[1].uc_link = &main_ctx;
makecontext(&ctx[1], func2, 0);
printf("start swapcontext\n");
swapcontext(&main_ctx, &ctx[0]);
printf("back to main\n");
return 0;
}
4、运行过程
-
swapcontext(&main_ctx, &ctx[0])
→ 保存 main 状态,切换到func1
。 -
func1
打印一次,swapcontext(&ctx[0], &ctx[1])
→ 切到func2
。 -
func2
打印一次,切回func1
。 -
两个函数交替运行,直到循环结束,返回 main。
输出类似:
cpp
start swapcontext
func1: step
func2: step
func1: step
func2: step
func1: step
func2: step
back to main
👉 这就是一个「协程切换」的雏形。
三、汇编方式(简略)
更底层的实现方式是用汇编直接操作栈指针和程序计数器,保存寄存器状态,实现函数间跳转。这是协程库、线程库的基础,但写法复杂,一般开发者不会直接用。