linux SIGSEGV 信号捕捉,保证发生段错误后程序不崩溃

        在linux中编程的时候 有时候 try catch 可能满足不了我们的需求。因为碰到类似数组越界 ,非法内存访问之类的 ,这样的错误无法捕获。下面我们介绍一种使用捕获信号实现的异常 用来保证诸如段错误之类的错误发生时程序不会崩溃,而是跳过代码继续执行。首先我们来看看发生段错误之后系统的处理。

        发生段错误后系统会抛出 SIGSEGV 信号 ,之后 调用默认的信号处理函数 ,产生core文件 ,然后关闭程序 。

        那有没有一种办法可以保证程序不会死掉呢,当然是有的 。首先我们想到的是 截获改信号,调用自己的信号处理函数 。

        让我们来看看signal 这个函数 。

        #include <signal.h>

       typedef void (*sighandler_t)(int);

       sighandler_t signal(int signum, sighandler_t handler);

        第一个参数 的意思表示你要绑定的信号 (可以使用在控制台使用 kill -l 查看都有哪些信号 ,这些就不讲了,有兴趣的可以上网查)

        第二个参数 是表示信号处理的函数 指针 ,返回值为void* 参数为int ,如上 ,另外 系统也定义了一些宏  (SIG_IGN,和 SIG_DFL) 第一个表示忽略这个信号 ,第二个表示 使用默认的信号处理函数 如果我们处理的       是SIGSEGV信号 ,那么它就会产生core文件 等等操作  

        返回值是一个信号处理函数的指针 ,如果发生错误 返回 SIG_ERR 这个宏 ,事实上 也是定义的一个函数 产生错误的原因 主要是因为给定的信号不正确 

另外这个使用函数 有两点要注意 

   1. 进入到信号处理函数之后 这个信号会被 阻塞(block) 直到信号处理函数 返回 这点非常重要 ,后面会讲到。

   2. 信号函数处理完之后,会将该信号恢复为默认处理状态 ,即重新与产生core文件...函数绑定,所以在下一次用到的时候要重新调用signal这个函数绑定

       自定义的信号处理函数

  1. #include <signal.h>  
  2. #include <setjmp.h>  
  3. #include <stdarg.h>  
  4. #include <stdlib.h>  
  5. #include <stdio.h>  
  6. //信号处理函数  
  7. void recvSignal(int sig)  
  8. {  
  9.        printf("received signal %d !!!\n",sig);  
  10. }  
  11. int main(int argc,char** argv)  
  12. {  
  13.        //给信号注册一个处理函数   
  14.        signal(SIGSEGV, recvSignal);  
  15.        int* s = 0;  
  16.        (*s) = 1;  
  17.        //以上两句用来产生 一个 传说中的段错误  
  18.        while(1)  
  19.       {  
  20.             sleep(1);  
  21.              printf("sleep 1 \n");  
  22.        }  
  23.        return 0;  
  24. }  


 

        编译运行  一直打印收到信号 11 (SIGSEGV),为什么呢 ,

        上面代码给SIGSEGV 这个信号注册了一个处理函数 ,替代了系统默认的产生core文件的处理函数 ,当错误发生后 ,系统 发送 SIGSEGV ,然后 中断了程序 跳到 recvSignal 处理函数中去 ,处理完成后 ,再跳回来错误发生的地方 ,然后继续产生错误 ,继续发送 SIGSEGV  信号 ... 

        使用 setjmp 和longjmp 尝试跳过错误堆栈  

        #include <setjmp.h>

         int setjmp(jmp_buf env);   void longjmp(jmp_buf env, int val);

        系统跳转函数 ,可以直接在函数之间跳转 (比goto 强大多了) 

        int setjmp(jmp_buf env);  这个函数 将上下文 ,就是cpu和内存的信息保存到env中 (不用去理解 jmp_buf,就当我们平时用的buff好了),然后调用 void longjmp(jmp_buf env, int val); 的时候 跳转到使用env中的信息 ,恢复上下文 。如果是第一回调用setjmp 它会返回 0,如果是在 从longjmp 跳转过来的 ,那就返回 longjmp的参数 val,根据setjmp的返回值 我们就可以决定执行可能发生错误的代码还是直接跳过这段代码 。知道了原理之后 我们可能就会这样写 

  1. #include <signal.h>  
  2. #include <setjmp.h>  
  3. #include <stdarg.h>  
  4. #include <stdlib.h>  
  5. #include <stdio.h>  
  6. jmp_buf env;  
  7. //信号处理函数  
  8. void recvSignal(int sig)  
  9. {  
  10.         printf("received signal %d !!!\n",sig);  
  11.         longjmp(env,1);  
  12. }  
  13. int main(int argc,char** argv)  
  14. {  
  15.   
  16.     //保存一下上下文   
  17.     int r = setjmp(env);  
  18.     if(  r  == 0)  
  19.     {  
  20.             //初次执行 ,那么可以执行 可能会发生错误的代码  
  21.             //给信号注册一个处理函数    
  22.             signal(SIGSEGV, recvSignal);  
  23.             printf("excute this code!!");  
  24.             int* s = 0;  
  25.             (*s) = 1;  
  26.     }  
  27.     else  
  28.     {  
  29.             //是由longjmp 跳转回来的  
  30.             printf("jump this code !!");   
  31.     }  
  32.     while(1)  
  33.     {  
  34.         sleep(1);  
  35.         printf("sleep 1 \n");  
  36.     }  
  37.     return 0;  
  38. }  

