47、setjmp/longjmp:非局部跳转与资源泄漏风险
说实话,setjmp和longjmp这对组合,在C语言里算是个“异类”。
我刚开始用它们的时候,觉得这东西真方便——想跳哪就跳哪,比goto还灵活。但后来在项目中吃了亏,才明白这玩意儿有多危险。今天咱们就聊聊这个“非局部跳转”的陷阱。
什么是setjmp/longjmp?
简单说,setjmp就像在代码里插了个“书签”,longjmp就是“直接翻到书签那页”。
它俩配合使用,可以实现跨函数的跳转。不像goto只能在一个函数里跳,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 val = setjmp(env);
if (val == 0) {
printf("首次调用setjmp\n");
func();
} else {
printf("从longjmp跳回,返回值:%d\n", val);
}
return 0;
}
运行结果:
首次调用setjmp
进入func
从longjmp跳回,返回值:42
看到了吗?func里的printf("这行不会执行")根本没机会跑。程序直接跳回了main里的setjmp位置。
资源泄漏:最大的坑
嗯,这里要重点说。longjmp最大的问题就是——它不管资源释放。
你想想看,正常函数返回时,局部变量的析构、malloc的free、文件句柄的close,这些都会按顺序执行。但longjmp一跳,中间所有栈帧都直接丢弃了。
我在项目中遇到过这样一个bug:一个嵌入式设备跑着跑着就死机了。查了三天,发现是某个异常处理路径用了longjmp,跳之前忘了释放一个DMA缓冲区。每次触发异常就泄漏一块内存,最后系统内存耗尽。
#include <setjmp.h>
#include <stdlib.h>
jmp_buf env;
void risky_func() {
char *buf = malloc(1024); // 分配内存
if (!buf) return;
// 做一些操作...
if (some_error) {
longjmp(env, 1); // ❌ buf泄漏了!
}
free(buf); // 正常路径会释放
}
int main() {
if (setjmp(env) == 0) {
risky_func();
} else {
// 异常处理,但buf已经泄漏了
}
return 0;
}
避坑指南:我踩过的那些坑
我曾经在一个通信协议栈里用setjmp/longjmp做错误恢复。结果发现,每次协议解析出错,系统可用内存就少一块。后来我总结了几条铁律:
- 不要在持有资源时调用longjmp——malloc、fopen、lock这些,跳之前必须清理干净
- 用“资源包装”模式——把资源分配和释放封装成一对函数,longjmp前统一调用清理函数
- 考虑用返回值替代——很多时候,用错误码逐层返回比longjmp更安全
setjmp/longjmp的知识体系
下面这张图,是我自己总结的setjmp/longjmp核心逻辑。你看一眼就明白了:
局部变量的状态:另一个陷阱
longjmp跳回去之后,局部变量是什么状态?答案是:不一定。
C标准说,如果局部变量是volatile的,那它的值就是longjmp时的值。如果不是volatile,那它的值就是“未指定的”。说白了,编译器可能已经把它优化到寄存器里了,longjmp恢复栈帧时不会恢复寄存器。
jmp_buf env;
void test() {
int a = 10;
volatile int b = 20;
if (setjmp(env) != 0) {
// 这里a的值是不确定的!
// b的值一定是20(因为volatile)
printf("a=%d, b=%d\n", a, b);
return;
}
a = 100;
b = 200;
longjmp(env, 1);
}
什么时候该用?什么时候不该用?
说实话,我现在的项目里几乎不用setjmp/longjmp了。但有些场景确实绕不开:
| 场景 | 推荐度 | 说明 |
|---|---|---|
| 深度嵌套的错误恢复 | ⚠️ 谨慎 | 可以用,但必须确保所有中间函数都清理了资源 |
| 协程/用户态线程 | ✅ 可用 | setjmp/longjmp是实现协程的基础工具之一 |
| 信号处理 | ❌ 避免 | 信号处理函数中调用longjmp可能导致未定义行为 |
| 普通错误处理 | ❌ 别用 | 用错误码或异常(C++)更安全 |
我个人习惯是:能用返回值解决的问题,绝不用longjmp。只有当你真的需要“跨多层函数调用直接跳回”时,才考虑它。而且一旦用了,就要像对待核武器一样——小心翼翼。
好了,关于setjmp/longjmp就聊这么多。记住一句话:非局部跳转,跳的是代码,漏的是资源。用之前,先想清楚你的内存、文件、锁,谁来释放。