在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这个函数绑定
自定义的信号处理函数
那么我们就可以开始尝试使用它了
- #include <signal.h>
- #include <setjmp.h>
- #include <stdarg.h>
- #include <stdlib.h>
- #include <stdio.h>
- //信号处理函数
- void recvSignal(int sig)
- {
- printf("received signal %d !!!\n",sig);
- }
- int main(int argc,char** argv)
- {
- //给信号注册一个处理函数
- signal(SIGSEGV, recvSignal);
- int* s = 0;
- (*s) = 1;
- //以上两句用来产生 一个 传说中的段错误
- while(1)
- {
- sleep(1);
- printf("sleep 1 \n");
- }
- return 0;
- }
编译运行 一直打印收到信号 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的返回值 我们就可以决定执行可能发生错误的代码还是直接跳过这段代码 。知道了原理之后 我们可能就会这样写
- #include <signal.h>
- #include <setjmp.h>
- #include <stdarg.h>
- #include <stdlib.h>
- #include <stdio.h>
- jmp_buf env;
- //信号处理函数
- void recvSignal(int sig)
- {
- printf("received signal %d !!!\n",sig);
- longjmp(env,1);
- }
- int main(int argc,char** argv)
- {
-
- //保存一下上下文
- int r = setjmp(env);
- if( r == 0)
- {
- //初次执行 ,那么可以执行 可能会发生错误的代码
- //给信号注册一个处理函数
- signal(SIGSEGV, recvSignal);
- printf("excute this code!!");
- int* s = 0;
- (*s) = 1;
- }
- else
- {
- //是由longjmp 跳转回来的
- printf("jump this code !!");
- }
- while(1)
- {
- sleep(1);
- printf("sleep 1 \n");
- }
- return 0;
- }
编译 ,执行 产生 SIGSEGV 信号 ,然后在信号函数 里边跳转 到 int r = setjmp(env); 这一行 ,之后 直接略过了 可能发生错误的这段代码 ,跳转生效,可是这种方式还有一个bug,我们看看下面的代码
- #include <signal.h>
- #include <setjmp.h>
- #include <stdarg.h>
- #include <stdlib.h>
- #include <stdio.h>
- jmp_buf env;
- //信号处理函数
- void recvSignal(int sig)
- {
- printf("received signal %d !!!\n",sig);
- longjmp(env,1);
- }
- int main(int argc,char** argv)
- {
-
- for(int i = 0; i < 2; i++)
- {
- //保存一下上下文
- int r = setjmp(env);
- if( r == 0)
- {
- //初次执行 ,那么可以执行 可能会发生错误的代码
- //给信号注册一个处理函数
- signal(SIGSEGV, recvSignal);
- printf("excute this code!!");
- int* s = 0;
- (*s) = 1;
- }
- else
- {
- //是由longjmp 跳转回来的
- printf("jump this code !!");
- }
- sleep(5);
- }
-
- while(1)
- {
- sleep(1);
- printf("sleep 1 \n");
- }
- return 0;
- }
当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 跳转的时候 会恢复 线程的 屏蔽表。
于是我们把上面的代码修改 后如下:
- #include <signal.h>
- #include <setjmp.h>
- #include <stdarg.h>
- #include <stdlib.h>
- #include <stdio.h>
- // jmp_buf env;
- //信号处理函数
- void recvSignal(int sig)
- {
- printf("received signal %d !!!\n",sig);
- siglongjmp(env,1);
- }
- int main(int argc,char** argv)
- {
-
- for(int i = 0; i < 2; i++)
- {
- //保存一下上下文
- int r = sigsetjmp(env,1);
- if( r == 0)
- {
- //初次执行 ,那么可以执行 可能会发生错误的代码
- //给信号注册一个处理函数
- signal(SIGSEGV, recvSignal);
- printf("excute this code!!");
- int* s = 0;
- (*s) = 1;
- }
- else
- {
- //是由longjmp 跳转回来的
- printf("jump this code !!");
- }
- sleep(5);
- }
-
- while(1)
- {
- sleep(1);
- printf("sleep 1 \n");
- }
- return 0;
- }
编译后 运行 。按照我们的需求 第二次进入for循环时, 发生段错误后程序不会死掉 ,而是会跳过这段代码了继续往下走 。下面我做了一个简单的封装 ,在错误发生时,我打印出了 错误信息 ,然后跳过错误的代码
- /*
- ** file name CException.h
- */
- #ifndef _CEXCEPTION_H_
- #define _CEXCEPTION_H_
- #include <setjmp.h>
- #include <stdlib.h>
- #include <stdarg.h>
- #include <execinfo.h>
- #include <stdio.h>
- #include <signal.h>
- #include <iostream>
- #include <string.h>
- typedef struct Except_frame
- {
- jmp_buf env;
- int flag;
- void clear()
- {
- flag = 0;
- bzero(env,sizeof(env));
- }
- bool isDef()
- {
- return flag;
- }
- Except_frame()
- {
- clear();
- }
- }Except_frame;
- extern Except_frame* except_stack;
- extern void errorDump();
- extern void recvSignal(int sig);
- Except_frame* except_stack = new Except_frame;
- void errorDump()
- {
- const int maxLevel = 200;
- void* buffer[maxLevel];
- int level = backtrace(buffer, maxLevel);
- const int SIZE_T = 1024;
- char cmd[SIZE_T] = "addr2line -C -f -e ";
- char* prog = cmd + strlen(cmd);
- readlink("/proc/self/exe", prog, sizeof(cmd) - (prog-cmd)-1);
- FILE* fp = popen(cmd, "w");
- if (!fp)
- {
- perror("popen");
- return;
- }
- for (int i = 0; i < level; ++i)
- {
- fprintf(fp, "%p\n", buffer[i]);
- }
- fclose(fp);
- }
-
- void recvSignal(int sig)
- {
- printf("received signal %d !!!\n",sig);
- errorDump();
- siglongjmp(except_stack->env,1);
- }
- #define TRY \
- except_stack->flag = sigsetjmp(except_stack->env,1);\
- if(!except_stack->isDef()) \
- { \
- signal(SIGSEGV,recvSignal); \
- printf("start use TRY\n");
- #define END_TRY \
- }\
- else\
- {\
- except_stack->clear();\
- }\
- printf("stop use TRY\n");
- #define RETURN_NULL \
- } \
- else \
- { \
- except_stack->clear();\
- }\
- return NULL;
- #define RETURN_PARAM { \
- except_stack->clear();\
- }\
- return x;
- #define EXIT_ZERO \
- }\
- else \
- { \
- except_stack->clear();\
- }\
- exit(0);
- #endif
另外建一个文件 ,- #include "CException.h"
- int main(int argc,char** argv)
- {
- //可以如下使用
- TRY
- int*s = 0;
- (int*s) = 1;
- END_TRY
- //使用这两个宏包含可能发生的错误代码 ,当然可以根据需求 使用
- //RETURN_NULL
- //RETURN_PARAM(0)
- //EXIT_ZERO 这三个宏
- return 0;
- }
这个时候我们就能使用TRY 和 END_TRY,RETURM_NULL,RETURN_PARAM(param) 来实现程序发生段错误后跳过错误代码继续运行了 ,不过此代码仅限于单线程使用