编译 ,执行  产生 SIGSEGV 信号 ,然后在信号函数 里边跳转 到  int r = setjmp(env); 这一行 ,之后 直接略过了 可能发生错误的这段代码 ,跳转生效,可是这种方式还有一个bug,我们看看下面的代码 

  1. #include <signal.h>  
  2. #include <setjmp.h>  
  3. #include <stdarg.h>  
  4. #include <stdlib.h>  
  5. #include <stdio.h>  
  6. jmp_buf env;  
  7. //信号处理函数  
  8. void recvSignal(int sig)  
  9. {  
  10.         printf("received signal %d !!!\n",sig);  
  11.         longjmp(env,1);  
  12. }  
  13. int main(int argc,char** argv)  
  14. {  
  15.   
  16.     for(int i = 0; i < 2; i++)  
  17.     {  
  18.             //保存一下上下文   
  19.             int r = setjmp(env);  
  20.             if(  r  == 0)  
  21.             {  
  22.                    //初次执行 ,那么可以执行 可能会发生错误的代码  
  23.                   //给信号注册一个处理函数    
  24.                   signal(SIGSEGV, recvSignal);  
  25.                   printf("excute this code!!");  
  26.                   int* s = 0;  
  27.                   (*s) = 1;  
  28.         }  
  29.         else  
  30.         {  
  31.                   //是由longjmp 跳转回来的  
  32.                   printf("jump this code !!");   
  33.         }  
  34.         sleep(5);  
  35.     }  
  36.   
  37.     while(1)  
  38.     {  
  39.            sleep(1);  
  40.            printf("sleep 1 \n");  
  41.     }  
  42.     return 0;  
  43. }  

当for循环第二次执行的时候 ,程序依然产生了 SIGSEGV,系统仍然调用了默认的处理函数产生了core文件 ,分析下原因 上面我们说过“进入到信号处理函数之后 这个信号会被 阻塞(block) 直到信号处理函数返回”,在进入到信号处理函数之后 ,这个时候 系统阻塞了 SIGSEGV 这个信号 ,当跳回到 int r = setjmp(env); 这行代码的时候  SIGSEGV 信号依然是阻塞的 ,那以后 再给他绑定信号处理函数 自然没有作用 。

好在系统给我们提供了int sigsetjmp(sigjmp_buf env, int savesigs);和  void siglongjmp(sigjmp_buf env, int val);这两个函数 ,这两个函数 和上面的 int setjmp(jmp_buf env);   void longjmp(jmp_buf env, int val); 大同小异 ,唯一的不同 是sigsetjmp 函数 多了 一个参数 ,savesigs,查看这函数的说明可以知道 ,当 savesigs 不为 0时,会保存当前的信号屏蔽表 (signal mask),然后在使用siglongjmp 跳转的时候 会恢复 线程的 屏蔽表。

于是我们把上面的代码修改 后如下:

[cpp] view plain copy

  1. #include <signal.h>  
  2. #include <setjmp.h>  
  3. #include <stdarg.h>  
  4. #include <stdlib.h>  
  5. #include <stdio.h>  
  6. // jmp_buf env;  
  7. //信号处理函数  
  8. void recvSignal(int sig)  
  9. {  
  10. printf("received signal %d !!!\n",sig);  
  11.         siglongjmp(env,1);  
  12. }  
  13. int main(int argc,char** argv)  
  14. {  
  15.   
  16.     for(int i = 0; i < 2; i++)  
  17.     {  
  18.             //保存一下上下文   
  19.         int r = sigsetjmp(env,1);  
  20.         if(  r  == 0)  
  21.         {  
  22.             //初次执行 ,那么可以执行 可能会发生错误的代码  
  23.             //给信号注册一个处理函数    
  24.             signal(SIGSEGV, recvSignal);  
  25.             printf("excute this code!!");  
  26.                int* s = 0;  
  27.                 (*s) = 1;  
  28.         }  
  29.         else  
  30.         {  
  31.                 //是由longjmp 跳转回来的  
  32.                 printf("jump this code !!");   
  33.         }  
  34.         sleep(5);  
  35.     }  
  36.   
  37.     while(1)  
  38.     {  
  39.         sleep(1);  
  40.         printf("sleep 1 \n");  
  41.     }  
  42.     return 0;  
  43. }  

