Skip to content

信号处理导致程序堆栈错误问题总结

问题现象

程序捕获信号并执行信号处理函数后,返回原程序时出现堆栈错误:0x00000000

问题原因

当程序阻塞在系统调用中(如 read(), write(), accept() 等)时接收到信号,会发生以下情况:

  1. 系统调用被中断:当前程序的阻塞状态被立即打断
  2. 执行信号处理函数:立即跳转到信号处理函数执行
  3. 系统调用返回失败:信号处理函数完成后,被中断的系统调用默认返回 -1
  4. 错误码设置errno 被设置为 EINTR(Interrupted system call)
  5. 程序异常退出:如果没有正确处理该错误,程序可能异常终止或出现堆栈错误

易受影响的系统调用

以下系统调用在被信号中断后会返回 EINTR

  • read() / write() - 文件/socket 读写
  • accept() / connect() - 网络连接
  • sleep() / nanosleep() - 休眠函数
  • wait() / waitpid() - 进程等待
  • select() / poll() / epoll_wait() - I/O 多路复用

解决方案

方案一:设置 SA_RESTART 标志(推荐)

在设置信号处理函数时,将 sigaction 结构体的 sa_flags 设置为 SA_RESTART,使被中断的系统调用自动重启。

代码示例

cpp
#include <signal.h>
#include <unistd.h>
#include <iostream>

// 信号处理函数
void signal_handler(int signum) {
    std::cout << "Caught signal " << signum << std::endl;
    // 处理信号...
}

int main() {
    struct sigaction sa;
    
    // 设置信号处理函数
    sa.sa_handler = signal_handler;
    
    // 清空信号集
    sigemptyset(&sa.sa_mask);
    
    // 关键:设置 SA_RESTART 标志
    sa.sa_flags = SA_RESTART;
    
    // 注册信号处理
    if (sigaction(SIGINT, &sa, nullptr) == -1) {
        perror("sigaction");
        return 1;
    }
    
    // 阻塞系统调用示例
    char buffer[1024];
    ssize_t n = read(STDIN_FILENO, buffer, sizeof(buffer));
    
    if (n == -1) {
        perror("read");
    } else {
        std::cout << "Read " << n << " bytes" << std::endl;
    }
    
    return 0;
}

方案二:手动检查 EINTR 并重试

如果不能使用 SA_RESTART(或需要更精细的控制),可以手动检查 errno 并重试:

cpp
ssize_t safe_read(int fd, void* buf, size_t count) {
    ssize_t n;
    
    // 循环重试直到成功或遇到其他错误
    while ((n = read(fd, buf, count)) == -1) {
        if (errno == EINTR) {
            continue;  // 被信号中断,重试
        } else {
            return -1; // 其他错误,返回失败
        }
    }
    
    return n;
}

SA_RESTART 标志说明

作用

SA_RESTART 标志使得被信号中断的系统调用在信号处理函数返回后自动重新执行,而不是返回 -1 并设置 errno = EINTR

适用场景

  • 阻塞式 I/O 操作
  • 网络服务器的 accept() 调用
  • 长时间运行的读写操作
  • 需要保证系统调用完整执行的场景

注意事项

⚠️ 并非所有系统调用都支持 SA_RESTART

  • sleep() / nanosleep() 不会重启(会返回剩余时间)
  • 带超时的系统调用(如 poll(), select())行为依赖于实现

最佳实践

  1. 默认使用 SA_RESTART:对于大多数信号处理场景,设置此标志可避免不必要的错误处理
  2. 关键系统调用手动检查:对于超时相关或特殊需求的系统调用,手动检查 EINTR 并决定是否重试
  3. 信号处理函数保持简洁:避免在信号处理函数中执行复杂操作,减少潜在问题

参考资料

  • man 2 sigaction - sigaction 系统调用文档
  • man 7 signal - 信号概述和安全函数列表

基于 VitePress 构建