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恢复上下文时,寄存器被还原了,但内存里的值没变。这就导致你拿到的值「看运气」。

⚠️ 重要警告: 在setjmp和longjmp之间被修改的自动变量,如果不加volatile,其值在longjmp之后是未定义的。这不是bug,是标准行为。

正确的做法是:要么把变量声明为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跳走了,这个文件描述符的状态就乱了。

💡 我的建议: 如果非要在信号处理里用longjmp,请确保:
1. 信号处理函数只做最简单的操作——设置标志位,然后longjmp。
2. 不要嵌套信号,在longjmp之前屏蔽其他信号。
3. 跳回去之后,立即检查并恢复所有可能被中断的资源(文件描述符、锁等)。

18.4 避坑指南——我踩过的那些坑

我曾经在一个嵌入式项目里,用setjmp/longjmp实现了一个「任务调度器」。想法很美好:每个任务用setjmp保存上下文,时间片到了就longjmp跳回调度器。结果呢?

  • 坑1: 忘了给自动变量加volatile,任务切换后数据全乱了。排查了两天,最后发现是编译器优化把变量放寄存器里了。
  • 坑2: 在中断服务函数里调用了longjmp,结果中断嵌套导致栈乱了。从那以后,我规定团队:中断里绝对不允许用longjmp。
  • 坑3: longjmp跳回去之后,malloc分配的内存没释放,造成了内存泄漏。因为longjmp不会调用任何析构函数或清理代码。

核心原则:

  1. setjmp和longjmp之间被修改的自动变量,必须加volatile。
  2. 信号处理函数中慎用longjmp,非用不可时,确保信号被屏蔽。
  3. longjmp不会释放资源(堆内存、文件描述符、锁等),跳转前要手动清理。
  4. 不要跨不同栈层级多次longjmp,容易导致栈混乱。
  5. setjmp的返回值只能用于判断是首次调用还是longjmp跳回,不要做其他算术运算。

18.5 知识体系图

下面这张图总结了setjmp/longjmp的核心知识点和它们之间的关系:

setjmp/longjmp 知识体系 setjmp / longjmp 基本用法 setjmp首次返回0 longjmp跳回,返回非0值 longjmp第二个参数不能为0 自动变量陷阱 值可能不确定 加volatile解决 或用static变量 信号处理交互 信号处理中慎用longjmp 可重入性问题 屏蔽其他信号再跳转 核心原则:volatile + 资源清理 + 信号安全 setjmp/longjmp是强大工具,但用错代价很大

18.6 总结

setjmp和longjmp是C语言里的一把「双刃剑」。用好了,可以实现优雅的错误恢复和协程调度;用不好,就是一场灾难。

我个人对它们的看法是:能不用就不用,非用不可时,严格遵守规则。 尤其是自动变量的volatile声明和信号处理中的使用限制,这两条是红线,踩了就是未定义行为。

你想想看,一个longjmp跳回去,变量值不对了,资源没释放,信号处理乱套了——这种bug查起来,真的是欲哭无泪。我在项目里吃过亏,所以现在看到setjmp/longjmp,都会多留个心眼。

一句话记住: setjmp/longjmp不是goto,它跳的是「执行上下文」,不是「代码行」。尊重它的规则,它才能为你所用。


公众号:蓝海资料掘金营,微信deep3321