linux SIGSEGV 信号捕捉,保证发生段错误后程序不崩溃

本文介绍了如何在Linux编程中使用信号处理技术来捕获并处理异常情况,如段错误,从而避免程序崩溃。通过自定义信号处理函数、使用setjmp和longjmp以及sigsetjmp和siglongjmp函数,可以确保程序在遇到错误时能够继续执行而不是终止。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在linux中编程的时候 有时候 try catch 可能满足不了我们的需求。因为碰到类似数组越界 ,非法内存访问之类的 ,这样的错误无法捕获。下面我们介绍一种使用捕获信号实现的异常 用来保证诸如段错误之类的错误发生时程序不会崩溃,而是跳过代码继续执行。首先我们来看看发生段错误之后系统的处理。

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

那有没有一种办法可以保证程序不会死掉呢,当然是有的 。首先我们想到的是 截获改信号,调用自己的信号处理函数 。

让我们来看看signal 这个函数 。
 #include <signal.h>
       typedef void (*sighandler_t)(int);
       sighandler_t signal(int signum, sighandler_t handler);
        第一个参数 的意思表示你要绑定的信号 (可以使用在控制台使用 kill -l 查看都有哪些信号 ,这些就不讲了,有兴趣的可以上网查)
        第二个参数 是表示信号处理的函数 指针 ,返回值为void* 参数为int ,如上 ,另外 系统也定义了一些宏 
                           (SIG_IGN,和 SIG_DFL) 第一个表示忽略这个信号 ,第二个表示 使用默认的信号处理函数 如果我们处理的       是SIGSEGV信号 ,那么它就会产生core文件 等等操作  
        返回值是一个信号处理函数的指针 ,如果发生错误 返回 SIG_ERR 这个宏 ,事实上 也是定义的一个函数 产生错误的原因 主要是因为给定的信号不正确 
另外这个使用函数 有两点要注意 
   1. 进入到信号处理函数之后 这个信号会被 阻塞(block) 直到信号处理函数 返回 这点非常重要 ,后面会讲到。
   2. 信号函数处理完之后,会将该信号恢复为默认处理状态 ,即重新与产生core文件...函数绑定,所以在下一次用到的时候要重新调用signal这个函数绑定
       自定义的信号处理函数
那么我们就可以开始尝试使用它了
 
#include <signal.h>
#include <setjmp.h>
#include <stdarg.h>
#include <stdlib.h>
#include <stdio.h>
//信号处理函数
void recvSignal(int sig)
{
	printf("received signal %d !!!\n",sig);
}
int main(int argc,char** argv)
{
  //给信号注册一个处理函数 
  signal(SIGSEGV, recvSignal);
  int* s = 0;
  (*s) = 1;
 //以上两句用来产生 一个 传说中的段错误
  while(1)
  {
    sleep(1);
    printf("sleep 1 \n");
  }
  return 0;
}



编译运行  一直打印收到信号 11 (SIGSEGV),为什么呢 ,
上面代码给SIGSEGV 这个信号注册了一个处理函数 ,替代了系统默认的产生core文件的处理函数 ,当错误发生后 ,系统 发送 SIGSEGV ,然后 中断了程序 跳到 recvSignal 处理函数中去 ,处理完成后 ,再跳回来错误发生的地方 ,然后继续产生错误 ,继续发送 SIGSEGV  信号 ... 
使用 setjmp 和longjmp 尝试跳过错误堆栈   
#include <setjmp.h>
 int setjmp(jmp_buf env);   void longjmp(jmp_buf env, int val);
系统跳转函数 ,可以直接在函数之间跳转 (比goto 强大多了) 
int setjmp(jmp_buf env);  这个函数 将上下文 ,就是cpu和内存的信息保存到env中 (不用去理解 jmp_buf,就当我们平时用的buff好了),然后调用 void longjmp(jmp_buf env, int val); 的时候 跳转到使用env中的信息 ,恢复上下文 。如果是第一回调用setjmp 它会返回 0,如果是在 从longjmp 跳转过来的 ,那就返回 longjmp的参数 val,根据setjmp的返回值 我们就可以决定执行可能发生错误的代码还是直接跳过这段代码 。知道了原理之后 我们可能就会这样写 
#include <signal.h>
#include <setjmp.h>
#include <stdarg.h>
#include <stdlib.h>
#include <stdio.h>
jmp_buf env;
//信号处理函数
void recvSignal(int sig)
{
printf("received signal %d !!!\n",sig);
        longjmp(env,1);
}
int main(int argc,char** argv)
{

    //保存一下上下文 
    int r = setjmp(env);
    if(  r  == 0)
    {
        //初次执行 ,那么可以执行 可能会发生错误的代码
        //给信号注册一个处理函数  
        signal(SIGSEGV, recvSignal);
        printf("excute this code!!");
           int* s = 0;
            (*s) = 1;
    }
    else
    {
        //是由longjmp 跳转回来的
            printf("jump this code !!"); 
    }
    while(1)
    {
        sleep(1);
        printf("sleep 1 \n");
    }
    return 0;
}


