1. 何为黑匣子程序及其必要性
飞机上面的黑匣子用于飞机失事后对事故的时候调查,同理,程序的黑匣子用于程序崩溃后对崩溃原因进程定位。其实Linux提供的core dump机制就是一种黑匣子(core文件就是黑匣子文件)。但是core文件并非在所有场景都适用,因为core文件是程序崩溃时的内存映像,如果程序使用的内存空间比较大,那产生的core文件也将会非常大,在64bit的操作系统中,该现象更为显著。但是,其实我们定位程序崩溃的原因一般只需要程序挂掉之前的堆栈信息、内存信息等就足够了。所以有的时候没有必要使用系统自带的core文件机制。
阅读本文前,推荐先看一下我的另外一篇博客《Linux 的 core 文件》,里面讲解了core文件,并介绍了一些Linux信号的基本知识。
2. 黑匣子程序设计
程序异常时,往往会产生某种信号,内核会对该信号进行处理。所以设计黑匣子程序的实质就是我们定义自己的信号处理函数,来代替内核的默认处理。在我们的信号处理函数中,我们可以将我们想要的信息保存下来(比如程序崩溃时的堆栈信息),以方便后面问题的定位。
下面我们先给出一个我写的程序,然后边分析程序边讲具体如何设计一个黑匣子程序:
#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; } |
2.1 定义一些数据结构
这里我们定义了一个sigInfo的数据结构,用来保存信号。利用这个数据结构我们可以将信号值与信号名映射起来。你可以在你的系统中使用 kill –l 命令去查看他们的对应关系。当然,在程序中,如果得到了信号值,也可以使用Linux提供的API函数strsignal来获取信号的名字,其函数原型如下:
#include <string.h>
char *strsignal(int sig);
之后定义了一个全局变量sigCatch来增加我们想要处理的信号。