编程进阶网 编程进阶网
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • Android提升进阶

  • iOS开发和进阶

  • Web开发和进阶

  • Linux应用开发

    • Linux应用开发
    • QML基础入门

    • QT核心库实践

    • Linux系统编程

      • Linux系统编程基础
      • 系统调用原理精讲
      • 文件IO深度解析
      • 进程管理深度解析
      • 线程并发深度解读
      • 信号处理深度解读
        • 进程间通信IPC
        • 网络IO多路复用
        • 内存管理深度解读
        • 动态链接与共享库
        • Linux与定时器
      • 综合项目实战

    • IoT智能硬件开发

    • Apps
    • Linux应用开发
    • Linux系统编程
    杨充
    2025-07-02
    目录

    信号处理深度解读

    # 05.信号处理深度解读

    信号是 Linux 最古老的 IPC 机制,也是嵌入式系统崩溃捕获和优雅退出最常用的手段。本节把信号生命周期拆开,教你写出不被信号杀死的嵌入式应用。

    # 目录介绍

    • 5.1 案例引入
      • 5.1.1 Qt 应用收到 SIGTERM 后没来得及保存配置
      • 5.1.2 核心问题
    • 5.2 信号的生命周期
      • 5.2.1 产生:硬件异常 vs 软件触发
      • 5.2.2 递达:pending 队列与信号屏蔽字
      • 5.2.3 处理:默认/忽略/捕捉三种动作
      • 5.2.4 信号的丢失与排队
    • 5.3 sigaction 的正确用法
      • 5.3.1 struct sigaction 字段详解
      • 5.3.2 SA_SIGINFO 与 siginfo_t 扩展信息
      • 5.3.3 SA_RESTART 与被中断的系统调用
      • 5.3.4 SA_NODEFER/SA_ONSTACK
    • 5.4 关键信号的嵌入式实战
      • 5.4.1 SIGSEGV 崩溃捕获与 core dump
      • 5.4.2 SIGPIPE 的默认行为与嵌入式网络程序
      • 5.4.3 SIGCHLD 与子进程回收
      • 5.4.4 SIGTERM/SIGINT 优雅退出模板
      • 5.4.5 SIGSTOP/SIGCONT 与嵌入式调试
    • 5.5 信号与线程
      • 5.5.1 进程级信号与线程级信号
      • 5.5.2 pthread_sigmask 线程信号屏蔽
      • 5.5.3 专用信号处理线程的模式
    • 5.6 signalfd:把信号变成文件描述符
      • 5.6.1 signalfd 与 select/epoll 集成
      • 5.6.2 signalfd 与 sigaction 的选择
    • 5.7 关键结论与速查表

    # 5.1 案例引入

    # 5.1.1 Qt 应用收到 SIGTERM 后没来得及保存配置

    某工业 HMI 软件在系统关机时,systemd 发送 SIGTERM 给 Qt 进程,要求 90 秒内退出。开发者在信号处理器中调用 QSettings::sync() 保存配置——然后在信号处理器里触发了 malloc(QString 内部分配内存)。

    信号处理器中调用 malloc → 如果主程序正在 malloc 中持有锁 → 死锁。设备关机时最后一次配置保存丢失,下次上电用户发现所有参数回到出厂值。

    出问题的代码:

    // ❌ 信号处理器中干的事太多
    void sigterm_handler(int sig) {
        // QSettings::sync() → QMap::insert() → malloc → 如果主线程正持锁 → 死锁
        settings->sync();
    
        // printf → fprintf → malloc (stdout 的缓冲区) → 同样危险
        printf("shutting down...\n");
    
        // fclose → free → 死锁风险
        fclose(log_file);
    
        exit(0);
    }
    

    修复方案——"信号处理器只设标志位"模式:

    // ✅ 信号处理器只做一件事:写 flag
    volatile sig_atomic_t shutdown_flag = 0;
    
    void sigterm_handler(int sig) {
        shutdown_flag = 1;    // ← 唯一的操作:原子类型赋值
    }
    
    // 主循环检查 flag
    int main() {
        signal(SIGTERM, sigterm_handler);
        signal(SIGINT, sigterm_handler);
    
        while (!shutdown_flag) {
            process_events();
        }
    
        // 安全:在主控制流中做清理
        settings->sync();     // ✅ 不在信号处理器内,malloc 安全
        fclose(log_file);
        printf("clean shutdown\n");
        return 0;
    }
    

    为什么 volatile sig_atomic_t 是信号处理器的铁律?

    属性 sig_atomic_t 普通 int
    读写原子性 POSIX 保证 不保证(64位可能半个字)
    信号处理安全 ✅ ❌
    volatile 作用 禁止编译器缓存 无
    能否用于条件判断 ✅ ⚠ 未定义行为

    sig_atomic_t 的典型大小:32 位系统上 int(4 字节),可以安全传递 exit code 或简单的计数器。

    # 5.1.2 核心问题

    1. 信号处理器里到底能调用哪些函数?(async-signal-safe 白名单)
    2. 为什么 signalfd 是比 sigaction 更现代的信号处理方式?
    3. SIGPIPE 在嵌入式 socket 编程中的"沉默杀手"效应
    4. 进程级信号和线程级信号怎么协作?多线程程序里信号发给谁?

    # 5.2 信号的生命周期

    # 5.2.1 产生:硬件异常 vs 软件触发

    信号从产生到处理的三类来源:

    来源 触发方式 典型信号 例子
    硬件异常 CPU 检测到非法操作 → 内核翻译为信号 SIGSEGV, SIGILL, SIGFPE, SIGBUS 空指针解引用、除零、对齐错误
    软件触发 进程/内核调用 kill/raise/abort 全部可发送 kill(pid, SIGTERM)
    终端事件 终端驱动检测到事件 → 通知前台进程组 SIGINT, SIGTSTP, SIGHUP Ctrl+C、Ctrl+Z、终端断开
    软件条件 管道破裂、子进程退出、定时器到期 SIGPIPE, SIGCHLD, SIGALRM 写已关闭的 socket、子进程退出

    硬件异常的处理链路(以 SIGSEGV 为例):

    1. CPU 执行: mov [rax], rdi          (rax = NULL)
    2. MMU: 页表项 Present bit = 0 → 触发 Page Fault
    3. CPU: 保存上下文 → 切换内核栈 → 调 do_page_fault()
    4. 内核: 检查 VMA (vm_area_struct)
             若 rax 地址不在任何 VMA 中 → 向当前进程递送 SIGSEGV
    5. 内核: 准备返回用户态 → 检测 pending 信号 → 调用 sig_handler
    6. 用户态: 信号处理器执行(或默认:coredump + 终止)
    

    siginfo_t 在硬件异常时提供的调试信息:

    void sigsegv_handler(int sig, siginfo_t *info, void *ucontext) {
        printf("CRASH: signal=%d at addr=%p\n", sig, info->si_addr);
        // info->si_addr = 引发异常的地址(NULL 或野指针)
        // info->si_code = SEGV_MAPERR (空指针) / SEGV_ACCERR (权限错误)
    }
    

    # 5.2.2 递达:pending 队列与信号屏蔽字

    信号的三种状态:

    产生 → 未决 (pending) 或 阻塞 (blocked) → 递达 (delivered) → 处理
    
    • 未决(pending):信号已产生但还未递达(内核中的 bitmap 记录)
    • 阻塞(blocked):进程显式屏蔽该信号(信号屏蔽字 sigprocmask)
    • 递达(delivered):信号被交给进程处理

    内核中的数据结构(task_struct 中):

    // 简化版
    struct task_struct {
        struct sigpending pending;       // 进程级 pending 信号集
        sigset_t blocked;                // 当前阻塞的信号集
        struct sighand_struct *sighand;  // 信号处理器表 (sigaction 数组)
    };
    
    struct sigpending {
        struct list_head list;           // 带附加数据的信号链表(实时信号)
        sigset_t signal;                 // pending 信号位图
    };
    

    信号屏蔽字与 9 个不可屏蔽信号:

    // SIGKILL(9) 和 SIGSTOP(19) 永远不能被屏蔽、忽略或捕捉
    sigset_t set;
    sigfillset(&set);
    sigprocmask(SIG_BLOCK, &set, NULL);  // 屏蔽所有信号
    
    kill(getpid(), SIGKILL);             // 立刻死亡,sigprocmask 挡不住
    kill(getpid(), SIGSTOP);             // 立刻暂停,挡不住
    

    完整的不可屏蔽信号列表:SIGKILL, SIGSTOP,以及某些实现中的 SIGCONT(部分屏蔽)。

    # 5.2.3 处理:默认/忽略/捕捉三种动作

    每个信号有三种处置方式:

    处置 API 效果
    默认(SIG_DFL) - 内核按信号类型的标准行为处理(终止/coredump/忽略/暂停)
    忽略(SIG_IGN) signal(sig, SIG_IGN) 信号被丢弃,子进程继承此设置
    捕捉 sigaction(sig, &act, NULL) 调用用户定义的处理函数

    系统默认处置的分类表:

    默认动作 信号 是否产生 core dump
    终止 (Term) SIGTERM, SIGINT, SIGHUP, SIGALRM, SIGPIPE 否
    Coredump + 终止 (Core) SIGSEGV, SIGABRT, SIGILL, SIGFPE, SIGBUS, SIGQUIT 是
    忽略 (Ignore) SIGCHLD, SIGURG, SIGWINCH -
    暂停 (Stop) SIGTSTP, SIGTTIN, SIGTTOU -
    恢复 (Continue) SIGCONT -

    exec 后信号处置的继承规则:exec 后,被忽略的信号继续保持忽略(防止忽略 SIGCHLD 后 exec 的子程序僵尸满天飞),被捕捉的信号恢复为默认。

    # 5.2.4 信号的丢失与排队

    传统 Unix 信号(1-31)不排队——同一信号多次产生,只记录一次 pending:

    进程 A 收到 3 次 SIGTERM:
      t1: SIGTERM → pending bitmap [SIGTERM] = 1
      t2: SIGTERM → pending bitmap [SIGTERM] 已经是 1,丢弃
      t3: SIGTERM → 同上,丢弃
      进程最终只处理 1 次 SIGTERM
    

    实时信号(SIGRTMIN ~ SIGRTMAX,34-64)会排队:

    #define MY_SIGNAL (SIGRTMIN + 1)   // 实时信号
    
    union sigval value;
    value.sival_int = 42;
    sigqueue(getpid(), MY_SIGNAL, value);  // 带数据 + 排队的信号
    
    // 信号处理器中
    void rt_handler(int sig, siginfo_t *info, void *ctx) {
        printf("got value: %d\n", info->si_value.sival_int);
    }
    

    实时信号 vs 传统信号:

    特性 传统信号 (1-31) 实时信号 (34-64)
    是否排队 ❌ 只记一个 bit ✅ 每条都入队
    递送顺序 无保证 低编号优先(同编号按 FIFO)
    携带数据 否 是(si_value 可以是 int 或指针)
    来源识别 否 是(si_pid + si_uid)
    典型用途 SIGTERM/SIGINT 线程间通知、自定义 IPC

    队列深度限制:/proc/sys/kernel/rt_sigqueue_max(通常为 1024),超出后 sigqueue 返回 EAGAIN。


    # 5.3 sigaction 的正确用法

    # 5.3.1 struct sigaction 字段详解

    sigaction 是当代信号处理的标准接口——signal() 的行为在不同 Unix 变体间不一致(BSD 自动重启系统调用,System V 不重启),POSIX 已将其标记为废弃。

    struct sigaction {
        void     (*sa_handler)(int);                    // 简单处理器 (SIG_DFL/SIG_IGN/函数指针)
        void     (*sa_sigaction)(int, siginfo_t *, void *); // 扩展处理器 (SA_SIGINFO 标志下使用)
        sigset_t sa_mask;                               // 执行处理器期间额外屏蔽的信号
        int      sa_flags;                              // 行为标志
        void     (*sa_restorer)(void);                  // 已废弃 (glibc 内部使用)
    };
    

    sa_mask 的作用——阻止信号嵌套:

    // 默认:在处理 SIGTERM 期间,SIGTERM 被自动屏蔽(防止嵌套)
    // sa_mask 可以额外屏蔽其他信号
    struct sigaction act = {0};
    sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask, SIGINT);   // 处理 SIGTERM 时也屏蔽 SIGINT
    sigaddset(&act.sa_mask, SIGQUIT);  // 也屏蔽 SIGQUIT
    act.sa_handler = handler;
    sigaction(SIGTERM, &act, NULL);
    

    # 5.3.2 SA_SIGINFO 与 siginfo_t 扩展信息

    设置 SA_SIGINFO 后,处理器使用三参数签名,能获取丰富的上下文信息:

    void sig_handler(int sig, siginfo_t *info, void *ucontext) {
        printf("Signal: %d, sender PID: %d, addr: %p\n",
               sig, info->si_pid, info->si_addr);
    }
    

    siginfo_t 的关键字段:

    字段 含义 适用信号
    si_signo 信号编号 所有
    si_code 信号来源代码(SI_USER/SI_KERNEL/SEGV_MAPERR 等) 所有
    si_pid 发送进程的 PID SI_USER / SI_QUEUE
    si_uid 发送进程的 UID SI_USER / SI_QUEUE
    si_addr 引发异常的地址 SIGSEGV, SIGBUS
    si_status 子进程退出状态 SIGCHLD
    si_value sigqueue 传递的值 实时信号

    ucontext 参数——访问信号发生时的 CPU 寄存器:

    #include <ucontext.h>
    
    void crash_handler(int sig, siginfo_t *info, void *ucontext) {
        ucontext_t *uc = (ucontext_t *)ucontext;
        // x86_64 下获取 RIP(PC):
        void *ip = (void *)uc->uc_mcontext.gregs[REG_RIP];
        // ARM 下获取 PC:
        // void *pc = (void *)uc->uc_mcontext.arm_pc;
    
        printf("CRASH at IP=%p, addr=%p\n", ip, info->si_addr);
        // 可写入 crash log 文件用于事后分析
        _exit(128 + sig);  // 只能用 _exit,不能用 exit!
    }
    

    # 5.3.3 SA_RESTART 与被中断的系统调用

    慢系统调用(read/write/sleep/select/poll/wait)被信号中断后有两种行为:

    标志 被信号中断后
    未设 SA_RESTART 系统调用返回 -1,errno = EINTR
    设 SA_RESTART 内核自动重启系统调用(继续等)
    // 没有 SA_RESTART
    char buf[1024];
    int n = read(fd, buf, sizeof(buf));
    if (n < 0 && errno == EINTR) {
        // 被信号中断,要手动重试
        n = read(fd, buf, sizeof(buf));
    }
    
    // 有 SA_RESTART
    struct sigaction act = {.sa_handler = handler, .sa_flags = SA_RESTART};
    sigaction(SIGTERM, &act, NULL);
    int n = read(fd, buf, sizeof(buf));  // 被中断后内核自动重试
    

    SA_RESTART 不能重启所有调用——以下调用永远不被重启(POSIX 规定):

    poll, ppoll, select, pselect, epoll_wait, epoll_pwait,
    sleep, nanosleep, clock_nanosleep,
    connect (对 socket), accept
    

    嵌入式最佳实践——不依赖 SA_RESTART,永远手动检查 errno == EINTR:

    while ((n = read(fd, buf, sizeof(buf))) < 0) {
        if (errno == EINTR) continue;   // 被信号中断,重试
        perror("read");
        break;                          // 真正的错误
    }
    

    # 5.3.4 SA_NODEFER/SA_ONSTACK

    SA_NODEFER——默认在信号处理器执行期间,当前信号被自动屏蔽(防止递归)。设置此标志取消自动屏蔽,允许信号嵌套:

    // 一般不推荐——递归信号处理器容易堆栈溢出
    act.sa_flags |= SA_NODEFER;
    

    SA_ONSTACK——在独立栈上执行信号处理器(必设!):

    // 分配信号处理器专用栈
    stack_t ss;
    ss.ss_sp = malloc(SIGSTKSZ);        // 8-16 KB 足够
    ss.ss_size = SIGSTKSZ;
    ss.ss_flags = 0;
    sigaltstack(&ss, NULL);
    
    // 告知内核用此栈处理信号
    act.sa_flags |= SA_ONSTACK;
    sigaction(SIGSEGV, &act, NULL);
    

    为什么需要 SA_ONSTACK?

    场景:线程栈已满(stack overflow)→ 触发 SIGSEGV
          没有独立信号栈 → 内核试图在已满的栈上执行 SIGSEGV 处理器
          → 又触发 SIGSEGV → 嵌套 → 进程直接终止(coredump 不完整)
    
    有 SA_ONSTACK: 内核切换到备用栈 → 信号处理器正常执行 → 可以记录 crash log
    

    # 5.4 关键信号的嵌入式实战

    # 5.4.1 SIGSEGV 崩溃捕获与 core dump

    嵌入式崩溃捕获的标准流程:

    static void crash_handler(int sig, siginfo_t *info, void *ucontext) {
        // 步骤 1:立即屏蔽所有信号 —— 防止嵌套崩溃
        sigset_t all;
        sigfillset(&all);
        sigprocmask(SIG_BLOCK, &all, NULL);
    
        // 步骤 2:只调 async-signal-safe 函数写入 crash log
        int crash_fd = crash_fd_global;   // 在 main 中提前 open 好的
        char buf[256];
        int len = snprintf(buf, sizeof(buf),
                           "CRASH: sig=%d addr=%p pid=%d\n",
                           sig, info->si_addr, getpid());
        write(crash_fd, buf, len);        // ← write 是信号安全的!
    
        // 步骤 3:如果需要 coredump,恢复默认处置后给自己重新发同信号
        signal(sig, SIG_DFL);
        raise(sig);                       // ← 触发默认行为:coredump
        // 此时代码不会执行到这里
    }
    
    // 在 main 中初始化 crash fd 和备用栈
    int crash_fd;
    int main() {
        crash_fd = open("/data/crash.log", O_WRONLY | O_CREAT | O_APPEND, 0644);
    
        struct sigaction act = {0};
        act.sa_sigaction = crash_handler;
        act.sa_flags = SA_SIGINFO | SA_ONSTACK;
        sigemptyset(&act.sa_mask);
        sigaddset(&act.sa_mask, SIGSEGV);
        sigaddset(&act.sa_mask, SIGBUS);
        sigaddset(&act.sa_mask, SIGFPE);
        sigaddset(&act.sa_mask, SIGILL);
        sigaddset(&act.sa_mask, SIGABRT);
    
        sigaction(SIGSEGV, &act, NULL);
        sigaction(SIGBUS, &act, NULL);
        sigaction(SIGFPE, &act, NULL);
        sigaction(SIGILL, &act, NULL);
        sigaction(SIGABRT, &act, NULL);
    
        // ...
    }
    

    嵌入式 core dump 配置:

    # 开启 coredump,限制大小
    ulimit -c unlimited
    
    # 指定 coredump 文件路径
    echo "/data/coredump/core.%e.%p.%t" > /proc/sys/kernel/core_pattern
    
    # 嵌入式系统(闪存空间有限)常用过滤
    echo 0x7f > /proc/self/coredump_filter  # 只 dump 匿名私有映射 + ELF 头
    

    # 5.4.2 SIGPIPE 的默认行为与嵌入式网络程序

    SIGPIPE 产生的条件:向一个已经断开连接的 socket/fd 写数据。

    int fd = socket(...); connect(fd, ...);
    // 对端关闭连接
    close(peer_fd);
    // 本端第一次写——触发 RST
    write(fd, buf, 10);     // 返回 -1, errno = ECONNRESET
    // 本端第二次写——触发 SIGPIPE(默认动作:杀死进程!)
    write(fd, buf, 10);     // ← 进程被 SIGPIPE 杀死!
    

    嵌入式设备上的典型事故:

    HMI 程序通过 socket 连后台服务器
    服务器端异常重启 → 所有客户端连接断开
    HMI 的定时器线程还在往 socket 写数据
    → 第一次写收到 RST
    → 第二次写触发 SIGPIPE → 整个 HMI 进程被杀
    → 司机面前的屏幕黑屏!
    

    三种修复方案:

    // 方案 A:忽略 SIGPIPE(全局生效)
    signal(SIGPIPE, SIG_IGN);
    // write 会返回 -1,errno = EPIPE
    
    // 方案 B:为 socket 设 MSG_NOSIGNAL(只影响此 socket)
    int flags = MSG_NOSIGNAL;
    send(fd, buf, len, flags);   // 用 send 代替 write
    // 或者:
    int set = 1;
    setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &set, sizeof(set));
    
    // 方案 C:检查 POLLRDHUP(epoll/poll 事件)提前感知对端断开
    struct pollfd pfd = {.fd = fd, .events = POLLIN | POLLRDHUP};
    

    建议:嵌入式网络程序永远在 main 第一行加 signal(SIGPIPE, SIG_IGN)。

    # 5.4.3 SIGCHLD 与子进程回收

    SIGCHLD 的默认动作是忽略——但这不是说子进程不会变僵尸。忽略 SIGCHLD 只影响信号处理器,不影响僵尸回收。

    // 方法 A:waitpid 循环回收(已在进程管理篇详述)
    void sigchld_handler(int sig) {
        while (waitpid(-1, NULL, WNOHANG) > 0);
    }
    
    // 方法 B:SIG_IGN —— 告诉内核自动回收(Linux 专有行为)
    signal(SIGCHLD, SIG_IGN);
    // 此后 fork 的子进程退出时 kernel 自动做 wait,无僵尸
    
    // 注意:SIG_IGN 与实际注册 SIG_IGN 后调用 waitpid 的区别
    // SIG_IGN + SIGCHLD: waitpid 返回 -1, errno = ECHILD(找不到子进程)
    

    SA_NOCLDWAIT——另一个自动回收选项:

    struct sigaction act = {.sa_handler = SIG_DFL, .sa_flags = SA_NOCLDWAIT};
    sigaction(SIGCHLD, &act, NULL);
    // 效果类似 SIG_IGN —— 内核自动回收,不产生僵尸
    

    # 5.4.4 SIGTERM/SIGINT 优雅退出模板

    "优雅退出"的完整状态机:

    收到 SIGTERM/SIGINT
      → 设 shutdown_flag = 1
      → 主循环退出
      → 通知各模块释放资源(每个模块有 N 秒的清理时间)
      → 保存配置(flush / fsync)
      → close 所有 fd(包括 socket)
      → 等待所有子线程 join
      → exit(0)
    
    volatile sig_atomic_t shutdown = 0;
    
    static void term_handler(int sig) { shutdown = 1; }
    
    int main() {
        struct sigaction act = {.sa_handler = term_handler};
        sigemptyset(&act.sa_mask);
        // 不设 SA_RESTART —— 让阻塞的 accept/select 返回 EINTR
        sigaction(SIGTERM, &act, NULL);
        sigaction(SIGINT, &act, NULL);
    
        // 主循环
        while (!shutdown) {
            int fd = accept(listen_fd, NULL, NULL);
            if (fd < 0) {
                if (errno == EINTR) continue;  // 被信号中断,检查 shutdown flag
                perror("accept"); break;
            }
            handle_client(fd);
        }
    
        // 延期清理
        printf("Shutting down...\n");
        save_config();
        join_all_threads();
        close_all_fds();
        return 0;
    }
    

    systemd 通知协议(sd_notify):

    // 在信号处理器中通知 systemd 正在关闭(systemd 会等足够长时间)
    sd_notify(0, "STOPPING=1");
    // 清理完后通知 systemd
    sd_notify(0, "STATUS=Shutdown complete");
    

    # 5.4.5 SIGSTOP/SIGCONT 与嵌入式调试

    SIGSTOP 是"进程暂停"神器——在 GDB 不适用或无法连接的嵌入式系统上,用信号做临时光效观察:

    # 暂停进程
    kill -STOP $(pidof hmi_app)
    
    # 此刻进程完全冻结(所有线程)
    # 可以安全地:
    #   - 读 /proc/<pid>/fd/* → 看打开的文件
    #   - 读 /proc/<pid>/maps → 看内存布局
    #   - 读 /proc/<pid>/status → 看 CPU 时间、内存占用
    #   - cat /proc/<pid>/stack → 看内核栈(卡在哪个系统调用)
    
    # 恢复运行
    kill -CONT $(pidof hmi_app)
    

    SIGCONT 的特殊行为:收到 SIGCONT 时,被 SIGSTOP/TSTP 暂停的进程自动恢复运行,且挂起的 SIGCONT 被丢弃(避免重复)。


    # 5.5 信号与线程

    # 5.5.1 进程级信号与线程级信号

    所有的信号都是进程级的——信号不直接"发给某个线程",而是"发给进程"。内核任选一个不阻塞该信号的线程来递送。

    线程选择规则:

    1. 优选当前没有阻塞该信号的线程
    2. 如果多个线程都不阻塞 → 任选其一(通常选主线程或最近运行的)
    3. 如果所有线程都阻塞 → 信号留在 pending 队列,直到有线程解除阻塞
    

    pthread_kill——向指定线程发信号:

    pthread_kill(thread_id, SIGTERM);   // 只向这个线程发送
    pthread_kill(thread_id, 0);         // 检测线程是否存在(不送信号)
    

    # 5.5.2 pthread_sigmask 线程信号屏蔽

    每个线程拥有独立的信号屏蔽字——可以不同线程屏蔽不同信号:

    sigset_t set;
    sigemptyset(&set);
    sigaddset(&set, SIGTERM);
    
    // 只有主线程响应 SIGTERM
    pthread_sigmask(SIG_BLOCK, &set, NULL);  // 工作线程屏蔽 SIGTERM
    pthread_sigmask(SIG_UNBLOCK, &set, NULL); // 主线程解除屏蔽
    

    sigprocmask vs pthread_sigmask:

    • sigprocmask 在多线程程序中行为未定义——永远只用 pthread_sigmask
    • pthread_sigmask 等价于单线程下的 sigprocmask

    # 5.5.3 专用信号处理线程的模式

    经典模式——把信号处理集中到一个线程:

    void *signal_thread(void *arg) {
        sigset_t set;
        sigfillset(&set);
        pthread_sigmask(SIG_BLOCK, &set, NULL);  // 本线程屏蔽所有信号
    
        int sig;
        while (1) {
            // sigwait 等待任意被屏蔽的信号
            if (sigwait(&set, &sig) == 0) {
                // 在这里安全地处理——已经不是异步信号上下文!
                if (sig == SIGTERM || sig == SIGINT) {
                    shutdown_flag = 1;
                    break;
                }
            }
        }
        return NULL;
    }
    
    int main() {
        // 步骤 1:阻塞 SIGTERM/SIGINT(防止意外递送给其他线程)
        sigset_t set;
        sigemptyset(&set);
        sigaddset(&set, SIGTERM);
        sigaddset(&set, SIGINT);
        pthread_sigmask(SIG_BLOCK, &set, NULL);
    
        // 步骤 2:创建信号处理线程(继承屏蔽集)
        pthread_t tid;
        pthread_create(&tid, NULL, signal_thread, NULL);
    
        // 步骤 3:创建其他工作线程
        // ...
    
        pthread_join(tid, NULL);
    }
    

    sigwait 的优势:信号在普通函数上下文中处理——可以调 printf、malloc、pthread_mutex_lock——完全没有 async-signal-safe 限制。


    # 5.6 signalfd:把信号变成文件描述符

    # 5.6.1 signalfd 与 select/epoll 集成

    signalfd 是 Linux 特有 API——把信号转换为文件描述符,可读时表示有信号到达:

    #include <sys/signalfd.h>
    
    // 步骤 1:阻塞需要用 signalfd 处理的信号
    sigset_t mask;
    sigemptyset(&mask);
    sigaddset(&mask, SIGTERM);
    sigaddset(&mask, SIGINT);
    sigprocmask(SIG_BLOCK, &mask, NULL);
    
    // 步骤 2:创建 signalfd
    int sfd = signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC);
    
    // 步骤 3:与 epoll 集成
    struct epoll_event ev = {.events = EPOLLIN, .data.fd = sfd};
    epoll_ctl(epfd, EPOLL_CTL_ADD, sfd, &ev);
    
    // 步骤 4:事件循环中读取信号
    while (1) {
        int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
        for (int i = 0; i < n; i++) {
            if (events[i].data.fd == sfd) {
                struct signalfd_siginfo fdsi;
                read(sfd, &fdsi, sizeof(fdsi));    // 读一个信号 ← 读完后自动清 pending
    
                if (fdsi.ssi_signo == SIGTERM) { graceful_shutdown(); }
            }
            // 处理其他 fd 事件...
        }
    }
    

    signalfd 与 epoll 集成的核心价值——信号和 I/O、socket、timer 都在同一个事件循环中统一处理,不需要异步信号上下文。

    # 5.6.2 signalfd 与 sigaction 的选择

    维度 sigaction signalfd
    可调用函数 仅 async-signal-safe 任何函数
    线程安全 复杂(需屏蔽 + 专用线程) 简单(epoll 天然线程局部)
    与 epoll 集成 ❌ 需额外机制 ✅ 原生
    信号排队 否(需实时信号) 自动排队
    信号丢失风险 高(传统信号不排队) 低(read 消费)
    复杂度 低(单信号简单处理时) 中(需要事件循环)
    可移植性 ✅ POSIX ❌ Linux 专有
    嵌入式适用 ✅ 传统脚本启动的 daemon ✅ 有 epoll 事件循环的嵌入式应用

    选择建议:

    单线程 daemon + 简单 SIGTERM 处理 → sigaction 足够
    有 epoll 事件循环的程序 → signalfd(统一 I/O 和信号)
    Qt 应用 → QSocketNotifier + signalfd
    纯嵌入式裸 socket 事件循环 → signalfd + epoll 最佳
    需跨平台移植 → sigaction
    

    # 5.7 关键结论与速查表

    核心结论五条:

    1. 信号处理器只做三件事:写 volatile sig_atomic_t 旗标、调 _exit()、调 write() 写 crash log。绝对不调 printf/malloc/exit
    2. SIGPIPE 是嵌入式网络程序的沉默杀手——main 第一行 signal(SIGPIPE, SIG_IGN) 是防御性标配
    3. SA_ONSTACK 是崩溃捕获的必选项——无备用栈时栈溢出 = 崩溃处理器自身也崩溃
    4. 多线程程序用 pthread_sigmask 屏蔽 + 专用线程 sigwait——告别 async-signal-safe 限制
    5. 有 epoll 就用 signalfd——信号和 I/O 统一事件循环,linux 专有但极其优雅

    async-signal-safe 函数速查表(信号处理器中可安全调用的函数):

    类别 函数
    退出 _exit
    I/O write, read, open, close, fsync
    信号 signal, sigaction, kill, raise, sigprocmask
    时间 time, clock_gettime
    进程 getpid, getppid, getuid, geteuid
    文件系统 unlink, rename, mkdir, rmdir, stat, chmod
    socket socket, bind, connect, accept, send, recv
    管道 pipe

    信号速查表:

    信号 编号 默认动作 典型用途 可捕捉
    SIGHUP 1 Term 终端断开 / 配置重载 ✅
    SIGINT 2 Term Ctrl+C ✅
    SIGQUIT 3 Core Ctrl+\ ✅
    SIGILL 4 Core 非法指令 ✅
    SIGABRT 6 Core abort() ✅
    SIGBUS 7 Core 总线错误(未对齐访问) ✅
    SIGFPE 8 Core 算术异常(除零) ✅
    SIGKILL 9 Term 强制杀死 ❌
    SIGSEGV 11 Core 段错误 ✅
    SIGPIPE 13 Term 写破裂管道/socket ✅
    SIGTERM 15 Term 优雅退出 ✅
    SIGCHLD 17 Ign 子进程状态改变 ✅
    SIGCONT 18 Cont 恢复暂停进程 ⚠️ 部分
    SIGSTOP 19 Stop 强制暂停 ❌
    SIGTSTP 20 Stop Ctrl+Z ✅
    SIGUSR1 10 Term 用户定义 1 ✅
    SIGUSR2 12 Term 用户定义 2 ✅
    SIGWINCH 28 Ign 终端窗口大小变化 ✅

    常见陷阱速查表:

    陷阱 根因 正确做法
    信号处理器中调 printf printf 内部有锁 → 死锁 用 write() + 自建缓冲区
    信号处理器中调 malloc malloc 内部有锁 → 死锁 用预分配内存或栈变量
    SIGPIPE 杀死进程 对端关闭后第二次写触发 SIGPIPE signal(SIGPIPE, SIG_IGN)
    SA_ONSTACK 未设 栈溢出信号处理器在已满栈执行 预先分配备用信号栈
    SA_RESTART 过度依赖 不是所有调用都能被重启 永远检查 errno == EINTR
    fork 后 SIGCHLD 僵尸 未 wait 子进程 SIGCHLD 处理器 + WNOHANG
    多线程下用 sigprocmask 行为未定义 用 pthread_sigmask

    下一步:在你的 Qt 应用中用 signalfd + QSocketNotifier 替换现有的 signal() 调用,实现信号与 Qt 事件循环的统一处理——然后验证 SIGTERM 下的配置保存是否 100% 可靠。


    延伸阅读:

    • man 7 signal —— 信号概览
    • man 2 sigaction —— 信号处理器注册
    • man 2 signalfd —— Linux 信号 fd 接口
    • man 7 signal-safety —— async-signal-safe 函数完整列表
    • 《The Linux Programming Interface》(Kerrisk) —— 第 20-22 章
    上次更新: 2026/07/12, 19:39:51
    线程并发深度解读
    进程间通信IPC

    ← 线程并发深度解读 进程间通信IPC→

    最近更新
    01
    audit
    07-27
    02
    C++入门教程全章思考题汇编
    07-24
    03
    12.技术团队建设能力
    07-21
    更多文章>
    Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 鄂ICP备2024073355号-1 | 鄂ICP备2024073355号
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式