编译 ,执行  产生 SIGSEGV 信号 ,然后在信号函数 里边跳转 到  int r = setjmp(env); 这一行 ,之后 直接略过了 可能发生错误的这段代码 ,跳转生效,可是这种方式还有一个bug,我们看看下面的代码 
#include <signal.h>
#include <setjmp.h>
#include <stdarg.h>
#include <stdlib.h>
#include <stdio.h>
jmp_buf env;
//信号处理函数
void recvSignal(int sig)
{
printf("received signal %d !!!\n",sig);
        longjmp(env,1);
}
int main(int argc,char** argv)
{

    for(int i = 0; i < 2; i++)
    {
            //保存一下上下文 
        int r = setjmp(env);
        if(  r  == 0)
        {
            //初次执行 ,那么可以执行 可能会发生错误的代码
            //给信号注册一个处理函数  
            signal(SIGSEGV, recvSignal);
            printf("excute this code!!");
               int* s = 0;
                (*s) = 1;
        }
        else
        {
                //是由longjmp 跳转回来的
                printf("jump this code !!"); 
        }
        sleep(5);
    }

    while(1)
    {
        sleep(1);
        printf("sleep 1 \n");
    }
    return 0;
}


当for循环第二次执行的时候 ,程序依然产生了 SIGSEGV,系统仍然调用了默认的处理函数产生了core文件 ,分析下原因 上面我们说过“进入到信号处理函数之后 这个信号会被 阻塞(block) 直到信号处理函数返回”,在进入到信号处理函数之后 ,这个时候 系统阻塞了 SIGSEGV 这个信号 ,当跳回到 int r = setjmp(env); 这行代码的时候  SIGSEGV 信号依然是阻塞的 ,那以后 再给他绑定信号处理函数 自然没有作用 。
好在系统给我们提供了int sigsetjmp(sigjmp_buf env, int savesigs);和  void siglongjmp(sigjmp_buf env, int val);这两个函数 ,这两个函数 和上面的 int setjmp(jmp_buf env);   void longjmp(jmp_buf env, int val); 大同小异 ,唯一的不同 是sigsetjmp 函数 多了 一个参数 ,savesigs,查看这函数的说明可以知道 ,当 savesigs 不为 0时,会保存当前的信号屏蔽表 (signal mask),然后在使用siglongjmp 跳转的时候 会恢复 线程的 屏蔽表。
于是我们把上面的代码修改 后如下:
#include <signal.h>
#include <setjmp.h>
#include <stdarg.h>
#include <stdlib.h>
#include <stdio.h>
// jmp_buf env;
//信号处理函数
void recvSignal(int sig)
{
printf("received signal %d !!!\n",sig);
        siglongjmp(env,1);
}
int main(int argc,char** argv)
{

    for(int i = 0; i < 2; i++)
    {
            //保存一下上下文 
        int r = sigsetjmp(env,1);
        if(  r  == 0)
        {
            //初次执行 ,那么可以执行 可能会发生错误的代码
            //给信号注册一个处理函数  
            signal(SIGSEGV, recvSignal);
            printf("excute this code!!");
               int* s = 0;
                (*s) = 1;
        }
        else
        {
                //是由longjmp 跳转回来的
                printf("jump this code !!"); 
        }
        sleep(5);
    }

    while(1)
    {
        sleep(1);
        printf("sleep 1 \n");
    }
    return 0;
}


