飛機上面的黑匣子用於飛機失過後對事故的時候調查,同理,程序的黑匣子用於程序崩潰後對崩潰緣由進程定位。其實Linux提供的core dump機制就是一種黑匣子(core文件就是黑匣子文件)。可是core文件並不是在全部場景都適用,由於core文件是程序崩潰時的內存映像,若是程序使用的內存空間比較大,那產生的core文件也將會很是大,在64bit的操做系統中,該現象更爲顯著。可是,其實咱們定位程序崩潰的緣由通常只須要程序掛掉以前的堆棧信息、內存信息等就足夠了。因此有的時候沒有必要使用系統自帶的core文件機制。linux
閱讀本文前,推薦先看一下個人另一篇博客《Linux 的 core 文件》,裏面講解了core文件,並介紹了一些Linux信號的基本知識。ubuntu
程序異常時,每每會產生某種信號,內核會對該信號進行處理。因此設計黑匣子程序的實質就是咱們定義本身的信號處理函數,來代替內核的默認處理。在咱們的信號處理函數中,咱們能夠將咱們想要的信息保存下來(好比程序崩潰時的堆棧信息),以方便後面問題的定位。數組
下面咱們先給出一個我寫的程序,而後邊分析程序邊講具體如何設計一個黑匣子程序:數據結構
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <signal.h> #include <unistd.h> #include <time.h> #include <sys/types.h> #include <execinfo.h> /* 定義一個數據結構用來保存信號 */ typedef struct sigInfo { int signum; char signame[20]; } sigInfo; /* 增長咱們想要捕捉的異常信號,這裏列舉了6個 */ sigInfo sigCatch[] = { {1, "SIGHUP"}, {2, "SIGINT"}, {3, "SIGQUIT"}, {6, "SIGABRT"}, {8, "SIGFPE"}, {11, "SIGSEGV"} }; /* 咱們自定義的信號處理函數 */ void blackbox_handler(int sig) { printf("Enter blackbox_handler: "); printf("SIG name is %s, SIG num is %d\n", strsignal(sig), sig); // 打印堆棧信息 printf("Stack information:\n"); int j, nptrs; #define SIZE 100 void *buffer[100]; char **strings; nptrs = backtrace(buffer, SIZE); printf("backtrace() returned %d addresses\n", nptrs); strings = backtrace_symbols(buffer, nptrs); if (strings == NULL) { perror("backtrace_symbol"); exit(EXIT_FAILURE); } for(j = 0; j < nptrs; j++) printf("%s\n", strings[j]); free(strings); _exit(EXIT_SUCCESS); } /* 有bug的程序,調用該程序,將隨機產生一些異常信號 */ void bug_func() { int rand; struct timeval tpstart; pid_t my_pid = getpid(); // 產生隨機數 gettimeofday(&tpstart, NULL); srand(tpstart.tv_usec); while ((rand = random()) > (sizeof(sigCatch)/sizeof(sigInfo))); printf("rand=%d\n", rand); //隨機產生異常信號 switch(rand % (sizeof(sigCatch)/sizeof(sigInfo))) { case 0: { // SIGHUP kill(my_pid, SIGHUP); break; } case 1: { // SIGINT kill(my_pid, SIGINT); break; } case 2: { // SIGQUIT kill(my_pid, SIGQUIT); break; } case 3: { // SIGABRT abort(); break; } case 4: { // SIGFPE int a = 6 / 0; break; } case 5: { // SIGSEGV kill(my_pid, SIGSEGV); break; } default: return; } } int main() { int i, j; struct sigaction sa; // 初始化信號處理函數數據結構 memset(&sa, 0, sizeof(sa)); sa.sa_handler = blackbox_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; for (i = 0; i < sizeof(sigCatch)/sizeof(sigInfo); i++) { // 註冊信號處理函數 if(sigaction(sigCatch[i].signum, &sa, NULL) < 0) { return EXIT_FAILURE; } } bug_func(); while(1); return EXIT_SUCCESS; }
這裏咱們定義了一個sigInfo的數據結構,用來保存信號。利用這個數據結構咱們能夠將信號值與信號名映射起來。你能夠在你的系統中使用 kill –l 命令去查看他們的對應關係。固然,在程序中,若是獲得了信號值,也可使用Linux提供的API函數strsignal來獲取信號的名字,其函數原型以下:dom
#include <string.h> char *strsignal(int sig);
以後定義了一個全局變量sigCatch來增長咱們想要處理的信號。函數
在main函數裏面,除了調用一些函數外,主要是註冊了一下咱們要處理的信號。其實就是將特定的信號與某個信號處理函數關聯起來。這裏咱們所要捕獲的信號的信號處理函數都是同一個blackbox_handler,由於咱們想在這些信號出現時保存堆棧信息,因此使用同一個函數徹底能夠。這裏須要介紹的是sigaction函數,其函數原型以下:測試
#include <signal.h> int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
使用該函數能夠改變程序默認的信號處理函數。優化
第一個參數signum指明咱們想要改變其信號處理函數的信號值。注意,這裏的信號不能是SIGKILL和SIGSTOP。這兩個信號的處理函數不容許用戶重寫,由於它們給超級用戶提供了終止程序的方法( SIGKILL and SIGSTOP cannot be caught, blocked, or ignored)。ui
第二個和第三個參數是一個struct sigaction的結構體,該結構體在<signal.h>中定義,用來描述信號處理函數。若是act不爲空,則其指向信號處理函數。若是oldact不爲空,則以前的信號處理函數將保存在該指針中。若是act爲空,則以前的信號處理函數不變。咱們能夠經過將act置空,oldact非空來獲取當前的信號處理函數。spa
咱們來看一下這個重要的結構體:
struct sigaction { void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; int sa_flags; void (*sa_restorer)(void); // 該成員如今已廢棄 };
能夠看到,該結構體共有5個成員:
sa_handler是一個函數指針,指向咱們定義的信號處理函數,該值也能夠是SIG_IGN(忽略信號)或者SIG_DEL(使用默認的信號處理函數)。
sa_mask字段說明了一個信號集,信號處理函數執行期間這一信號集要加到進程的信號屏蔽字中。僅當從信號處理函數返回時再將進程的信號屏蔽字復位爲原先的值。這樣在調用信號處理函數時就能阻塞某些信號。在信號處理函數被調用時,操做系統創建的新信號屏蔽字包括正在被遞送的信號。所以保證了在處理一個給定信號時,若是這種信號再次發生,那麼它會被阻塞到對前一個信號的處理結束爲止。
sa_flags字段指定對信號處理的一些選項,經常使用的選項及其含義說明以下(在 <signal.h>中定義):
選項 |
含義 |
SA_INTERRUPT | 由此信號中斷的系統調用不會自動重啓 |
SA_NOCLDSTOP | 若signo是SIGCHLD,當子進程中止(做業控制)時,不產生此信號。當子進程終止時,仍產生此信號(參加SA_NOCLDWAIT說明)。若已設置此標誌,則當中止的進程繼續運行時,做爲XSI擴展,不發送SIGCHLD信號。 |
SA_NOCLDWAIT | 若signo是SIGCHLD,則當調用進程的子進程終止時,不建立殭屍進程。若調用進程在後面調用wait,則調用進程阻塞,直到其全部子進程都終止,此時返回-1,並將errno設置爲ECHILD。 |
SA_NODEFER | 當捕捉到此信號時,在執行其信號處理函數時,系統不自動阻塞此信號(除非sa_mask包括了此信號)。 |
SA_ONSTACK | 若用sigaltstack聲明瞭以替換棧,則將此信號遞送給替換棧上的進程。 |
SA_RESETHAND | 在此信號處理函數的入口處,將此信號的處理方式復位爲SIG_DEF,並清除SA_SIGINFO標誌。可是,不能自動復位SIGILL和SIGTRAP這兩個信號的配置。設置此標誌是sigaction的行爲如同SA_NODEFER標誌也設置了同樣。 |
SA_RESTART | 由此信號中斷的系統調用會自動重啓動。 |
SA_SIGINFO | 此選項對信號處理程序提供了附加信息:一個指向siginfo結構的指針以及一個指向進程上下文標識符的指針。 |
sa_sigaction是一個替代的信號處理函數,當sa_flags字段設置爲SA_SIGINFO時,使用該信號處理函數。須要注意的是,對於sa_sigaction和sa_handler字段,其實現可能使用同一存儲區,因此應用程序只能一次使用這兩個字段中的一個。一般,按以下方式調用信號處理函數:
void handler(int signo);
可是,若是設置了SA_SIGINFO標誌,則按照以下方式調用信號處理函數:
void handler(int signo, siginfo_t *info, void *context);
可見第二種方式比第一種方式多了後面兩個參數。其中第二個參數爲一個siginfo_t結構的指針,該結構描述了信號產生的緣由,該結構通常定義以下:
struct siginfo_t { int si_signo; // signal number int si_errno; // if nonzero, errno value from <errno.h> int si_code; // additional info (depends on signal) pid_t si_pid; // sending process ID uid_t si_uid; // sending process real user ID void *si_addr; // address that cased the fault int si_status; // exit value or signal number long si_band; // band number for SIGPOLL /* possibly other fileds also */ }
通常siginfo_t結構至少包含si_signo和si_code成員。第三個參數context是一個無類型的指針,它能夠被強制轉換爲ucntext_t結構類型,用於標識信號傳遞時進程的上下文。
2.3 信號集
信號種類數目可能超過一個整型量所包含的位數,因此通常而言,不能用整型量中的一位表明一種信號,也就是不能用一個整型量表示信號集(使用信號集能夠表示多個信號)。POSIX.1定義了數據結構sigset_t以包含一個信號集,而且定義了下面5個處理信號集的函數:
#include <signal.h> /* 前四個函數成功返回0,失敗返回-1 */ int sigemptyset(sigset_t *set); int sigfillset(sigset_t *set); int sigaddset(sigset_t *set, int signum); int sigdelset(sigset_t *set, int signum); /* 真返回1,假返回0,出錯返回-1 */ int sigismember(const sigset_t *set, int signum);
每個進程都有一個信號屏蔽字,它規定了當前要阻塞遞送到該進程的信號集。對於每種可能的信號,該屏蔽字中都有一位與之對應。對於某種信號,若其對應爲已設置,則它當前是被阻塞的。進程能夠調用sigprocmask來檢測和更改當前信號的屏蔽字。
函數sigemptyset初始化由set指向的信號集,清除其中全部的信號。函數sigfillset初始化由set指向的信號集,使其包括全部信號。全部應用程序在使用信號集前,要對該信號集調用sigemptyset或sigfillset一次。這是由於C編譯器把未賦初值的外部和靜態變量都初始化爲0. 一旦已經初始化了一個信號集,之後就能夠在該信號集中增、刪特定的信號。函數sigaddset將一個信號添加到現有集中,sigdelset則從信號集中刪除一個信號。
bug_func函數的做用是產生一些異常信號,用於咱們的測試。裏面有兩個注意點:(1)咱們使用微秒數來做爲隨機數種子,這樣產生的僞隨機數分佈會比其餘不少方式更均勻一些。(2)咱們調用了kill函數和abort函數來產生一些信號。其函數原型以下:
#include <signal.h> int kill(pid_t pid, int sig); int raise(int sig); #include <stdlib.h> void abort(void);
kill函數將信號發送給進程或進程組。kill的pid參數有4種不一樣的狀況:
pid>0. 將該信號發送給進程ID爲pid的進程。
pid==0. 將該信號發送給與發送進程屬於同一進程組的全部進程(這些進程的進程組ID等於發送進程的進程組ID),並且發送進程具備向這些進程發送信號的權限。注意,這裏的「全部進程」不包括實現定義的系統進程集。對於大多數UNIX系統,系統進程集包括內核進程以及init(pid等於1)進程。
pid<0. 將該信號發送給其進程組ID等於pid的絕對值,並且發送進程具備向其發送信號的權限。如上所述,「全部進程集」不包括某些系統進程。
pid==-1. 將該信號發送給發送進程有權限向它們發送信號的系統上全部的進程。不包括某些系統進程。
raise函數等價於kill(getpid(), signo).
abort函數會先清除對SIGABRT信號阻塞(若是有阻塞的話),而後調用raise函數向調用進程發送信號。注意:若是abort函數使得進程終止了,那終止前會刷新和關閉全部打開的流。
2.5 backtrace&&backtrace_symbols函數
在黑匣子信號處理函數中咱們使用了backtrace和backtrace_symbols函數來獲取進程崩潰時的堆棧信息。這兩個函數的函數原型以下:
#include <execinfo.h> int backtrace(void **buffer, int size); char **backtrace_symbols(void *const *buffer, int size); void backtrace_symbols_fd(void *const *buffer, int size, int fd);
backtrace函數會返回進程的調用棧信息,並保存在buffer指向的二維數組中;size指明buffer中能夠保存的最大棧幀數目,若是調用棧信息超過了size的值,則只會保存近期的調用棧信息。返回值是保存的棧幀數。
使用backtrace函數獲得調用棧信息後,咱們就可使用backtrace_symbols函數將調用棧的地址信息翻譯爲用符號描述的信息,保存在返回值裏面。須要注意的是咱們只須要定義返回值的指針,其空間由函數backtrace_symbols本身調用maolloc分配,可是使用完之後的空間由咱們負責釋放。
backtrace_symbols_fd沒有返回值,它與backtrace_symbols的不一樣之處在於它會將翻譯的調用棧信息保存在文件裏面。
注意:
使用backtrace函數時,在編譯選項中須要加上 –rdynamic 選項,好比: gcc –rdynamic blackbox.c –o blackbox 。
backtrace_symbols函數會輸出出錯時的16進制的地址,此時咱們可使用addr2line命令將其轉換爲咱們具體的代碼行數,命令格式爲: addr2line –e execute_file addr ,好比 addr2line –e ./a.out 0x400d62 。
在該黑匣子程序中,涉及到了不少Linux信號的知識,以及一些相關的數據結構和API,但願對你們有用。但其實該黑匣子程序在有些極端狀況下仍是有必定的問題,後面咱們會分析並進一步優化。
在前文中,咱們實現了一個黑匣子程序——在進程崩潰後,能夠保存進程的調用棧。可是,在文章結尾咱們說程序有bug,那bug是什麼呢?先看下面一個程序:
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <signal.h> #include <unistd.h> #include <time.h> #include <sys/types.h> #include <execinfo.h> void blackbox_handler(int sig) { printf("Enter blackbox_handler: "); printf("SIG name is %s, SIG num is %d\n", strsignal(sig), sig); // 打印堆棧信息 printf("Stack information:\n"); int j, nptrs; #define SIZE 100 void *buffer[100]; char **strings; nptrs = backtrace(buffer, SIZE); printf("backtrace() returned %d addresses\n", nptrs); strings = backtrace_symbols(buffer, nptrs); if (strings == NULL) { perror("backtrace_symbol"); exit(EXIT_FAILURE); } for(j = 0; j < nptrs; j++) printf("%s\n", strings[j]); free(strings); _exit(EXIT_SUCCESS); } long count = 0; void bad_iter() { int a, b, c, d; a = b = c = d = 1; a = b + 3; c = count + 4; d = count + 5 * c; count++; printf("count:%ld\n", count); bad_iter(); } int main() { struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_handler = blackbox_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; if (sigaction(SIGSEGV, &sa, NULL) < 0) { return EXIT_FAILURE; } bad_iter(); while(1); return EXIT_SUCCESS; }
該程序的執行結果以下:
... ... count:261856 count:261857 count:261858 count:261859 count:261860 count:261861 Segmentation fault (core dumped) allan@ubuntu:temp$ |
該程序是一種極端狀況:咱們的程序中使用了無線層次的遞歸函數,致使棧空間被用盡,此時會產生SIGSEGV信號。可是從輸出看,並無走到咱們的信號處理函數裏面。這是由於但因爲棧空間已經被用完,因此咱們的信號處理函數是無法被調用的,這種狀況下,咱們的黑匣子程序是無法捕捉到異常的。
可是該問題也很好解決,咱們能夠爲咱們的信號處理函數在堆裏面分配一塊內存做爲「可替換信號棧」。
使用可替換棧優化後的程序以下:
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <signal.h> #include <unistd.h> #include <time.h> #include <sys/types.h> #include <execinfo.h> void blackbox_handler(int sig) { printf("Enter blackbox_handler: "); printf("SIG name is %s, SIG num is %d\n", strsignal(sig), sig); // 打印堆棧信息 printf("Stack information:\n"); int j, nptrs; #define SIZE 100 void *buffer[100]; char **strings; nptrs = backtrace(buffer, SIZE); printf("backtrace() returned %d addresses\n", nptrs); strings = backtrace_symbols(buffer, nptrs); if (strings == NULL) { perror("backtrace_symbol"); exit(EXIT_FAILURE); } for(j = 0; j < nptrs; j++) printf("%s\n", strings[j]); free(strings); _exit(EXIT_SUCCESS); } long count = 0; void bad_iter() { int a, b, c, d; a = b = c = d = 1; a = b + 3; c = count + 4; d = count + 5 * c; count++; printf("count:%ld\n", count); bad_iter(); } int main() { stack_t ss; struct sigaction sa; ss.ss_sp = malloc(SIGSTKSZ); ss.ss_size = SIGSTKSZ; ss.ss_flags = 0; if (sigaltstack(&ss, NULL) == -1) { return EXIT_FAILURE; } memset(&sa, 0, sizeof(sa)); sa.sa_handler = blackbox_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_ONSTACK; if (sigaction(SIGSEGV, &sa, NULL) < 0) { return EXIT_FAILURE; } bad_iter(); while(1); return EXIT_SUCCESS; }
編譯 gcc –rdynamic blackbox_overflow.c 後運行,輸出爲:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
... ... count:261989 count:261990 count:261991 count:261992 Enter blackbox_handler: SIG name is Segmentation fault, SIG num is 11 Stack information: backtrace() returned 100 addresses ./a.out(blackbox_handler+0x63) [0x400c30] /lib/x86_64-linux-gnu/libc.so.6(+0x36ff0) [0x7f6e68d74ff0] /lib/x86_64-linux-gnu/libc.so.6(_IO_file_write+0xb) [0x7f6e68db7e0b] /lib/x86_64-linux-gnu/libc.so.6(_IO_do_write+0x7c) [0x7f6e68db931c] /lib/x86_64-linux-gnu/libc.so.6(_IO_file_xsputn+0xb1) [0x7f6e68db84e1] /lib/x86_64-linux-gnu/libc.so.6(_IO_vfprintf+0x7fa) [0x7f6e68d8879a] /lib/x86_64-linux-gnu/libc.so.6(_IO_printf+0x99) [0x7f6e68d92749] ./a.out(bad_iter+0x7a) [0x400d62] ./a.out(bad_iter+0x84) [0x400d6c] ./a.out(bad_iter+0x84) [0x400d6c] ./a.out(bad_iter+0x84) [0x400d6c] ./a.out(bad_iter+0x84) [0x400d6c] ./a.out(bad_iter+0x84) [0x400d6c] ... ... |
能夠看到,使用可替換棧之後,雖然一樣棧溢出了,可是咱們的黑匣子程序仍是起做用了。因此這種優化是有效的。下面咱們來看優化的代碼。
能夠看到咱們的代碼中使用了sigaltstack函數,該函數的做用就是在在堆中爲函數分配一塊區域,做爲該函數的棧使用。因此,雖然遞歸函數將系統默認的棧空間用盡了,可是當調用咱們的信號處理函數時,使用的棧是它實如今堆中分配的空間,而不是系統默認的棧,因此它仍舊能夠正常工做。
該函數函數原型以下:
#include <signal.h> int sigaltstack(const stack_t *ss, stack_t *oss);
該函數兩個個參數爲均爲stack_t類型的結構體,先來看下這個結構體:
typedef struct { void *ss_sp; /* Base address of stack */ int ss_flags; /* Flags */ size_t ss_size; /* Number of bytes in stack */ }
若是想要禁用已存在的一個可替換信號棧,可將ss_flags設置爲SS_DISABLE。要想建立一個新的可替換信號棧,ss_flags必須設置爲0,ss_sp和ss_size分別指明可替換信號棧的起始地址和棧大小。系統定義了一個常數SIGSTKSZ,該常數對極大多數可替換信號棧來講均可以知足需求,MINSIGSTKSZ規定了可替換信號棧的最小值。
而sigaltstack第一個參數爲建立的新的可替換信號棧,第二個參數能夠設置爲NULL,若是不爲NULL的話,將會將舊的可替換信號棧的信息保存在裏面。函數成功返回0,失敗返回-1.
通常來講,使用可替換信號棧的步驟以下:
在內存中分配一塊區域做爲可替換信號棧
使用sigaltstack()函數通知系統可替換信號棧的存在和內存地址
使用sigaction()函數創建信號處理函數的時候,經過將sa_flags設置爲SA_ONSTACK來告訴系統信號處理函數將在可替換信號棧上面運行。
sig_handler.h
#ifndef __SIG_HANDLER_H_ #define __SIG_HANDLER_H_ #include <stdio.h> #include <string.h> #include <stdlib.h> #include <signal.h> #include <unistd.h> #include <time.h> #include <sys/types.h> #include <execinfo.h> /* 定義一個數據結構用來保存信號 */ typedef struct sig_info { int signum; char signame[20]; } sig_info_t; /* 咱們自定義的信號處理函數 */ extern void blackbox_handler(int sig); extern void registe_sig_handler(); #endif
sig_handler.c
其中日誌輸出內容部分須要改爲本身的IO輸出
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <signal.h> #include <unistd.h> #include <time.h> #include <sys/types.h> #include <execinfo.h> #include "config.h" //日誌相關 #include "sig_handler.h" //日誌相關 #include "log.h" //日誌相關 /* 增長咱們想要捕捉的異常信號,這裏列舉了6個 */ sig_info_t sigCatch[] = { {1, "SIGHUP"}, {2, "SIGINT"}, {3, "SIGQUIT"}, {6, "SIGABRT"}, {8, "SIGFPE"}, {11, "SIGSEGV"} }; void blackbox_handler(int sig) { log_write(CONF.lf,LOG_INFO,"Enter blackbox_handler: "); log_write(CONF.lf,LOG_INFO,"SIG name is %s, SIG num is %d\n", strsignal(sig), sig); int j, nptrs; #define SIZE 100 void *buffer[100]; char **strings; nptrs = backtrace(buffer, SIZE); log_write(CONF.lf,LOG_INFO,"backtrace() returned %d addresses\n", nptrs); strings = backtrace_symbols(buffer, nptrs); if (strings == NULL) { log_write(CONF.lf,LOG_INFO,"backtrace_symbol"); exit(EXIT_FAILURE); } for(j = 0; j < nptrs; j++) log_write(CONF.lf,LOG_INFO,"%s\n", strings[j]); log_write(CONF.lf,LOG_INFO,"server exit unormal"); free(strings); _exit(EXIT_SUCCESS); } void registe_sig_handler(){ int i=0; struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_handler = blackbox_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; //register sig for (i = 0; i < sizeof(sigCatch)/sizeof(sig_info_t); i++) { // 註冊信號處理函數 if(sigaction(sigCatch[i].signum, &sa, NULL) < 0) { return EXIT_FAILURE; } } }