1,函数介绍

在C中,我们知道可以使用goto实现程序的跳转。但是,goto语句是不能跨越函数的。如果想要跨函数跳转,那么我们就要使用setjmp和longjmp函数了。这两个函数对于处理发生在深层嵌套函数调用中的出错情况非常有用。这两个函数的函数原型如下:

#include <setjmp.h>

int setjmp(jmp_buf env);

void longjmp(jmp_buf env, int val);

通常的用法是:我们在希望返回到的位置调用setjmp,然后在出错的地方调用longjmp返回到setjmp的地方。

setjmp的参数env是一个特殊的类型jmp_buf,这个数据类型一般是某种形式的数组,其中存放着调用setjmp时进程的栈状态信息。等到后面调用longjmp时,就可以使用这个信息来恢复栈之前的状态。因为该变量需要在不同的函数中使用,所以一般都将该变量定义为全局变量。setjmp直接调用时返回值为0,若从longjmp调用返回则返回非0值。其实就是返回longjmp中的第二个参数。

longjmp则相对简单,没有返回值。第一个参数即为setjmp的第一个参数值env,第二个参数为调用longjmp后,setjmp的返回值(因为setjmp直接调用时返回0,所以longjmp得第二个参数不能为0,如果设置为0,那当调用longjmp时,setjmp会返回1,而不是0)。一般一个setjmp会对应多个longjmp,为了区分是从哪个longjmp跳转过来,我们一般讲val设为不同的值。

所以这两个函数的用法也很简单,我们在程序栈正常的地方设置一个“原点”(setjmp),如果某一时刻程序出错,就又跳回(longjmp)到这个“原点”,然后做一些错误处理之类的动作。

2,栈中信息的有效性

当我们利用longjmp返回到setjmp的地方的时候,栈中变量的信息是否还是有效的?或者更准确的说变量的值是第一次调用setjmp时的值(即回滚到原先值)还是最后一个更改的值?

Linux的man手册里面是这样描述的:

The stack context will be invalidated if the function which called setjmp() returns.

其实很多标准里面都是说它们的值是不确定的。所以,我们一般调用longjmp后,就不要再对这些变量的值有所依赖,因为它们很可能是无效的。
但是在《APUE》一书中,有提到这样一个说法:存放在存储器中的变量将具有longjmp时的值,而在CPU和浮点寄存器中的变量则恢复为调用setjmp时的值。我们可以用下面的例子测试一下:

#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>

static void f1(int, int, int, int);
static void f2(void);

static jmp_buf    jmpbuffer;
static int        glob_val;

int main(void)
{
    int                auto_val;
    register int     reg_val;
    volatile int     vola_val;
    static int         stat_val;

    glob_val = 1;
    auto_val = 2;
    reg_val = 3;
    vola_val = 4; 
    stat_val = 5;

    if ( setjmp(jmpbuffer) != 0 )
    {
        printf("After longjmp:n");
        printf("glob_val = %d, auto_val = %d, reg_val = %d, vola_val = %d, stat_val = %dn",
                glob_val, auto_val, reg_val, vola_val, stat_val);
        exit(0);
    }

    /* Change variables after setjmp, but before longjmp */
    glob_val = 91;
    auto_val = 92;
    reg_val = 93;
    vola_val = 94; 
    stat_val = 95;

    f1(auto_val, reg_val, vola_val, stat_val);
    exit(0);
}

static void f1(int i, int j, int k, int l)
{
    printf("In f1():n");
    printf("glob_val = %d, auto_val = %d, reg_val = %d, vola_val = %d, stat_val = %dn",
                glob_val, i, j, k, l);
    f2();
}

static void f2(void)
{
    longjmp(jmpbuffer, 1);
}

运行结果如下:

allan@ubuntu:temp$ gcc jmp.c       # 不进行任何优化的编译
allan@ubuntu:temp$ ./a.out 
In f1():
glob_val = 91, auto_val = 92, reg_val = 93, vola_val = 94, stat_val = 95
After longjmp:
glob_val = 91, auto_val = 92, reg_val = 93, vola_val = 94, stat_val = 95
allan@ubuntu:temp$ gcc -O jmp.c    # 进行全部优化编译
allan@ubuntu:temp$ ./a.out 
In f1():
glob_val = 91, auto_val = 92, reg_val = 93, vola_val = 94, stat_val = 95
After longjmp:
glob_val = 91, auto_val = 2, reg_val = 3, vola_val = 94, stat_val = 95

可以看到,全局、静态和易失变量不受优化的影响,在调用longjmp后,他们的值是最近所呈现的值。但自动变量和寄存器变量却受到了优化的影响。这是因为没有优化的时候,所有的变量都存储在存储器中。而优化后,自动变量和寄存器变量都放在了寄存器中。然后他们的值就被回滚了。所以,可见调用longjmp后,在存储器中的变量的值不会被回滚,但是在CPU和寄存器中的值将会被回滚。所以,我们如果想要变量的值不被回滚,那最好就加上volatile属性。

3,sigsetjmp和siglongjmp

从函数名就可以看出来,这两个函数和信号有关系。函数原型如下:

#include <setjmp.h>

int sigsetjmp(sigjmp_buf env, int savesigs);
void siglongjmp(sigjmp_buf env, int val);

其实这两个函数与setjmp和longjmp之间唯一的区别是sigsetjmp增加了一个参数savesigs,如果该参数值非0,那么调用sigsetjmp就会在env中保存进程的当前信号屏蔽字。之后调用siglongjmp时,就会从env中恢复保存的信号屏蔽字。

本文参考自《AUPE》Second Edition一书。