18、setjmp与longjmp:正确使用setjmp/longjmp、自动变量的值、longjmp与信号处理的交互
setjmp和longjmp,这对函数在C语言里算是个「异类」。它们能让你在函数调用栈里「跳来跳去」,有点像goto的超级加强版。但说实话,我在项目里见过不少人用它们翻车。今天我就把这块的坑和正确用法,掰开了讲清楚。
18.1 setjmp/longjmp的基本用法
先看个最简单的例子,感受一下它们是怎么工作的:
#include <setjmp.h>
#include <stdio.h>
jmp_buf env;
void func() {
printf("进入func\n");
longjmp(env, 42); // 跳回setjmp处,返回值为42
printf("这行不会执行\n");
}
int main() {
int ret;
if ((ret = setjmp(env)) == 0) {
printf("第一次调用setjmp,ret=%d\n", ret);
func();
} else {
printf("longjmp跳回来了,ret=%d\n", ret);
}
return 0;
}
运行结果:
第一次调用setjmp,ret=0
进入func
longjmp跳回来了,ret=42
setjmp第一次调用时返回0,然后你正常执行。当longjmp被调用时,程序会「瞬间」跳回到setjmp的位置,并且setjmp这次返回的是longjmp的第二个参数(这里是42)。
嗯,这里要注意:longjmp的第二个参数不能是0。如果你传了0,它会自动变成1。这是标准规定的,我猜是为了和setjmp首次返回0做区分。
18.2 自动变量的值——一个经典的陷阱
这是setjmp/longjmp最容易踩的坑,没有之一。我在项目中就遇到过,排查了整整一个下午。
看这段代码:
#include <setjmp.h>
#include <stdio.h>
jmp_buf env;
void func() {
int a = 100; // 自动变量
longjmp(env, 1);
}
int main() {
int val = 0;
if (setjmp(env) == 0) {
val = 42;
func();
} else {
printf("val = %d\n", val); // 结果可能是42,也可能是未定义的值
}
return 0;
}
为什么会这样?因为longjmp跳回去之后,自动变量的值是不确定的。标准说:如果自动变量在setjmp和longjmp之间被修改了,并且这个变量是volatile的,那它的值是确定的;否则,值是不确定的。
说白了,编译器可能把变量存在寄存器里,longjmp恢复上下文时,寄存器被还原了,但内存里的值没变。这就导致你拿到的值「看运气」。
正确的做法是:要么把变量声明为volatile,要么声明为static。
volatile int val = 0; // 加volatile,保证值正确
// 或者
static int val = 0; // 静态变量不受影响
我个人习惯是:只要用了setjmp/longjmp,所有在两者之间可能被修改的自动变量,一律加volatile。省心。
18.3 longjmp与信号处理的交互
信号处理函数里用longjmp,这事我劝你三思。但有时候确实需要,比如超时处理、异常恢复等场景。
先看一个典型的「信号+longjmp」模式:
#include <setjmp.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
jmp_buf env;
volatile int timeout_flag = 0;
void sigalrm_handler(int sig) {
timeout_flag = 1;
longjmp(env, 1); // 信号处理中跳转
}
int main() {
signal(SIGALRM, sigalrm_handler);
if (setjmp(env) == 0) {
alarm(3); // 3秒后触发SIGALRM
printf("开始等待...\n");
while (!timeout_flag) {
// 做一些耗时操作
}
alarm(0); // 取消闹钟
printf("正常完成\n");
} else {
printf("超时了!\n");
}
return 0;
}
这段代码看起来没问题,但实际运行中可能出问题。为什么?
第一,信号处理函数中调用longjmp是不安全的。 标准说,信号处理函数里可以调用longjmp,但前提是信号处理函数执行期间,没有未完成的异步信号。说白了,如果你在信号处理函数里longjmp,而这时候又来了一个信号,程序行为就是未定义的。
第二,可重入性问题。 信号处理函数里longjmp跳走了,但有些系统调用可能处于「半完成」状态。比如你正在read一个文件描述符,信号来了,longjmp跳走了,这个文件描述符的状态就乱了。
1. 信号处理函数只做最简单的操作——设置标志位,然后longjmp。
2. 不要嵌套信号,在longjmp之前屏蔽其他信号。
3. 跳回去之后,立即检查并恢复所有可能被中断的资源(文件描述符、锁等)。
18.4 避坑指南——我踩过的那些坑
我曾经在一个嵌入式项目里,用setjmp/longjmp实现了一个「任务调度器」。想法很美好:每个任务用setjmp保存上下文,时间片到了就longjmp跳回调度器。结果呢?
- 坑1: 忘了给自动变量加volatile,任务切换后数据全乱了。排查了两天,最后发现是编译器优化把变量放寄存器里了。
- 坑2: 在中断服务函数里调用了longjmp,结果中断嵌套导致栈乱了。从那以后,我规定团队:中断里绝对不允许用longjmp。
- 坑3: longjmp跳回去之后,malloc分配的内存没释放,造成了内存泄漏。因为longjmp不会调用任何析构函数或清理代码。
核心原则:
- setjmp和longjmp之间被修改的自动变量,必须加volatile。
- 信号处理函数中慎用longjmp,非用不可时,确保信号被屏蔽。
- longjmp不会释放资源(堆内存、文件描述符、锁等),跳转前要手动清理。
- 不要跨不同栈层级多次longjmp,容易导致栈混乱。
- setjmp的返回值只能用于判断是首次调用还是longjmp跳回,不要做其他算术运算。
18.5 知识体系图
下面这张图总结了setjmp/longjmp的核心知识点和它们之间的关系:
18.6 总结
setjmp和longjmp是C语言里的一把「双刃剑」。用好了,可以实现优雅的错误恢复和协程调度;用不好,就是一场灾难。
我个人对它们的看法是:能不用就不用,非用不可时,严格遵守规则。 尤其是自动变量的volatile声明和信号处理中的使用限制,这两条是红线,踩了就是未定义行为。
你想想看,一个longjmp跳回去,变量值不对了,资源没释放,信号处理乱套了——这种bug查起来,真的是欲哭无泪。我在项目里吃过亏,所以现在看到setjmp/longjmp,都会多留个心眼。
一句话记住: setjmp/longjmp不是goto,它跳的是「执行上下文」,不是「代码行」。尊重它的规则,它才能为你所用。
公众号:蓝海资料掘金营,微信deep3321