linux c语言编程,使用setjmp和longjmp函数自制类似python的try-catch模块捕获异常,建立栈数据结构,实现多级嵌套使用(2)

上一节的代码,虽然成功的用 C 语言模拟了 python 的 try 功能,但是因为使用全局变量保存现场信息(env),所以有两个致命问题:

  • 覆盖了原有的信号处理函数。
  • 信号处理函数不能传递参数,因此只能使用全局变量,但是这样一来,模拟 try 功能就不能支持嵌套使用了。

本节将尽力解决这两个问题。下面再贴一下上一节的代码。

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

jmp_buf mark;

void sigHandle(int sig)
{
    siglongjmp(mark, -1);
}

int main()
{
    int ret = 0, a = 0;

    signal(SIGFPE, sigHandle);

    ret = sigsetjmp(mark, 1);
    if(0==ret){
        a = 8/0;
        printf("ret == 0 is true\n");   
    }else{
        printf("ERROR: division by zero\n");
    }

    return 0;
}

为什么会覆盖原有的信号处理函数

这个问题真的非常简单,假设之前程序定义和绑定了其他的SIGFPE的信号处理函数。现在为了捕获SIGFPE异常,却需要将SIGFPE的信号处理函数修改为我们的sigHandle函数,这不就覆盖了吗?如果不知道原来的信号处理函数是什么,那么在捕获异常动作结束后,也无法恢复,这就是问题。

为什么用全局变量保存现场(env ),为什么不能支持嵌套调用?

一句话,这是因为 linux 的信号处理函数不接受额外的参数,也无法接收出信号外的其他参数。

请看信号处理函数 sigHandle ,因为无法把除信号外的任何参数传递给它,所以 siglongjmp 的 mark 参数只能通过一些特殊手段传递。比较简单的方法就是使用全局变量。上面的 demo 使用了全局变量 mark 保存现场,这样就无法支持嵌套捕获异常了。举个例子:

...
 ret = sigsetjmp(mark, 1);
    if(0==ret){

         ret2 = sigsetjmp(mark, 1);             // 嵌套保存现场

        a = 8/0;
        printf("ret == 0 is true\n");   
    }else{
        printf("ERROR: division by zero\n");
    }
...

第一次保存的现场,被第二次的 sigsetjmp 函数覆盖了,所以执行 a=8/0出错时,程序会返回到

ret2 = sigsetjmp(mark, 1);              // 嵌套保存现场

这并不是我们期望的。

解决办法

知道了问题的症结所在,就可以解决它了。

  • 记录原信号处理函数,在捕获异常结束后,再改回去。这样就解决了 C 语言模拟 try 功能会覆盖原信号处理函数的问题。
  • 建立多元全局变量,将每次 sigsetjmp 保存的现场环境分开保存,就有望解决因现场被覆盖而不能嵌套调用的问题。

经过上面的分析,现知道不仅要保存现场环境,还需要保存其他信息,所以,可以如下建立结构体(LCC是我的名字缩写,没别的意思):

typedef struct __LCC_TRY
{
    jmp_buf             mark;
    int                 markRet;
    int                 sig;
    struct sigaction    oldAct;
}LCC_TRY;

mark 和 markRet 用于记录 sigsetjmp 保存的现场和返回值,sig 和 oldAct 则用来保存要捕获的信号和该信号原来的信号处理函数,方便之后恢复。

因为要支持多级嵌套使用,所以 LCC_TRY 要建立多个。仔细分析一下,知道其实 sigsetjmp 保存的现场在捕获结束后就没用了,并不需要一直保存。所以建立栈结构的多元 LCC_TRY 数据结构非常合适。

linux c 语言利用循环数组建立环形栈

不多话,直接放出代码:

#include <stdio.h>
#include <setjmp.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include "lcc_try.h"

// ****************
LCC_STACK       g_lcc_try_stack = {0};  // = {0} 就相当于 init 了

void lcc_stack_init(LCC_STACK* s)
{
    s->top = 0;
    s->pushTimes = 0;
    memset(s->tryInfo, 0, sizeof(LCC_STACK));
}

int lcc_stack_push(LCC_STACK* s, LCC_TRY* iTryInfo)
{
    memcpy(&(s->tryInfo[(s->top++)%LCC_TRY_MAX_NEST]), iTryInfo, sizeof(LCC_TRY));
    s->pushTimes = (s->pushTimes<LCC_TRY_MAX_NEST)?(s->pushTimes+1):LCC_TRY_MAX_NEST;
    return 1;
}

int lcc_stack_pop(LCC_STACK* s, LCC_TRY* oTryInfo)
{
    if( 0<s->pushTimes )
        s->pushTimes--;
    else{
        printf("ERROR: pop empty stack\n");
        exit(-1);
    }
    memcpy(oTryInfo, &(s->tryInfo[(--s->top)%LCC_TRY_MAX_NEST]), sizeof(LCC_TRY));
    return 1;
}

void __lcc_SigHandle(int sig)
{
    LCC_TRY tmp = {0};
    lcc_stack_pop(&g_lcc_try_stack, &tmp);  
    siglongjmp(tmp.mark, -1);
}

