如何在linux程序中捕获异常信号

来源:互联网 发布:铁血战士玩具淘宝 编辑:程序博客网 时间:2024/04/30 00:38

最近在搞一个linux项目,碰巧遇到了一个段错误的问题。经过一段时间的排查和学习,对段错误这个概念有了些许的理解,现总结如下:

一、什么是段错误

一句话来说,段错误是指访问的内存超出了系统给这个程序所设定的内存空间,例如访问了不存在的内存地址、访问了系统保护的内存地址、访问了只读的内存地址等等情况。这里贴一个对于“段错误”的准确定义:

A segmentation fault (often shortened to segfault) is a particular error condition that can occur during the operation of computer software. In short, a segmentation fault occurs when a program attempts to access a memory location that it is not allowed to access, or attempts to access a memory location in a way that is not allowed (e.g., attempts to write to a read-only location, or to overwrite part of the operating system). Systems based on processors like the Motorola 68000 tend to refer to these events as Address or Bus errors.

Segmentation is one approach to memory management and protection in the operating system. It has been superseded by paging for most purposes, but much of the terminology of segmentation is still used, “segmentation fault” being an example. Some operating systems still have segmentation at some logical level although paging is used as the main memory management policy.

On Unix-like operating systems, a process that accesses invalid memory receives the SIGSEGV signal. On Microsoft Windows, a process that accesses invalid memory receives the STATUS_ACCESS_VIOLATION exception.

二、为什么会出现段错误

很多非法的操作都会导致系统产生段错误,大致分为以下几类:

1、访问不存在的内存地址

#include<stdio.h>#include<stdlib.h>void main(){        int *ptr = NULL;        *ptr = 0;}

2、访问系统保护的内存地址

#include<stdio.h>#include<stdlib.h>void main(){        int *ptr = (int *)0;        *ptr = 100;}

3、访问只读的内存地址

#include<stdio.h>#include<stdlib.h>#include<string.h>void main(){        char *ptr = "test";        strcpy(ptr, "TEST");}

4、栈溢出

#include<stdio.h>#include<stdlib.h>void main(){        main();}

5、其他原因

  1. 使用非法的指针,包括使用未经初始化及已经释放的指针(指针使用之前和释放之后置为NULL);
  2. 内存读/写越界。包括数组访问越界,或在使用一些写内存的函数时,长度指定不正确或者这些函数本身不能指定长度,典型的函数有strcpy(strncpy),sprintf(snprint)等等;
  3. 对于C++对象,请通过相应类的接口来去内存进行操作,禁止通过其返回的指针对内存进行写操作,典型的如string类的data()和c_str()两个接口;
  4. 函数不要返回其中局部对象的引用或地址,当函数返回时,函数栈弹出,局部对象的地址将失效,改写或读这些地址都会造成未知的后果;
  5. 避免在栈中定义过大的数组,否则可能导致进程的栈空间不足,此时也会出现段错误;
  6. 操作系统的相关限制,如:进程可以分配的最大内存,进程可以打开的最大文件描述符个数等,这些需要通过ulimit或setrlimit或sysctl来解除相关的限制;
  7. 多线程的程序,涉及到多个线程同时操作一块内存时必须进行互斥,否则内存中的内存将不可预料;
  8. 使用非线程安全的函数调用,例如strerror函数等;
  9. 在有信号的环境中,使用不可重入函数调用,而这些函数内部会读或写某片内存区,当信号中断时,内存写操作将被打断,而下次进入时将不避免的出错;
  10. 跨进程传递某个地址;
  11. 某些有特殊要求的系统调用,例如epool_wait,正常情况下使用close关闭一个套接字后,epool会不再返回这个socket上的事件,但是如果你使用dup或dup2操作,将导致epool无法进行移除操作。

三、怎么捕获段错误信息

可以通过向系统注册一个段错误的捕获函数,来实现段错误的捕获。如下:

/****************************************************************************************************  函数名称:  segv_error_handle**  功能描述:  段错误的实际处理函数**  输入参数:  无**  输出参数:  无**  返回参数:  无**************************************************************************************************/static void segv_error_handle(int v){    system("sync");    printf("segv_error(value: %d), proc is going to exit now!!!\n", v);    exit(1);}/****************************************************************************************************  函数名称:  install_segv_handler**  功能描述:  注册段错误的处理函数**  输入参数:  无**  输出参数:  无**  返回参数:  无**************************************************************************************************/static void install_segv_handler(){    struct sigaction siga;    siga.sa_handler = segv_error_handle;    siga.sa_flags = 0;    memset(&siga.sa_mask, 0, sizeof(sigset_t));    sigaction(SIGSEGV, &siga, NULL);                                           /* 捕获段非法错误的信号 */    sigaction(SIGTERM, &siga, NULL);                                           /* 捕获软件终止的信号 */    sigaction(SIGINT,  &siga, NULL);                                           /* 捕获进程中断的信号 */}/****************************************************************************************************  函数名称:  main**  功能描述:  主函数**  输入参数:  无**  输出参数:  无**  返回参数:  无**************************************************************************************************/int main(int argc, char *argv[]){    install_segv_handler();                                                    /* 段错误的处理函数 */    ........                                                                   /* 其他代码 */}

可以看到,上述代码中,向系统注册了3类的段错误,分别为SIGSEGV,SIGTERM,SIGINT。那么,这几个类型到底代表什么意思?除了他们之外,还有哪些其他的类型呢?参见下表:

01)SIGHUP:本信号在用户终端连接(正常或非正常)结束时发出,通常是在终端的控制进程结束时,通知同一session内的各个作业,这时它们与控制终端不再关联;
02)SIGINT:程序终止(interrupt)信号,在用户键入INTR字符(通常是Ctrl-C)时发出;
03)SIGQUIT:和SIGINT类似,但由QUIT字符(通常是Ctrl-)来控制。进程在因收到SIGQUIT退出时会产生core文件,在这个意义上类似于一个程序错误信号;
04)SIGILL:执行了非法指令。通常是因为可执行文件本身出现错误,或者试图执行数据段。堆栈溢出时也有可能产生这个信号;
05)SIGTRAP:由断点指令或其它trap指令产生,由debugger使用;
06)SIGABRT:程序自己发现错误并调用abort时产生;
06)SIGIOT:在PDP-11上由iot指令产生,在其它机器上和SIGABRT一样;
07)SIGBUS:非法地址,:包括内存地址对齐(alignment)出错。eg:访问一个四个字长的整数,但其地址不是4的倍数;
08)SIGFPE:在发生致命的算术运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为0等其它所有的算术的错误;
09)SIGKILL:用来立即结束程序的运行。本信号不能被阻塞,处理和忽略;
10)SIGUSR1:留给用户使用;
11)SIGSEGV:试图访问未分配给自己的内存,或试图往没有写权限的内存地址写数据;
12)SIGUSR2:留给用户使用;
13)SIGPIPE:Broken:pipe;
14)SIGALRM:时钟定时信号,计算的是实际的时间或时钟时间。alarm函数使用该信号;
15)SIGTERM:程序结束(terminate)信号,与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出。shell命令kill缺省产生这个信号;
17)SIGCHLD:子进程结束时,父进程会收到这个信号;
18)SIGCONT:让一个停止(stopped)的进程继续执行。本信号不能被阻塞。可以用一个handler来让程序在由stopped状态变为继续执行时完成特定的工作。例如,重新显示提示符;
19)SIGSTOP:停止(stopped)进程的执行。注意它和terminate以及interrupt的区别:该进程还未结束,只是暂停执行。本信号不能被阻塞,处理或忽略;
20)SIGTSTP:停止进程的运行,但该信号可以被处理和忽略。用户键入SUSP字符时(通常是Ctrl-Z)发出这个信号;
21)SIGTTIN:当后台作业要从用户终端读数据时,该作业中的所有进程会收到SIGTTIN信号。缺省时这些进程会停止执行;
22)SIGTTOU:类似于SIGTTIN,但在写终端(或修改终端模式)时收到;
23)SIGURG:有”紧急”数据或out-of-band数据到达socket时产生;
24)SIGXCPU:超过CPU时间资源限制。这个限制可以由getrlimit/setrlimit来读取/改变;
25)SIGXFSZ:超过文件大小资源限制;
26)SIGVTALRM:虚拟时钟信号。类似于SIGALRM,但是计算的是该进程占用的CPU时间;
27)SIGPROF:类似于SIGALRM/SIGVTALRM,但包括该进程用的CPU时间以及系统调用的时间;
28)SIGWINCH:窗口大小改变时发出;
29)SIGIO:文件描述符准备就绪,可以开始进行输入/输出操作;
30)SIGPWR:Power:failure;

其中有两个信号可以停止进程:SIGTERM和SIGKILL。

  • SIGTERM比较友好,进程能捕捉这个信号,根据您的需要来关闭程序。在关闭程序之前,您可以结束打开的记录文件和完成正在做的任务。在某些情况下,假如进程正在进行作业而且不能中断,那么进程可以忽略这个SIGTERM信号。
  • 对于SIGKILL信号,进程是不能忽略的。这是一个“我不管您在做什么,立刻停止”的信号。假如发送SIGKILL信号给进程,linux就将进程停止在那里。

四、其他注意事项

  1. 出现段错误时,首先应该想到段错误的定义,从它出发考虑引发错误的原因。
  2. 在使用指针时,定义了指针后记得初始化指针,在使用的时候记得判断是否为NULL。
  3. 在使用数组时,注意数组是否被初始化,数组下标是否越界,数组元素是否存在等。
  4. 在访问变量时,注意变量所占地址空间是否已经被程序释放掉。
  5. 在处理变量时,注意变量的格式控制是否合理等。
原创粉丝点击