非本地跳转函数setjmp和longjmp

作者: Leon_Geo | 来源:发表于2020-03-23 16:40 被阅读0次

    C语言提供了一种用户级的异常控制流形式,称为非本地跳转(nonlocal jump),它可以将控制流直接从一个函数转移到另一个当前正在执行的函数,而不需要经过正常的调用和返回序列。而这些都是通过两组函数实现的。

    setjmp函数

    首先来看看函数原型:

    #include <setjmp.h>
    
    int setjmp(jmp_buf env);                        //返回0
    int sigsetjmp(sigjmp_buf env, int savesigs);    //返回0               
    
    • setjmp函数在env缓冲区中保存当前的调用环境(PC值、sp值及通用寄存器),以供后面的longjmp使用,并返回0。

    • setjmp的返回值不能用来赋值给变量,但却可以作为判断语句(switch、if)的条件。

    • sigsetjmp是设计用来被信号处理程序使用的,它的作用和setjmp类似。savesigs用来保存当前环境下的信号状态;

    longjmp函数

    首先来看看函数原型:

    #include <setjmp.h>
    void longjmp(jmp_buf env, int retval);      //从不返回
    void siglongjmp(jmp_buf env, int retval);   //从不返回
    
    • longjmp函数从env缓冲区中恢复最近一次setjmp保存的调用环境,然后控制流跳转到该setjmp函数处,并使函数返回非零的retval。
    • setjmp被调用一次,但可以返回多次;而longjmp被调用后却从不返回,或者说它从最近的setjmp处返回。

    应用

    1. 从深层嵌套中立即返回

    他的意思有点向C语言中的go语句。当程序执行到深层嵌套中的某个函数时,如果此时监测到了一个错误,我们可以利用setjmp和longjmp的组合,直接从最里层返回到上层的错误处理程序,而不需要繁琐的一层层解开调用栈。

    下面展示的例子,说明了上述情况。main函数首先调用setjmp函数保存当前的调用环境,然后再调用函数foo,foo依次调用bar。如果任一函数遇到错误,它们立即通过longjmp调用从setjmp返回。而我们可以通过setjmp的不同非零返回值知道错误类型,而后进入相应的解错程序。

    #include <setjmp.h>
    
    jmp_buf buf;
    
    /*此处我们假设程序可能有两个错误,其中error2会发生;*/
    int error1 = 0;     
    int error2 = 1;
    
    void foo(void),bar(void);   //声明两个函数
    
    int main(void)
    {
        switch(setjmp(buf)){
            case 0:
                foo();
                break;
            case 1:
                printf("Detected error1 in foo.\n");
                break;
            case 2:
                printf("Detected error2 in bar.\n");
                break;
            default:
                printf("Unknown error in foo.\n");           
        }
        exit(0);
    }
    
    void foo(void)
    {
        if(error1)
            longjmp(buf, 1);
        bar();
    }
    
    void bar(void)
    {
        if(error2)
            longjmp(buf, 2);
    }
    

    但在使用这种非本地跳转时要避免跳过的中间过程不会产生副作用;例如,如果我们在foo函数中调用了malloc函数分配了一些内存空间,本来预计在函数结尾处回收它们。但偏偏又在调用bar函数的过程中发生了错误,导致了bar函数直接返回到了最上层,这样就会发生内存泄漏。

    2. 使信号处理分支到达特定位置,而非返回到被中断的指令处

    下面展示了一个虽然被要求终止,但却可以自我重启的程序。程序第一次启动时,对sigsetjmp函数的调用保存了调用环境和信号上下文(待处理的信号位向量pending和被阻塞的信号位向量blocked)。随后,主函数进入无限循环。当用户键入Ctrl + C时,内核向程序发送SIGINT信号,进程捕获后执行我们自定义的信号处理程序,该程序调用siglongjmp函数使得程序跳转到sigsetjmp处返回retval。主程序得以重新开始。

    #include <signal.h>
    
    sigjmp_buf buf;
    
    void handler(int sig)
    {
        siglongjmp(buf, 1);
    }
    
    int main(void)
    {
        if(!sigsetjmp(buf, 1)){
            Signal(SIGINT, handler);
            Unix_exit("starting\n");
        }
        else
            Unix_exit("restarting\n");
        
        while(1){
            sleep(1);
            Unix_exit("processing...\n");
        }
        exit(0);
    }
    
    • 当用户键入Ctrl + C时,程序非本地跳转到开头的sigsetjmp函数处,它会返回1,进而选择执行else语句然后重新进入无限循环。
    • 考虑如果Signal(SIGINT, handler);语句放在sigsetjmp函数前,会有什么风险?

    获取更多知识,请点击关注:
    嵌入式Linux&ARM
    CSDN博客
    简书博客


    相关文章

      网友评论

        本文标题:非本地跳转函数setjmp和longjmp

        本文链接:https://www.haomeiwen.com/subject/qxwwyhtx.html