第 21 章 信号:信号处理器 (Signals: Signal Handlers)

      +

      核心结论

      • handler 设计原则:尽量简单——两个常见模式:「设 flag 后 return」让主程序检查;「清理后 exit 或 siglongjmp」。

      • Reentrancy 与 async-signal-safe:handler 可能打断主程序中任何「非可重入函数」的中间状态;只有「async-signal-safe」函数可在 handler 中安全调用。

      • 非可重入函数清单:stdio 库(printf/scanf 等)、malloc/free、crypt、getpwnam、gethostbyname 等返回静态缓冲区的函数——handler 调用它们会破坏主程序状态或反之。

      • 全局变量与 sig_atomic_t:handler 与主程序共享变量必须 volatile sig_atomic_t——保证原子读写、防止编译器优化。

      • SA_SIGINFO 三参数 handlervoid handler(int sig, siginfo_t *info, void *ucontext)——info 含发送者 PID/UID、信号来源(硬件/软件/kill/sigqueue)、si_value 等。

      • 中断系统调用与 SA_RESTART:handler 打断阻塞系统调用时返回 -1 EINTR;某些系统调用自动重启;其他需要手动循环或使用 SA_RESTART 标志。

      本章主旨

      本章深入信号处理器的设计与工程实践。读者应掌握:handler 设计的两大模式(设 flag / 清理后退出)、为什么必须避免非可重入函数、volatile sig_atomic_t 全局变量的正确用法、sigsetjmp/siglongjmp 实现 nonlocal goto、sigaltstack 替代栈处理栈溢出场景、SA_SIGINFO 三参数 handler 获取发送者信息、被中断系统调用的 EINTR 处理与 SA_RESTART 机制。这些是写「健壮信号驱动代码」的必备知识。

      一、核心概念

      本章围绕 6 个核心概念展开:从 handler 设计哲学入手,到 reentrancy/async-signal-safe、共享变量、nonlocal goto、altstack、SA_SIGINFO,最后是系统调用中断与自动重启。

      概念 定义 + 重要性 实现提示

      handler 设计哲学

      「尽量简单」是金科玉律;两大模式:set flag + return(主程序检查)、clean-up + exit/siglongjmp;handler 是异步执行的独立「逻辑线程」。

      §21.1;handler 可在任何指令边界打断主程序;可能与主程序并发访问全局变量;可能调用非可重入函数导致状态破坏。

      Reentrancy 与 async-signal-safe

      Reentrant:函数被多线程同时调用结果正确;async-signal-safe:从 handler 调用也安全;非可重入函数清单:stdio、malloc/crypt/getpwnam 等。

      §21.1.2;POSIX.1-2008 (SUSv4) 表 B 列出了 118+ 个 async-signal-safe 函数;常见非安全:printf、malloc、exit(exit 是安全的,但 _exit 与 abort 是安全的)。

      全局变量与 sig_atomic_t

      handler 与主程序共享的变量必须 volatile sig_atomic_t;sig_atomic_t 保证读写原子(通常是 int);volatile 防止编译器优化到寄存器。

      §21.1.3;只适合单变量 flag;复杂数据结构需要 mutex/lock,但 mutex 非 async-signal-safe——只能在 handler 中 lock、main 中 unlock。

      siglongjmp/sigsetjmp (Nonlocal goto)

      sigsetjmp(env, savemask) 保存上下文与信号掩码;siglongjmp(env, val) 跳转;比 longjmp 安全——不丢失被阻塞信号。

      §21.1/§21.2.1;典型用法:handler 中 siglongjmp 跳回主循环入口;用于「在深层调用栈中响应信号后回到预定位置」。

      SA_SIGINFO 三参数 handler

      void handler(int sig, siginfo_t *info, void *ucontext);info 含 si_signo、si_pid(发送者 PID)、si_uid、si_code(来源)、si_value(伴随数据,实时信号)。

      §21.4;si_code 区分 SIGCHLD 的具体原因(CLD_EXITED/CLD_KILLED/CLD_STOPPED/CLD_CONTINUED);区分硬件 vs 软件触发。

      系统调用中断与 SA_RESTART

      handler 打断阻塞系统调用时返回 -1 + errno = EINTR;BSD socket 有 SO_RESTART 自动重启;其他调用可手动循环或用 SA_RESTART 让 sigaction 自动重启。

      §21.5;不是所有阻塞系统调用都自动重启——详见 signal(7) 手册「Interruption of system calls」。

      二、详细笔记

      21.1 handler 设计原则

      What:handler 是「信号投递时被自动调用的函数」;可中断主程序任何位置;设计原则是「尽量简单」。

      Why:复杂的 handler 易引入 race 与状态破坏——handler 与主程序形成「两个独立的逻辑线程」访问相同数据。

      How:两大模式:

      模式 适用场景 示例

      set flag + return

      主程序周期性检查 flag 并处理

      SIGTERM 标志 + 主循环退出检查

      clean-up + exit/siglongjmp

      handler 中必须做清理(释放锁、删除临时文件)

      SIGINT 清理 + longjmp 回主循环

      模式 1 适用于简单事件通知;模式 2 适用于错误恢复——siglongjmp 把控制权直接交回主程序预定位置,跳过中间所有栈帧。

      handler 应避免:

      • 调用 stdio(printf、fprintf 等)——非 async-signal-safe。

      • 调用 malloc/free——维护链表,中间状态被破坏。

      • 调用非可重入的库函数(crypt、getpwnam 等)——返回静态缓冲区。

      • 长时间操作——handler 期间阻塞其他信号投递。

      When

      • 写需要「立即处理」的 handler——exit/siglongjmp。

      • 写需要「延迟处理」的 handler——set flag + 主程序检查。

      • 写需要「事件计数」的 handler——sig_atomic_t counter(注意标准信号不排队,可能漏计)。

      Example:模式 1——「set flag」:

      static volatile sig_atomic_t got_sigterm = 0;
      static void handler(int sig) { got_sigterm = 1; }
      int main(void) {
          signal(SIGTERM, handler);
          while (!got_sigterm) { /* 主工作 */ }
          cleanup();
          return 0;
      }

      21.1.2 Reentrancy 与 async-signal-safe

      What:reentrant 函数可被多线程同时调用,结果仍正确;async-signal-safe 函数可从 handler 安全调用——前者是后者的子集。

      Why:handler 可在主程序任何指令边界打断——如果主程序正调用 malloc(维护空闲块链表),handler 又调 malloc,链表被破坏。

      How:非可重入的常见情形:

      • 维护全局/静态数据结构(malloc 的空闲链表、stdio 的缓冲区)。

      • 返回静态缓冲区指针(crypt、getpwnam、gethostbyname、getservbyname)。

      • 调用了其他非可重入函数。

      POSIX.1-2008(与 SUSv4)Table B 列出 async-signal-safe 函数——118+ 个;常见的有:

      • _exit_Exitabortacceptaccessaio_erroraio_returnaio_suspendalarmbindcfgetispeedcfgetospeedcfsetispeedcfsetospeedchdirchmodchownclock_gettimecloseconnectcreatdupdup2execleexecvefaccessatfchdirfchmodfchmodatfchownfchownatfcntlfdatasyncfexecveforkfpathconffstatfstatatfsyncftruncatefutimensgetegidgeteuidgetgidgetgroupsgetpeernamegetpgrpgetpidgetppidgetsocknamegetsockoptgetuidkilllinklinkatlistenlseeklstatmkdirmkdiratmkfifomkfifoatmknodmknodatopenopenatpathconfpausepipepollposix_trace_eventpselectpthread_killpthread_selfpthread_sigmaskreadreadlinkreadlinkatrecvrecvfromrecvmsgrenamerenameatrmdirselectsem_postsendsendmsgsendtosetgidsetpgidsetsidsetsockoptsetuidshutdownsigactionsigaddsetsigdelsetsigemptysetsigfillsetsigismembersigpendingsigprocmasksigqueuesigsetsigsuspendsleepsockatmarksocketsocketpairstatsymlinksymlinkattcdraintcflowtcflushtcgetattrtcgetpgrptcsendbreaktcsetattrtcsetpgrptimetimer_getoverruntimer_gettimetimer_settimetimesumaskunameunlinkunlinkatutimeutimensatutimeswaitwaitpid、`write`等。

      printfmallocexit 不在清单内——handler 中调用它们可能产生「奇怪输出」「崩溃」「数据破坏」。

      When:在 handler 中输出调试信息——用 write(STDOUT_FILENO, msg, len)(write 是 async-signal-safe)。

      Example:handler 中安全输出:

      static void handler(int sig) {
          const char msg[] = "Ouch!\n";
          write(STDOUT_FILENO, msg, sizeof(msg) - 1);
      }

      21.1.3 全局变量与 sig_atomic_t

      What:handler 与主程序共享的全局变量必须声明为 volatile sig_atomic_t;保证读写原子、防止编译器优化。

      Why:没有 volatile,编译器可能把变量缓存到寄存器,导致 handler 修改「看不见」;没有 sig_atomic_t,对 int/long 的读写在某些架构上非原子(虽然 x86 上 int 读写原子,但标准要求可移植)。

      How

      static volatile sig_atomic_t got_signal = 0;
      static void handler(int sig) { got_signal = 1; }
      int main(void) {
          /* ... */
          while (!got_signal) { /* 主循环 */ }
      }

      注意:

      • sig_atomic_t 在 Linux 上通常是 int;只能存「单一标志」或计数器。

      • 复合状态(多字段结构体)需用 mutex 或其他同步——但 mutex 不是 async-signal-safe(handler 中 lock 会死锁主程序已持有的锁)。

      • 真实工程中常通过「handler 写 1 字节到专用 pipe,主程序在 select 中读」避免共享变量。

      When:最简单的「标志位」传递;状态机或多变量同步应用其他机制。

      Example:第 20 章 Listing 20-7 sig_receivergotSigint 变量即为此模式。

      21.2 sigsetjmp/siglongjmp(Nonlocal goto)

      Whatsigsetjmp(env, savemask) 保存上下文(含信号掩码);siglongjmp(env, val) 跳转回;handler 中调用 siglongjmp 直接回到主程序预定位置,跳过中间栈帧。

      Why:相比 longjmp,sigsetjmp 在 SIG_SETMASK 模式下保存信号掩码——避免「handler 修改了信号掩码,longjmp 后旧值丢失」的问题。

      How

      #include <setjmp.h>
      static sigjmp_buf env;
      static volatile sig_atomic_t can_jump = 0;
      
      static void handler(int sig) {
          if (!can_jump) return;                  /* 防伪唤醒 */
          siglongjmp(env, 1);                    /* 跳回 setjmp,val=1 */
      }
      
      int main(void) {
          signal(SIGINT, handler);
          if (sigsetjmp(env, 1) == 0) {
              can_jump = 1;
              /* 临界代码——Ctrl-C 后会回到这里 */
              long_running_call();
          } else {
              /* 从 handler 跳转回来的分支 */
              printf("Interrupted\n");
          }
      }

      can_jump 防伪唤醒:handler 不能在 sigsetjmp 之前执行——避免 env 未初始化时的非法跳转。

      When

      • handler 需要「逃离」深层调用栈——典型如 sleep/read/write 中响应信号。

      • 错误恢复——handler 中清理资源后回到主循环入口。

      Example:第 21 章的 siglongjmp.c——Ctrl-C 终止 long_running_call 回到主循环。

      21.3 sigaltstack 替代栈

      Whatsigaltstack 让 handler 在「替代栈」上运行——处理栈溢出场景(如 SIGSEGV 的 handler 中不能访问正常栈)。

      Why:若 handler 触发的信号(如 SIGSEGV)是因为栈溢出——在溢出栈上调用 handler 会再次溢出;sigaltstack 提供「备用」栈。

      How

      #include <signal.h>
      static char altstack[SIGSTKSZ];            /* 替代栈空间 */
      
      stack_t ss = {
          .ss_sp = altstack,
          .ss_flags = 0,
          .ss_size = sizeof(altstack)
      };
      sigaltstack(&ss, NULL);                    /* 安装替代栈 */
      
      struct sigaction sa = {0};
      sa.sa_handler = handler;
      sa.sa_flags = SA_ONSTACK;                  /* 使用替代栈 */
      sigaction(SIGSEGV, &sa, NULL);

      When

      • 在 SIGSEGV handler 中执行(栈可能已溢出)。

      • 调试栈深度——若主线程栈空间不足,可用替代栈。

      Example:第 21 章 Listing 21-2 演示 altstack 用于 SIGSEGV 处理。

      21.4 SA_SIGINFO 三参数 handler

      What:设置 SA_SIGINFO 标志后,handler 接收三个参数:void handler(int sig, siginfo_t *info, void *ucontext);info 含发送者 PID/UID、信号来源、伴随数据等。

      Why:基础 handler 只知道「信号几号」——SA_SIGINFO 让 handler 能区分「谁发的」「为什么发的」「伴随什么数据」。

      How

      struct sigaction sa = {0};
      sa.sa_sigaction = handler;                 /* 不是 sa_handler */
      sa.sa_flags = SA_SIGINFO;
      sigaction(SIGUSR1, &sa, NULL);
      
      static void handler(int sig, siginfo_t *info, void *ucontext) {
          printf("SIGUSR1 from PID %d, UID %d, code = %d\n",
                 info->si_pid, info->si_uid, info->si_code);
      }

      siginfo_t 关键字段:

      • si_signo —— 信号号(与第一参数相同)。

      • si_pid / si_uid —— 发送者 PID / UID(kill/sigqueue 时设置)。

      • si_code —— 信号来源:SI_USER(kill)、SI_KERNEL(内核)、SI_QUEUE(sigqueue)、SI_TIMER(定时器)、SI_ASYNCIO(异步 I/O)、SI_MESGQ(消息队列)等。

      • si_value —— 伴随数据(仅实时信号 sigqueue 时携带,sival_int 或 sival_ptr)。

      • si_status —— 子进程退出状态(SIGCHLD)。

      • si_addr —— 触发硬件异常的地址(SIGSEGV/SIGBUS)。

      ucontext 含寄存器上下文——通常用 ucontext_t 取得信号发生时的 CPU 状态。

      When

      • daemon 想区分「SIGHUP 来自内核(终端挂起)还是来自管理员手动 kill」——查 si_code

      • 实时信号——si_value 携带应用自定义数据。

      • SIGCHLD handler——si_code 区分 CLD_EXITED/CLD_KILLED/CLD_STOPPED/CLD_CONTINUED

      Example:SIGCHLD 用 SA_SIGINFO 区分子进程退出原因:

      if (info->si_code == CLD_EXITED) { printf("正常退出: status=%d\n", info->si_status); }
      else if (info->si_code == CLD_KILLED) { printf("被信号杀死: signo=%d\n", info->si_status); }
      else if (info->si_code == CLD_STOPPED) { /* 子进程停止 */ }

      21.5 系统调用中断与 SA_RESTART

      What:handler 打断阻塞系统调用时,内核让系统调用立即返回 -1 + errno = EINTR;BSD socket 有 SO_RESTART 自动重启;Linux 默认部分调用自动重启;用 SA_RESTART 标志可让 sigaction 自动重启其他调用。

      Why:理解 EINTR 行为是写「信号驱动 I/O」的必备——否则 read 返回 EINTR 会让程序误以为出错。

      How

      /* 手动循环处理 EINTR */
      ssize_t n;
      do {
          n = read(fd, buf, sizeof(buf));
      } while (n == -1 && errno == EINTR);
      
      /* 用 SA_RESTART 让 sigaction 自动重启 */
      sa.sa_flags = SA_RESTART;
      sigaction(SIGINT, &sa, NULL);

      Linux 默认行为(无 SA_RESTART 时):

      • read/write/open(文件、设备、管道)——某些自动重启。

      • poll/epoll_wait/select/sleep/nanosleep/clock_nanosleep——不自动重启。

      • accept/connect/send/recv/sendmsg/recvmsg(BSD socket)——不自动重启。

      • ioctl/fcntl/wait/waitpid——通常不重启。

      When

      • 写库代码——不要依赖默认重启行为,应手动检查 EINTR。

      • 简单事件循环——poll 返回 EINTR 通常代表「无事发生」,主程序应继续循环。

      • 想让代码简单——用 SA_RESTART(注意:不是所有阻塞系统调用都支持,见 signal(7))。

      Example:第 21 章 multi_sig.c 演示多个信号打断 read;handler 返回后 read 失败 EINTR——程序需手动循环。

      三、关键图表

      (本章无独立编号图表)

      async-signal-safe 函数分类
      类别 典型函数

      进程控制

      fork, execve, _exit, getpid, getuid, kill, pause, sigaction, sigprocmask, sleep, wait

      文件 I/O

      open, close, read, write, dup, dup2, lseek, stat, fstat, fsync, fdatasync, access, link, unlink

      套接字

      socket, bind, listen, accept, connect, send, recv, sendmsg, recvmsg, select, poll, shutdown, getsockname, getpeername, setsockopt, getsockopt, socketpair

      信号操作

      sigemptyset, sigfillset, sigaddset, sigdelset, sigismember, sigpending, sigsuspend

      终端 I/O

      tcgetattr, tcsetattr, tcdrain, tcflush, tcflow, tcgetpgrp, tcsetpgrp

      时间

      time, clock_gettime, nanosleep

      IPC

      pipe, mkfifo, sem_post

      内存/同步

      (少) — mutex/cond 非 async-signal-safe

      sigaction sa_flags 速查
      标志 含义

      SA_NOCLDSTOP

      sig=SIGCHLD 时,子进程 stop 不发信号

      SA_NOCLDWAIT

      sig=SIGCHLD 时,子进程终止不变成僵尸

      SA_NODEFER

      handler 调用时不让触发信号自动进入掩码

      SA_ONSTACK

      handler 在 sigaltstack 提供的栈上运行

      SA_RESETHAND

      handler 调用时把信号处置恢复为默认

      SA_RESTART

      自动重启被 handler 中断的系统调用

      SA_SIGINFO

      使用三参数 handler(sa_sigaction)

      四、思维导图

      mindmap
        root((第 21 章 信号处理器))
          设计原则
            简单优先
            set flag 模式
            cleanup 退出模式
            异步逻辑线程
          Reentrancy
            可重入函数
            async signal safe
            非可重入清单
            stdio malloc
            crypt getpwnam
            write 替代 printf
          全局变量
            volatile 必要
            sig atomic t
            flag 标志
            counter 计数
            pipe 通信
          siglongjmp
            sigsetjmp env
            savemask 1
            跳过中间栈帧
            can jump 防伪唤醒
          sigaltstack
            SIGSTKSZ 大小
            SA ONSTACK
            栈溢出场景
            SIGSEGV handler
          SA_SIGINFO
            三参数 handler
            si pid si uid
            si code 来源
            si value 实时数据
            SIGCHLD 区分
          系统调用中断
            EINTR 错误
            BSD SO RESTART
            SA RESTART 标志
            手动循环
            poll 不自动重启

      五、重点与易错点

      1. handler 尽量简单——复杂 handler 易引入 race;两大模式:set flag、cleanup + exit/siglongjmp;handler 是异步执行的「独立逻辑线程」。

      2. 非可重入函数清单——stdio(printf/scanf)、malloc/free、crypt、getpwnam、gethostbyname、getservbyname 等;handler 调用它们会破坏主程序状态或反之。

      3. handler 中输出用 write——write(STDOUT_FILENO, msg, len);printf/sprintf 在 handler 中不安全——可能产生乱码、崩溃、数据破坏。

      4. sig_atomic_t + volatile——共享变量必须 volatile sig_atomic_t;sig_atomic_t 保证原子读写,volatile 防止编译器优化到寄存器;只适合单变量 flag。

      5. sigsetjmp + siglongjmp——比 longjmp 安全(保存信号掩码);典型用法:handler 中 siglongjmp 跳回主循环;用 can_jump 标志防伪唤醒。

      6. SA_SIGINFO 三参数 handler——sa_sigaction(不是 sa_handler);info 含发送者 PID/UID(si_pid/si_uid)、信号来源(si_code)、伴随数据(si_value);区分 SIGCHLD 子进程退出原因用 si_code。

      7. sigaltstack 处理栈溢出场景——SIGSEGV handler 不能在溢出栈上运行;sigaltstack + SA_ONSTACK 提供替代栈;典型用于栈检查、调试。

      8. SIGCHLD handler 与僵尸进程——handler 中调 wait 回收;不显式回收的子进程会变僵尸;SA_NOCLDWAIT 让内核自动回收(不产生僵尸)。

      9. EINTR 处理——handler 打断阻塞系统调用时返回 -1 + EINTR;BSD socket 默认不重启;Linux 文件 I/O 部分自动重启;最稳妥是手动循环;用 SA_RESTART 标志让 sigaction 自动重启支持的系统调用。

      10. 不要依赖系统调用自动重启——库代码必须手动处理 EINTR;不同 UNIX 默认行为差异大;用 SA_RESTART 也仅支持部分调用(见 signal(7))。

      11. 硬件异常 handler 不能正常返回——SIGSEGV/SIGBUS/SIGFPE/SIGILL 正常返回会再次触发原指令;正确处理是 _exitsiglongjmp;详见第 22 章。

      12. siglongjmp 跳到构造函数 / 中间状态——可能绕过析构/清理逻辑(特别是 C++);信号驱动代码应仔细设计状态机。

      13. 「终止」与「退出」——handler 中 _exit(EXIT_FAILURE) 立即终止(不调用 atexit);exit() 会执行 atexit handler,可能不安全。

      14. 跨章衔接:第 20 章信号基础;第 22 章硬件异常处理、core dump、sigsuspend 原子等待;第 26 章 SIGCHLD 与 wait 配合回收僵尸;第 29 章多线程信号;第 34 章 job control 与 SIGHUP/SIGTSTP。