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一跳,中间所有栈帧都直接丢弃了。

⚠️ 警告: longjmp不会执行任何清理代码。malloc分配的内存、打开的文件、锁住的互斥量,全都会泄漏。

我在项目中遇到过这样一个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更安全
💡 我的习惯: 如果非要用longjmp,我会在setjmp之后立即注册一个“清理钩子”。说白了,就是维护一个全局的“待释放资源链表”,longjmp跳转前遍历链表释放所有资源。

setjmp/longjmp的知识体系

下面这张图,是我自己总结的setjmp/longjmp核心逻辑。你看一眼就明白了:

setjmp/longjmp 核心逻辑与风险 setjmp(env) 返回0 正常执行路径 longjmp(env, val) 非局部跳转 返回非0值(val) ⚠️ 主要风险 • 资源泄漏(malloc/fopen) • 栈帧损坏 • 未定义行为(局部变量) • 可读性灾难 ✅ 最佳实践 • 跳前释放所有资源 • 使用清理函数包装 • 优先用返回值替代 • 只在错误恢复时使用

局部变量的状态:另一个陷阱

longjmp跳回去之后,局部变量是什么状态?答案是:不一定。

C标准说,如果局部变量是volatile的,那它的值就是longjmp时的值。如果不是volatile,那它的值就是“未指定的”。说白了,编译器可能已经把它优化到寄存器里了,longjmp恢复栈帧时不会恢复寄存器。

🔑 关键点: 在setjmp和longjmp之间可能被修改的局部变量,一定要加volatile修饰。否则你会看到“幽灵值”——变量莫名其妙变回旧值。
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。只有当你真的需要“跨多层函数调用直接跳回”时,才考虑它。而且一旦用了,就要像对待核武器一样——小心翼翼。

💡 最后一个小技巧: 如果你不得不用longjmp,建议在setjmp之后立即设置一个“清理标志”。然后在每个可能分配资源的函数入口检查这个标志。这样即使longjmp跳走了,你也能知道哪些资源需要清理。

好了,关于setjmp/longjmp就聊这么多。记住一句话:非局部跳转,跳的是代码,漏的是资源。用之前,先想清楚你的内存、文件、锁,谁来释放。


专注资料整理