int lcc_stack_set_try(LCC_STACK* s, LCC_TRY* iTryInfo)
{   
    memcpy(&(s->tryInfo[(s->top)%LCC_TRY_MAX_NEST]), iTryInfo, sizeof(LCC_TRY));
    return 0;
}

int lcc_stack_get_try(LCC_STACK* s, LCC_TRY* oTryInfo)
{
    memcpy(oTryInfo, &(s->tryInfo[(s->top)%LCC_TRY_MAX_NEST]), sizeof(LCC_TRY));
    return 1;
}

代码非常简单,也比较粗糙,抛砖引玉了。建立的不会“满”,当栈保存的数据超过一定的长度,最旧的数据会被顶出栈。LCC_STACK的结构如下定义:

typedef struct  __LCC_STACK
{
    LCC_TRY         tryInfo[LCC_TRY_MAX_NEST];
    int             top;
    unsigned int    pushTimes;  
}LCC_STACK;

C语言模拟python的try-catch功能

既然使用C语言模拟python的 try 功能,当然也要尽量保持语法结构也一致,所以采用了宏定义。下面直接上代码:

// 这里两个细节,一是利用函数形参做局部变量。一是利用 if(a&&b)的 a,b 执行顺序和执行个数。
#define        lcc_try(tryInfo, sigs)                                      \
                sigaction((sigs), NULL, &((tryInfo).oldAct));           \
                signal((sigs), __lcc_SigHandle);                        \
                (tryInfo).sig = (sigs);                                 \
                (tryInfo).markRet = sigsetjmp((tryInfo).mark, 1);       \
                if(0!=(tryInfo).markRet){                               \
                    lcc_stack_get_try(&g_lcc_try_stack, &(tryInfo));    \
                    (tryInfo).markRet = -1;}                            \
                if( (                                                   \
                        (lcc_stack_push(&g_lcc_try_stack, &(tryInfo)))  \
                    )&&                                                 \
                        (0==(tryInfo).markRet) )            

#define        lcc_catch(tryInfo)                                              \
                else if( (lcc_stack_pop(&g_lcc_try_stack, &(tryInfo)))&&    \
                         (lcc_stack_push(&g_lcc_try_stack, &(tryInfo)))&&   \
                         (0!=(tryInfo).markRet) )


#define        lcc_try_end(tryInfo)                                            \
                lcc_stack_pop(&g_lcc_try_stack, &(tryInfo));                \
                signal((tryInfo).sig, (tryInfo).oldAct.sa_handler);

测试

经过宏定义的封装,终于有点像 try 功能了。下面放出一段测试代码:

#include <stdio.h>
#include <signal.h>
#include "pthread.h"
#include "unistd.h"
#include "lcc_try.h"

void* thread(void* param)
{
    LCC_TRY tryInfo;
    char* p = NULL;

    lcc_try(tryInfo, SIGSEGV){
        printf("thread running...\n");
        *p = 1;
    }lcc_catch(tryInfo){
        printf("thread ERROR\n");
    }lcc_try_end(tryInfo);  
}                               

void defHandle(int sig)
{
    printf("recieved SIGSEGV!\n");
    raise(9);
}

int main()
{
    char* buf = NULL;
    int tmp = 0;
    LCC_TRY tryInfo;

    signal(SIGSEGV, defHandle);         // 设定默认的信号处理函数

    putchar('\n');
    lcc_try(tryInfo, SIGSEGV){
        printf("main running...\n");

        lcc_try(tryInfo, SIGFPE){
            printf("inner 1 running...\n");
            tmp = 8/0;
        }lcc_catch(tryInfo){
            printf("inner 1 ERROR\n");
        }lcc_try_end(tryInfo);          

        *buf = NULL;
    }lcc_catch(tryInfo){
        printf("main ERROR\n");
    }lcc_try_end(tryInfo);

    pthread_t pid;
    pthread_create(&pid,NULL,thread,NULL);  
    sleep(1);

    printf("finished\n\n"); 
    *buf = 1;                           // 测试是否恢复为原来的信号处理函数 

    return 0;
}

编译,执行之,得到结果如下:

$ ./a.out 

main running...
inner 1 running...
inner 1 ERROR
main ERROR
thread running...
thread ERROR
finished

recieved SIGSEGV!
Killed

可以看出,我们成功了。嵌套的异常被成功捕获了,线程里的异常也被捕获了。

还有问题吗

当然是有的,既然使用了全局变量,那么就应该考虑在多线程,多进程的情况下的竞争问题。

相信能解决,这篇文章就到这里了。

阅读更多:   Linux笔记
添加新评论

icon_redface.gificon_idea.gificon_cool.gif2016kuk.gificon_mrgreen.gif2016shuai.gif2016tp.gif2016db.gif2016ch.gificon_razz.gif2016zj.gificon_sad.gificon_cry.gif2016zhh.gificon_question.gif2016jk.gif2016bs.gificon_lol.gif2016qiao.gificon_surprised.gif2016fendou.gif2016ll.gif