编译后 运行 。按照我们的需求 第二次进入for循环时, 发生段错误后程序不会死掉 ,而是会跳过这段代码了继续往下走 。下面我做了一个简单的封装 ,在错误发生时,我打印出了 错误信息 ,然后跳过错误的代码 

  1. /* 
  2. ** file name CException.h 
  3. */  
  4. #ifndef _CEXCEPTION_H_  
  5. #define _CEXCEPTION_H_  
  6. #include <setjmp.h>  
  7. #include <stdlib.h>  
  8. #include <stdarg.h>  
  9. #include <execinfo.h>  
  10. #include <stdio.h>  
  11. #include <signal.h>  
  12. #include <iostream>  
  13. #include <string.h>  
  14. typedef struct Except_frame  
  15. {  
  16.     jmp_buf env;  
  17.     int flag;  
  18.     void clear()  
  19.     {  
  20.        flag = 0;  
  21.        bzero(env,sizeof(env));  
  22.     }  
  23.     bool isDef()  
  24.     {  
  25.        return flag;  
  26.     }  
  27.     Except_frame()  
  28.     {  
  29.       clear();  
  30.     }  
  31. }Except_frame;  
  32. extern Except_frame* except_stack;  
  33. extern void errorDump();  
  34. extern void recvSignal(int sig);  
  35. Except_frame* except_stack = new Except_frame;  
  36. void errorDump()  
  37. {  
  38.     const int maxLevel = 200;  
  39.     void* buffer[maxLevel];  
  40.     int level = backtrace(buffer, maxLevel);  
  41.     const int SIZE_T = 1024;  
  42.     char cmd[SIZE_T] = "addr2line -C -f -e ";  
  43.     char* prog = cmd + strlen(cmd);  
  44.     readlink("/proc/self/exe", prog, sizeof(cmd) - (prog-cmd)-1);  
  45.     FILE* fp = popen(cmd, "w");  
  46.     if (!fp)  
  47.     {  
  48.         perror("popen");  
  49.         return;  
  50.     }  
  51.     for (int i = 0; i < level; ++i)  
  52.     {  
  53.         fprintf(fp, "%p\n", buffer[i]);  
  54.     }  
  55.     fclose(fp);  
  56. }  
  57.   
  58. void recvSignal(int sig)  
  59. {  
  60.     printf("received signal %d !!!\n",sig);  
  61.     errorDump();  
  62.     siglongjmp(except_stack->env,1);  
  63. }  
  64. #define TRY \  
  65.     except_stack->flag = sigsetjmp(except_stack->env,1);\  
  66.     if(!except_stack->isDef()) \  
  67.     { \  
  68.       signal(SIGSEGV,recvSignal); \  
  69.       printf("start use TRY\n");  
  70. #define END_TRY \  
  71.     }\  
  72.     else\  
  73.     {\  
  74.       except_stack->clear();\  
  75.     }\  
  76.     printf("stop use TRY\n");  
  77. #define RETURN_NULL \  
  78.     } \  
  79.     else \  
  80.     { \  
  81.       except_stack->clear();\  
  82.     }\  
  83.     return NULL;  
  84. #define RETURN_PARAM  { \  
  85.       except_stack->clear();\  
  86.     }\  
  87.     return x;  
  88. #define EXIT_ZERO \  
  89.     }\  
  90.     else \  
  91.     { \  
  92.       except_stack->clear();\  
  93.     }\  
  94.     exit(0);  
  95. #endif  


另外建一个文件 ,

  1. #include "CException.h"  
  2. int main(int argc,char** argv)  
  3. {  
  4.     //可以如下使用   
  5.     TRY  
  6.         int*s = 0;  
  7.         (int*s) = 1;  
  8.     END_TRY  
  9.     //使用这两个宏包含可能发生的错误代码 ,当然可以根据需求 使用   
  10.     //RETURN_NULL   
  11.     //RETURN_PARAM(0)  
  12.     //EXIT_ZERO  这三个宏  
  13.     return 0;  
  14. }  



这个时候我们就能使用TRY 和 END_TRY,RETURM_NULL,RETURN_PARAM(param) 来实现程序发生段错误后跳过错误代码继续运行了 ,不过此代码仅限于单线程使用

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值