飞机上面的黑匣子用于飞机失过后对事故的时候调查,同理,程序的黑匣子用于程序崩溃后对崩溃缘由进程定位。其实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; } } }