编译后 运行 。按照我们的需求 第二次进入for循环时, 发生段错误后程序不会死掉 ,而是会跳过这段代码了继续往下走 。下面我做了一个简单的封装 ,在错误发生时,我打印出了 错误信息 ,然后跳过错误的代码 
/*
** file name CException.h
*/
#ifndef _CEXCEPTION_H_
#define _CEXCEPTION_H_
#include <setjmp.h>
#include <stdlib.h>
#include <stdarg.h>
#include <execinfo.h>
#include <stdio.h>
#include <signal.h>
#include <iostream>
#include <string.h>
typedef struct Except_frame
{
    jmp_buf env;
    int flag;
    void clear()
    {
       flag = 0;
       bzero(env,sizeof(env));
    }
    bool isDef()
    {
       return flag;
    }
    Except_frame()
    {
      clear();
    }
}Except_frame;
extern Except_frame* except_stack;
extern void errorDump();
extern void recvSignal(int sig);
Except_frame* except_stack = new Except_frame;
void errorDump()
{
    const int maxLevel = 200;
    void* buffer[maxLevel];
    int level = backtrace(buffer, maxLevel);
    const int SIZE_T = 1024;
    char cmd[SIZE_T] = "addr2line -C -f -e ";
    char* prog = cmd + strlen(cmd);
    readlink("/proc/self/exe", prog, sizeof(cmd) - (prog-cmd)-1);
    FILE* fp = popen(cmd, "w");
    if (!fp)
    {
        perror("popen");
        return;
    }
    for (int i = 0; i < level; ++i)
    {
        fprintf(fp, "%p\n", buffer[i]);
    }
    fclose(fp);
}

void recvSignal(int sig)
{
    printf("received signal %d !!!\n",sig);
    errorDump();
    siglongjmp(except_stack->env,1);
}
#define TRY \
    except_stack->flag = sigsetjmp(except_stack->env,1);\
    if(!except_stack->isDef()) \
    { \
      signal(SIGSEGV,recvSignal); \
      printf("start use TRY\n");
#define END_TRY \
    }\
    else\
    {\
      except_stack->clear();\
    }\
    printf("stop use TRY\n");
#define RETURN_NULL \
    } \
    else \
    { \
      except_stack->clear();\
    }\
    return NULL;
#define RETURN_PARAM  { \
      except_stack->clear();\
    }\
    return x;
#define EXIT_ZERO \
    }\
    else \
    { \
      except_stack->clear();\
    }\
    exit(0);
#endif


另外建一个文件 ,
#include "CException.h"
int main(int argc,char** argv)
{
    //可以如下使用 
    TRY
        int*s = 0;
        (int*s) = 1;
    END_TRY
    //使用这两个宏包含可能发生的错误代码 ,当然可以根据需求 使用 
    //RETURN_NULL 
    //RETURN_PARAM(0)
    //EXIT_ZERO  这三个宏
    return 0;
}


