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 跳轉的時候 會恢復 線程的 屏蔽表。
於是我們把上面的代碼修改 后如下:
 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) 來實現程序發生段錯誤后跳過錯誤代碼繼續運行了 ,不過此代碼僅限於單線程使用

 

from:http://blog.csdn.net/work_msh/article/details/8470277


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM