Linux&C--多线程程序中捕获段错误segmentation fault

通过对可能出现segmentation fault的代码段添加类似java中try catch结构,实现在多线程中分开捕获各自的segment fault的能力,达到可定位、可自定义处理异常方法的效果。

仅“预防型”处理segmentation fault。

1 知识点列表

1.1 Linux段错误的系统处理

发生段错误后系统会抛出 SIGSEGV 信号 ,之后调用默认的信号处理函数 ,产生core文件,然后关闭程序。

1.2 signal函数

用来截获信号,调用自己的信号处理函数。
可以捕获整个进程中的SIGSEGV信号,然后调用自己的信号处理函数,自行处理段错误。但是无法确认是哪里报的SIGSEGV异常。

1.3 setJump和longJump

非局部跳转语句—setjmp和longjmp函数。非局部指的是,这不是由普通C语言goto,语句在一个函数内实施的跳转,而是在栈上跳过若干调用帧,返回到当前函数调用路径上的某一个函数中。
利用setJump记录可能异常的开始位置,结合signal函数,捕获SIGSEGV信号后,在信号处理函数中利用longjump跳回异常开始位置,可定位异常位置

1.4 __thread修饰线程变量

定义一个线程局部变量很简单,只需简单的在全局或静态变量的声明中包含__thread说明符即可。对于这个变量,每个线程保存自己的一份拷贝,因此每个线程的这个变量的地址不同。

1.5 自定义信号处理函数

SIGSEGV在同一线程不能被自定义的信号处理函数捕获两次及以上,是因为进入到信号处理函数之后,这个信号会被阻塞(block),此时不能再绑定信号处理函数。

2 详解

2.1 signal函数

参考:https://blog.csdn.net/work_msh/article/details/8470277

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

第一个参数 的意思表示你要绑定的信号
第二个参数 是表示信号处理的函数指针,返回值为void,参数为int,如上 ,另外 系统也定义了一些宏,如下,分别表示错误返回,使用默认的信号处理函数,忽略该信号。

/* Fake signal functions.  */
#define SIG_ERR	((__sighandler_t) -1)		/* Error return.  */
#define SIG_DFL	((__sighandler_t) 0)		/* Default action.  */
#define SIG_IGN	((__sighandler_t) 1)		/* Ignore signal.  */

返回值是一个信号处理函数的指针,如果发生错误,返回 SIG_ERR 这个宏,事实上也是定义的一个函数,产生错误的原因主要是因为给定的信号不正确。
另外这个使用函数 有两点要注意:

a) 进入到信号处理函数之后,这个信号会被阻塞(block)直到信号处理函数返回。所以不能再给它绑定信号处理函数,所以不能被捕获两次

b) 信号函数处理完之后,会将该信号恢复为默认处理状态 ,即SIGSEGV信号的处理函数会重置为系统默认的产生core文件函数,所以在下一次用到的时候要重新调用signal这个函数绑定。

c) 设置一次,整个进程有效,当某线程中信号函数处理完,只会讲该线程中的信号恢复为默认处理状态。

2.2 setjump & longjump

非局部跳转语句—setjmp和longjmp函数。非局部指的是,这不是由普通C语言goto,语句在一个函数内实施的跳转,而是在栈上跳过若干调用帧,返回到当前函数调用路径上的某一个函数中。

#include <setjmp.h>
int setjmp(jmp_buf env);

setjmp将上下文 ,就是cpu和内存的信息保存到env中;
返回值:若直接调用则返回0,若从longjmp调用返回则返回非0值的longjmp中的val值

#include <setjmp.h>
void longjmp(jmp_buf env, int val);

调用此函数则返回到语句setjmp所在的地方,其中env 就是setjmp中的 env,而val 则是使setjmp的返回值变为val。
使用setjmp和longjmp要注意以下几点:
a) setjmp与longjmp结合使用时,它们必须有严格的先后执行顺序,也即先调用setjmp函数,之后再调用longjmp函数,以恢复到先前被保存的“程序执行点”。否则,如果在setjmp调用之前,执行longjmp函数,将导致程序的执行流变的不可预测,很容易导致程序崩溃而退出

b) longjmp必须在setjmp调用之后,而且longjmp必须在setjmp的作用域之内。具体来说,在一个函数中使用setjmp来初始化一个全局标号,然后只要该函数未曾返回,那么在其它任何地方都可以通过longjmp调用来跳转到 setjmp的下一条语句执行。实际上setjmp函数将发生调用处的局部环境保存在了一个jmp_buf的结构当中,只要主调函数中对应的内存未曾释放 (函数返回时局部内存就失效了),那么在调用longjmp的时候就可以根据已保存的jmp_buf参数恢复到setjmp的地方执行。

2.3 sigsetjmp & siglongjmp
int sigsetjmp(sigjmp_buf env, int savesigs);
void siglongjmp(sigjmp_buf env, int val);

sigsetjmp 函数多了一个参数,savesigs,当 savesigs 不为 0时,会保存当前的信号屏蔽表 (signal mask),然后在使用siglongjmp 跳转的时候,会恢复线程的屏蔽表。
Todo:信号掩码暂时没弄明白。
可解决不能捕获两次及以上的问题。

2 实现

参考:https://blog.csdn.net/yangping_zheng/article/details/20781071

3.1 exception.h
#ifndef CLIONTEST_EXCEPTION_H
#define CLIONTEST_EXCEPTION_H

// 保存当前局部环境
//#define Try(type) if ( ((type) = setjmp(_StackInfo)) == 0)
#define Try(type) if ( ((type) = sigsetjmp(_StackInfo, 1)) == 0)

// 捕获指定sig异常
#define Catch(type, sig) else if ((type) == (sig))

// 捕获其余sig异常
#define CatchElse else


#endif //CLIONTEST_EXCEPTION_H
3.2 多线程捕获SIGSEGV
#include <setjmp.h>
#include <signal.h>
#include <stdio.h>
#include <pthread.h>

#include "exception.h"

__thread jmp_buf _StackInfo; // setjmp函数将发生调用处的局部环境保存在了一个jmp_buf的结构

static void HandleSigInt(int signum)
{
//    longjmp(_StackInfo, signum);
	siglongjmp(_StackInfo, signum);
}

void *handleThread() {
    signal(SIGSEGV, HandleSigInt);
    int type;
    Try (type) {
        char *charP = NULL;
        *charP = 'd';
    } Catch (type, SIGSEGV) {
        printf("=======异常类型:SIGSEGV\n");
    } CatchElse {
        printf("=======异常\n");
	}
	pthread_exit(NULL);
}

int main() {
    signal(SIGSEGV, HandleSigInt);
    int type;
    Try(type) {
        pthread_t pid;
        pthread_create(&pid, NULL, handleThread, NULL);
        pthread_join(pid, NULL);
        char *charP = NULL;
        *charP = 'd';
    } Catch(type, SIGSEGV) {
        printf("异常类型:SIGSEGV\n");
    } CatchElse {
        printf("异常\n");
	}
	return 0;
}

猜你喜欢

转载自blog.csdn.net/chenmeiling_5/article/details/107773423