这个时候我们就能使用TRY 和 END_TRY,RETURM_NULL,RETURN_PARAM(param) 来实现程序发生段错误后跳过错误代码继续运行了 ,不过此代码仅限于单线程使用
<think>嗯,用户的问题是在Linux环境下使用Qt框架如何捕捉和处理异常的最佳实践。我需要先回忆一下Qt本身的异常处理机制,以及Linux下的一些工具和方法。 首先,Qt框架主要使用C++,而C++有try-catch机制,但Qt本身的一些部分可能并完全依赖异常处理,比如信号槽机制。用户可能需要了解如何在Qt程序中正确使用try-catch来捕获异常,尤其是在多线程环境下或者结合Qt的事件循环时。 然后,用户提到Linux系统,可能需要考虑系统级别的异常处理,比如信号处理。比如SIGSEGV这样的信号,通常是由于内存错误引起的,如何捕获这些信号并处理,可能需要用到sigaction或者Qt提供的某些接口。 另外,用户可能还需要了解Qt中的异常安全,比如资源管理,避免内存泄漏。在析构函数中使用noexcept,或者智能指针来管理资源。 再想到用户提供的引用内容,引用[2]提到Qt直接提供崩溃检测,但可以通过调试工具、系统日志、异常处理和第三方工具。比如AddressSanitizer可以帮助检测内存错误,引用[3]中的代码示例展示了如何使用try-catch来捕获网络相关的异常。 可能的最佳实践包括: 1. 正确使用C++的异常处理机制,在可能抛出异常的代码块使用try-catch,特别是在网络操作、文件IO等地方。 2. 结合Qt的信号槽机制,可能需要将异常转换为信号,以便在GUI应用中处理,避免异常跨过信号槽边界导致程序崩溃。 3. 使用Linux信号处理函数来捕获系统信号,如SIGSEGV、SIGABRT,并记录相关信息,方便调试。 4. 利用Qt的日志系统或qDebug输出异常信息,方便追踪问题。 5. 集成第三方工具如AddressSanitizer或Valgrind进行内存检测,预防异常发生。 6. 编写异常安全的代码,使用RAII管理资源,确保在异常发生时资源能被正确释放。 7. 对于多线程应用,确保每个线程的异常被正确捕获和处理,避免主线程崩溃。 需要注意,在Qt的主事件循环中,异常如果未被捕获可能导致程序直接终止,因此需要特别小心。可能需要将可能抛出异常的操作放在单独的线程中,或者确保所有异常都被捕获并处理。 另外,引用[3]中的示例代码展示了网络操作中的异常捕获,用户可能需要类似的结构来处理Qt中的网络模块,比如使用Qt Network模块时的异常处理。 总结步骤应该是:先使用C++的异常处理机制,再结合Qt的信号和日志,然后处理系统信号,最后利用工具进行检测和预防。</think>在Linux系统中通过Qt框架进行异常捕获和处理,需结合C++标准异常机制与Qt框架特性。以下是分步解决方案: ### 一、C++标准异常处理基础 1. **基础try-catch结构**:对可能抛出异常的代码段进行包裹 ```cpp try { QFile file("data.txt"); if (!file.open(QIODevice::ReadOnly)) { throw std::runtime_error("文件打开失败"); } // 文件操作... } catch (const std::exception& e) { qCritical() << "异常捕获:" << e.what(); } ``` 2. **自定义异常类型**(增强错误分类能力) ```cpp class NetworkException : public std::runtime_error { public: NetworkException(const QString& msg) : std::runtime_error(msg.toStdString()) {} }; // 使用示例 throw NetworkException("Socket连接超时"); ``` ### 二、Qt框架集成处理 3. **信号异常转发**(解决跨线程异常传递问题) ```cpp class Worker : public QObject { Q_OBJECT public slots: void doWork() { try { // 工作任务... } catch (...) { emit errorOccurred(QString::fromLocal8Bit("工作线程异常")); } } signals: void errorOccurred(const QString&); }; ``` 4. **QApplication全局捕获**(GUI应用顶层处理) ```cpp int main(int argc, char *argv[]) { QApplication a(argc, argv); try { MainWindow w; w.show(); return a.exec(); } catch (const std::exception& e) { QMessageBox::critical(nullptr, "致命错误", QString::fromLocal8Bit(e.what())); return -1; } } ``` ### 三、Linux系统级集成 5. **信号处理设置**(捕获系统级异常) ```cpp #include <csignal> void signalHandler(int sig) { qFatal("捕获系统信号 %d,调用栈:\n%s", sig, QStackWalker().dump().toLatin1().constData()); } // 初始化设置 std::signal(SIGSEGV, signalHandler); std::signal(SIGABRT, signalHandler); ``` 6. **AddressSanitizer集成**(内存错误检测) ```bash # 在.pro文件中添加编译选项 QMAKE_CXXFLAGS += -fsanitize=address QMAKE_LFLAGS += -fsanitize=address ``` ### 四、诊断增强措施 7. **堆栈跟踪实现**(需安装libunwind-dev) ```cpp #include <libunwind.h> void printStackTrace() { unw_cursor_t cursor; unw_context_t context; unw_getcontext(&context); unw_init_local(&cursor, &context); while (unw_step(&cursor) > 0) { unw_word_t offset, pc; char sym[256]; unw_get_reg(&cursor, UNW_REG_IP, &pc); if (pc == 0) break; unw_get_proc_name(&cursor, sym, sizeof(sym), &offset); qDebug() << "0x" << QString::number(pc, 16) << ":" << sym << "+0x" << QString::number(offset, 16); } } ``` 8. **核心转储配置**(Ubuntu系统示例) ```bash ulimit -c unlimited sudo sysctl -w kernel.core_pattern=/var/crash/core-%e-%p-%t ``` ### 五、最佳实践建议 1. **资源管理规范**:所有QObject派生对象使用父子关系管理内存 2. **异常安全等级**: - 基本保证异常发生时资源泄漏 - 强保证:操作具备原子性(使用事务模式) 3. **日志记录策略**:结合qInstallMessageHandler实现分级日志 ```cpp void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& msg) { // 写入文件或发送到日志服务器 } ``` 4. **单元测试集成**:使用GTest框架验证异常路径 ```cpp TEST(ExceptionTest, FileOpenFailure) { EXPECT_THROW({ QFile file("nonexist.txt"); if (!file.open(QIODevice::ReadOnly)) { throw FileException("强制抛出异常"); } }, FileException); } ``` [^1]: 通过合理配置系统信号处理,可以增强Qt应用在Linux环境下的健壮性。引用[2]中提到的AddressSanitizer工具能有效检测内存相关异常。 [^2]: 核心转储文件的分析需要配合调试符号,建议发布版本保留调试信息或使用separate debug files。引用[3]展示的异常处理结构适用于网络通信场景。
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值