第 22 章 信号:高级特性 (Signals: Advanced Features)

      +

      核心结论

      • core dump 文件:默认写到 CWD 的 core;可由 /proc/sys/kernel/core_pattern 控制(含格式说明符与 |program 模式);ulimit -c / RLIMIT_CORE 控制大小。

      • 实时信号(SIGRTMIN..SIGRTMAX):Linux 32-63;可排队、可携带数据(sigval)、投递顺序按编号(低编号优先);用 sigqueue() 发送,SA_SIGINFO handler 接收。

      • sigsuspend 原子等待sigsuspend(mask) = 原子地「设置 mask + pause」+ handler 返回后恢复原 mask;解决「sigprocmask + pause」之间的 race。

      • sigwaitinfo 同步等待:替代「handler + sigsuspend」组合——无 handler、同步接收、siginfo_t 信息完整;适合「主线程独占处理信号」的多线程程序。

      • signalfd 文件描述符化信号:把信号转为可读事件,可与 select/poll/epoll 整合;Linux 2.6.22+。

      • 硬件异常的特殊规则:SIGSEGV/SIGBUS/SIGFPE/SIGILL 返回 handler 通常会再次触发原指令;Linux 2.6+ 上若被阻塞则立即终止。

      本章主旨

      本章是信号三章的最后一章,覆盖「高级主题」——core dump、硬件异常、同步信号、信号投递顺序、EINTR 与 SA_RESTART 深入、实时信号、sigsuspend 原子等待、sigwaitinfo 同步等待、signalfd 文件描述符化信号、以及 BSD/SysV 旧 signal API 简史。读者应掌握实时信号与标准信号的本质区别(可排队 + 携带数据 + 投递有序)、sigsuspend 为什么是「正确」的等待模式、signalfd 如何把信号整合进事件循环。这些是写「现代 Linux 信号驱动代码」的必备知识。

      一、核心概念

      本章围绕 7 个核心概念展开:从 core dump 入手,到硬件异常与同步信号、信号投递顺序与可靠性历史、实时信号、sigsuspend/sigwaitinfo 高级等待,最后是 signalfd。

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

      core dump 文件

      进程异常终止时生成的内存镜像;用于事后调试;默认写到 CWD 的 core;/proc/sys/kernel/core_pattern 控制命名与位置(含格式说明符与 | 起始的 program 模式)。

      §22.1;触发条件:ulimit -c unlimited + set-user-ID 进程不被其他用户 dump;调试:gdb + gcore 取运行中进程 core。

      硬件异常信号(同步信号)

      SIGBUS/SIGFPE/SIGILL/SIGSEGV/SIGSYS——硬件异常触发;同步、可预测;不能正常 return(会再次触发);Linux 2.6+ 阻塞时立即终止。

      §22.4/§22.5;正确处理:_exitsiglongjmp;handler 中不要依赖原上下文。

      实时信号

      SIGRTMIN..SIGRTMAX(Linux 32-63);可排队(多次发送多次投递)、可携带数据(sigval)、投递顺序按编号;用 sigqueue() 发送,SA_SIGINFO 接收。

      §22.8;SIGRTMIN/SIGRTMAX 可能是函数(Linux 上)——不能在预处理中使用;SIGQUEUE_MAX 限制队列长度(Linux 2.6.8+ 用 RLIMIT_SIGPENDING)。

      sigsuspend 原子等待

      sigsuspend(mask) 原子地「设置 mask + pause」+「handler 返回后恢复原 mask」;解决「sigprocmask + pause」之间的 race。

      §22.9;正确模式:sigprocmask 阻塞 → 临界区 → sigsuspend(原 mask);不要用 pause + 两段 sigprocmask。

      sigwaitinfo 同步等待

      sigwaitinfo(set, info) 同步阻塞直到 set 中某信号到达;返回信号号 + siginfo_t;无需 handler;适合多线程程序。

      §22.10;通常配合 sigprocmask 阻塞相关信号——否则信号会被 handler 截走;sigtimedwait 加超时;比「handler + sigsuspend」略快。

      signalfd 文件描述符化信号

      signalfd(fd, mask, flags) 把信号转为文件描述符——可读事件;与 select/poll/epoll 整合;Linux 2.6.22+。

      §22.11;需先 sigprocmask 阻塞信号;典型模式:signalfd + epoll,单线程处理多源事件。

      signal() 历史与可移植性

      signal() 在 4.2BSD 之前不可靠(handler 自动复位、不自动阻塞当前信号);System V 沿用旧语义;BSD 引入「可靠信号」;POSIX.1-1990 基于 BSD 模型。

      §22.7;推荐始终用 sigaction()——跨平台一致;signal() 只用于 SIG_DFL/SIG_IGN 设置。

      二、详细笔记

      22.1 core dump 文件

      What:core dump 是进程异常终止时的内存镜像文件;默认写到 CWD,文件名 core;可用 gdb 加载查看程序崩溃时的状态。

      Why:事后调试的核心手段——崩溃现场无法重现时,core dump 是「程序死前最后一帧」。

      How:控制 core dump 的因素:

      • 触发信号:SIGABRT/SIGBUS/SIGFPE/SIGILL/SIGQUIT/SIGSEGV/SIGSYS/SIGTRAP/SIGXCPU/SIGXFSZ(默认 core)。

      • 不生成条件:无写权限、目标已是 hard link 数量 >1、目录不存在、RLIMIT_CORE=0RLIMIT_FSIZE=0、二进制无可读权限、FS 只读/满/无 i-node、set-user-ID 进程由非所有者执行。

      • 文件命名/proc/sys/kernel/core_pattern(含格式说明符 %p/%u/%g/%s/%t/%e/%h/%c);以 | 开头则把 core 通过 stdin 传给指定程序(适合 systemd-coredump)。

      • 进程级控制/proc/PID/coredump_filter(自 Linux 2.6.23)控制哪些类型的内存映射被 dump(private anonymous / shared anonymous / file-backed)。

      • SUID 控制/proc/sys/fs/suid_dumpable(0=禁止、1=允许、2=以 root 安全模式 dump)。

      • 进程 dumpable 标志prctl(PR_SET_DUMPABLE, 1)

      When:调试 set-user-ID 程序的崩溃——临时 prctl(PR_SET_DUMPABLE, 1) 或修改 /proc/sys/fs/suid_dumpable;自动化 core 收集——/proc/sys/kernel/core_pattern=|/path/to/handler

      Example

      $ ulimit -c unlimited            # 允许任意大小 core
      $ echo "core.%p" | sudo tee /proc/sys/kernel/core_pattern
      # 之后所有 core 文件名形如 core.12345

      22.2 特殊信号:SIGKILL/SIGSTOP/SIGCONT

      What:SIGKILL 与 SIGSTOP 不能被阻塞、捕获、忽略——保证管理员始终能 kill/stop;SIGCONT 在 stop 状态下即使被阻塞也强制投递。

      Why:避免失控进程通过安装 handler 逃脱管理;这是「运维底线」。

      How

      • SIGKILL / SIGSTOP:默认动作不可改;signal/sigaction 返回错误;sigprocmask 静默忽略。

      • SIGCONT:投递给 stopped 进程时,即使进程阻塞或忽略 SIGCONT 也会强制唤醒;唤醒后若 SIGCONT 之前被阻塞,handler 在解阻后调用。

      • SIGCONT 与 stop 信号互斥——投递 SIGCONT 时清空 pending stop 信号;投递 stop 信号时清空 pending SIGCONT。

      When:运维 kill 失控进程——kill -9 一定有效;shell 恢复后台作业——kill -CONT pgid

      22.3 进程睡眠状态:INTERRUPTIBLE/UNINTERRUPTIBLE/KILLABLE

      What:内核把阻塞的进程置于不同睡眠状态;信号能否打断取决于状态。

      Why:理解为什么有时 kill -9 杀不掉进程——它在 UNINTERRUPTIBLE 状态(典型如 NFS 挂死)。

      How

      • TASK_INTERRUPTIBLE (S)——等待事件(如 pipe 数据、信号量);可被信号打断。

      • TASK_UNINTERRUPTIBLE (D)——等待特定事件(如磁盘 I/O、NFS);不可被信号打断;kill -9 无效;卡住需重启系统。

      • TASK_KILLABLE(Linux 2.6.25+)——类似 UNINTERRUPTIBLE,但致命信号可唤醒;NFS 已改用此状态。

      Whenps aux 看到 D 状态的进程不能 kill;这是底层问题的征兆(FS 问题、驱动 hang)。

      22.4 硬件异常信号

      What:SIGBUS/SIGFPE/SIGILL/SIGSEGV/SIGSYS——硬件异常触发;同步、可预测、发生位置确定。

      Why:这些信号是「最后一道防线」——通常意味着 bug;handler 的设计需特别小心。

      How

      • 不要正常 return——返回会再次触发原指令(通常导致无限循环)。

      • 不要忽略——Linux 强制投递即使程序请求忽略。

      • 不要阻塞——Linux 2.4 及更早忽略阻塞请求;2.6+ 改为「阻塞时立即终止进程」(避免死锁)。

      • 正确处理:_exit(EXIT_FAILURE) 终止;或 siglongjmp 跳到已知安全位置。

      When

      • 调试器/动态分析工具——捕获 SIGSEGV/SIGBUS 转储寄存器。

      • 用户态内存保护——捕获 SIGSEGV 后 mmap 页面(demo 内存保护 demo)。

        • 防御性 attributeno_sanitize("address") 风格的 sanitizer——不依赖信号。

      Example:硬件异常处理——siglongjmp 跳过触发指令:

      static sigjmp_buf env;
      static void handler(int sig) { siglongjmp(env, 1); }
      int main(void) {
          signal(SIGSEGV, handler);
          volatile int *bad = NULL;
          if (sigsetjmp(env, 1) == 0) {
              *bad = 42;  /* 触发 SIGSEGV */
              printf("Should not reach here\n");
          } else {
              printf("Caught SIGSEGV via longjmp\n");
          }
      }

      22.5 同步 vs 异步信号

      What:同步信号——由进程自身执行触发(如硬件异常、raise/kill(getpid(), sig));异步信号——由其他进程或独立事件触发。

      Why:理解同步性才能写出可重现的测试与调试代码——同步信号发生位置可预测。

      How

      • 同步信号投递时机:触发指令执行后立即(在 raise 返回前、硬件异常 fault 后)。

      • 异步信号投递时机:进程下次从内核态返回用户态时(系统调用完成、调度点等)。

      When:调试时区分同步 vs 异步——同步信号现场可重现;异步信号可能在不同位置打断程序。

      22.6 信号投递顺序

      What:标准信号投递顺序 SUSv3 留作「实现定义」;Linux 按信号编号升序投递;实时信号保证「低编号优先」。

      Why:标准信号可能丢失(同号多次只投递一次),顺序不重要;实时信号保留所有实例,顺序对应用语义关键。

      How

      • Linux 标准信号:pending 解阻时按编号升序投递。

      • 实时信号:低编号先投;同号多个按发送顺序投递;siginfo_t.si_value 保留每条发送的伴随数据。

      When:选择信号类型——需要保留所有实例与投递顺序时用实时信号。

      22.7 signal() 的实现与可移植性

      What:早期 UNIX 信号不可靠——handler 自动复位、不自动阻塞当前信号;4.2BSD 引入「可靠信号」;POSIX.1-1990 基于 BSD 模型。

      Why:理解为什么推荐用 sigaction——signal() 在不同 UNIX 上语义不同。

      How:第 22 章 Listing 22-1 —— 用 sigaction 实现 signal:

      // 摘自《The Linux Programming Interface》第 22 章(Listing 22-1)
      sighandler_t signal(int sig, sighandler_t handler) {
          struct sigaction newDisp, prevDisp;
          newDisp.sa_handler = handler;
          sigemptyset(&newDisp.sa_mask);
      #ifdef OLD_SIGNAL
          newDisp.sa_flags = SA_RESETHAND | SA_NODEFER;  /* 旧 SysV 语义 */
      #else
          newDisp.sa_flags = SA_RESTART;                 /* 可靠信号语义 */
      #endif
          if (sigaction(sig, &newDisp, &prevDisp) == -1)
              return SIG_ERR;
          return prevDisp.sa_handler;
      }

      When:跨 UNIX 移植代码——不用 signal(),始终用 sigaction();signal() 仅用于 SIG_DFL/SIG_IGN。

      22.8 实时信号

      What:SIGRTMIN..SIGRTMAX(Linux 32-63);POSIX.1b 定义;解决标准信号三大限制:可排队、可携带数据、投递有序。

      Why:标准信号不排队——计数类应用必须用实时信号;进程间消息传递需要「数据伴随」。

      How

      // 发送:sigqueue(pid, sig, value)
      union sigval { int sival_int; void *sival_ptr; };
      int sigqueue(pid_t pid, int sig, const union sigval value);
      
      // 接收:SA_SIGINFO handler
      struct sigaction sa = {0};
      sa.sa_sigaction = handler;         /* 不是 sa_handler */
      sa.sa_flags = SA_SIGINFO | SA_RESTART;
      sigaction(SIGRTMIN + 1, &sa, NULL);
      
      // handler
      static void handler(int sig, siginfo_t *info, void *ucontext) {
          printf("got signal %d, value = %d, from PID %d\n",
                 sig, info->si_value.sival_int, info->si_pid);
      }

      实时信号特点:

      • 可排队——多次发送多次投递(直到 RLIMIT_SIGPENDING 上限)。

      • 可携带数据——sigval 联合(int 或 pointer);pointer 在跨进程时几乎无用,但 timerfd / mq_notify 等用得到。

      • 投递有序——低编号先投;同编号按发送顺序。

      • Linux 限制——/proc/sys/kernel/rtsig-max(旧);2.6.8+ 用 RLIMIT_SIGPENDING(每 UID 上限,默认通常 1024);超限 sigqueue 返回 EAGAIN

      When

      • 进程间状态机通知——「事件 A 发生了 N 次」用实时信号 + 计数。

      • 应用消息总线——sigval 携带小整数(命令或参数)。

      • POSIX 定时器——timer_create + 实时信号投递。

      Example:第 22 章 Listing 22-2 t_sigqueue.c——发送实时信号带数据:

      // 摘自《The Linux Programming Interface》第 22 章(Listing 22-2)
      union sigval sv;
      for (j = 0; j < numSigs; j++) {
          sv.sival_int = sigData + j;
          sigqueue(getLong(argv[1], 0, "pid"), sig, sv);
      }
      • SIGRTMIN/SIGRTMAX 在 Linux 上是函数(不是常量)——不能用在预处理(#if SIGRTMIN+5 > SIGRTMAX);运行时检查。

      22.9 sigsuspend 原子等待

      Whatsigsuspend(mask) 原子地「替换进程信号掩码为 mask → 阻塞直到信号被捕获 → handler 返回后恢复原掩码」。

      Why:解决「sigprocmask(SIG_SETMASK, &old) + pause()」之间的 race——信号可能在两调用之间到达并被 handler 截走,导致 pause 错过事件。

      How:第 22 章 Listing 22-5 t_sigsuspend.c —— 标准等待模式:

      // 摘自《The Linux Programming Interface》第 22 章(Listing 22-5 简化)
      sigset_t origMask, blockMask;
      sigfillset(&blockMask);
      sigdelset(&blockMask, SIGINT);    /* 不阻塞 SIGINT/SIGTERM */
      sigdelset(&blockMask, SIGTERM);
      
      sigprocmask(SIG_BLOCK, &blockMask, &origMask);  /* 阻塞多数信号 */
      
      while (!gotSigquit) {
          /* 临界区 */
          sigsuspend(&origMask);          /* 原子恢复原掩码 + 等待 */
          /* 此处是 handler 已返回 */
      }

      pause vs sigsuspend

      • pause() 简单但与 sigprocmask 配合有 race。

      • sigsuspend(mask) 是正确模式——原子「设置 mask + 等待 + 恢复」。

      When

      • 写「等待一个事件 + 屏蔽其他」的正确代码——用 sigsuspend。

      • 多线程——主线程 sigsuspend + 工作线程 sigwaitinfo 配合。

      Example:sigwaitinfo 也是「原子 + 同步」——本质上是更现代的 sigsuspend 替代。

      22.10 sigwaitinfo 同步等待

      Whatsigwaitinfo(set, info) 同步阻塞直到 set 中某信号到达;返回信号号 + siginfo_t;无需 handler。

      Why:避免「handler + 异步处理」的复杂度;适合「线程独占处理信号」的多线程程序。

      How

      sigset_t allSigs;
      sigfillset(&allSigs);
      sigprocmask(SIG_BLOCK, &allSigs, NULL);  /* 阻塞所有信号 */
      
      siginfo_t si;
      for (;;) {
          int sig = sigwaitinfo(&allSigs, &si);
          if (sig == SIGINT || sig == SIGTERM) break;
          printf("got signal %d from PID %ld, value = %d\n",
                 sig, (long) si.si_pid, si.si_value.sival_int);
      }

      注意:

      • 通常先 sigprocmask 阻塞相关信号——否则信号会被 handler 截走。

      • 返回时信号从 pending 集移除;siginfo_t 含发送者 PID/UID、si_code(SI_USER/SI_QUEUE 等)、si_value。

      • 比「handler + sigsuspend」略快——少一次上下文切换。

      相关函数:sigtimedwait(set, info, timeout)——带超时;sigqueue(pid, sig, value)——发送实时信号携带数据。

      When

      • 多线程程序——一个线程专门 sigwaitinfo 处理信号,其他线程不被信号打扰。

      • 同步事件循环——主线程 sigwaitinfo + 工作线程做事。

      Example:第 22 章 Listing 22-6 t_sigwaitinfo.c —— 阻塞后 sigwaitinfo 接收。

      22.11 signalfd 信号文件描述符化

      Whatsignalfd(fd, mask, flags) 把信号转为可读事件;与 select/poll/epoll 整合;Linux 2.6.22+。

      Why:传统「signal + handler」与事件循环(epoll)整合麻烦;signalfd 让信号像 I/O 事件一样处理——单线程事件循环统一处理「I/O + 信号」。

      How

      #include <sys/signalfd.h>
      sigset_t mask;
      sigemptyset(&mask);
      sigaddset(&mask, SIGINT);
      sigaddset(&mask, SIGTERM);
      
      /* 关键:必须先 sigprocmask 阻塞信号,否则信号仍按 handler 处理 */
      sigprocmask(SIG_BLOCK, &mask, NULL);
      
      int sfd = signalfd(-1, &mask, SFD_CLOEXEC);
      
      /* sfd 可加入 epoll */
      struct epoll_event ev = { .events = EPOLLIN, .data.fd = sfd };
      epoll_ctl(epfd, EPOLL_CTL_ADD, sfd, &ev);
      
      /* 事件循环 */
      while (1) {
          int n = epoll_wait(epfd, events, MAX, -1);
          for (i = 0; i < n; i++) {
              if (events[i].data.fd == sfd) {
                  struct signalfd_siginfo si;
                  read(sfd, &si, sizeof(si));
                  if (si.ssi_signo == SIGINT) goto cleanup;
              } else {
                  /* 其他 I/O */
              }
          }
      }

      When

      • 单线程事件循环要处理信号——用 signalfd 整合。

      • 多线程——主线程 signalfd + 工作线程做事。

      • 替代「signal + 自定义 pipe」方案。

      三、关键图表

      (本章无独立编号图表)

      core_pattern 格式说明符
      说明符 替换为

      %p

      进程 ID

      %u

      真实 UID

      %g

      真实 GID

      %s

      导致 dump 的信号号

      %t

      dump 时间(自 epoch 起秒数)

      %e

      可执行文件名(无路径前缀)

      %h

      主机名

      %c

      core 文件大小软资源限制(Linux 2.6.24+)

      %%

      字面量 %

      信号投递模式对比
      模式 优点 缺点

      signal + handler

      经典;可读性高;handler 内可访问上下文

      非 async-signal-safe;handler 写全局要 volatile sig_atomic_t

      sigsuspend

      正确原子等待;无 handler

      仍需 handler;与 select/poll 难整合

      sigwaitinfo

      同步;无 handler;siginfo_t 完整

      仍需 sigprocmask 阻塞;单线程独占

      signalfd

      与 epoll 整合;单线程事件循环

      Linux 2.6.22+;需 sigprocmask 阻塞

      四、思维导图

      mindmap
        root((第 22 章 信号高级特性))
          core dump
            core 文件
            core pattern 命名
            coredump_filter
            suid_dumpable
            |program 管道模式
          硬件异常
            SIGSEGV BUS FPE
            不能正常返回
            同步可预测
            Linux 2.6 阻塞则终止
          同步异步
            同步可预测
            异步不可预测
            内核态返回时投递
          实时信号
            SIGRTMIN SIGRTMAX
            可排队
            sigval 数据
            sigqueue 发送
            SA SIGINFO 接收
            投递有序
          投递顺序
            标准信号升序
            实时信号低号优先
            同号按发送序
          signal 历史
            旧 SysV 不可靠
            BSD 可靠信号
            POSIX 标准化
            推荐 sigaction
          sigsuspend
            原子等待
            解决 race
            mask 替换恢复
            比 pause 安全
          sigwaitinfo
            同步接收
            无 handler
            siginfo_t 信息
            多线程常用
          signalfd
            信号转 fd
            epoll 整合
            Linux 2.6.22
            替代 pipe 方案
          EINTR 与 SA RESTART
            自动重启部分调用
            poll 不自动重启
            BSD SO RESTART
            手动循环处理

      五、重点与易错点

      1. core dump 命名由 /proc/sys/kernel/core_pattern 控制——含格式说明符;以 | 开头把 core 通过 stdin 传给指定程序(systemd-coredump 即此模式)。

      2. set-user-ID 进程不产生 core(默认)——防止敏感信息泄露;调试时 prctl(PR_SET_DUMPABLE, 1) 或修改 /proc/sys/fs/suid_dumpable

      3. ulimit -c 控制 core 大小——ulimit -c 0 完全禁止 core;现代推荐 ulimit -c unlimited + systemd-coredump 集中管理。

      4. 硬件异常不能正常返回——SIGSEGV/SIGBUS/SIGFPE/SIGILL 返回 handler 会再次触发原指令;正确处理是 _exitsiglongjmp;Linux 2.6+ 阻塞此类信号会立即终止。

      5. Linux 进程睡眠状态 INTERRUPTIBLE(S) vs UNINTERRUPTIBLE(D)——D 状态进程不能被 kill -9 杀死(典型 NFS 挂死);Linux 2.6.25+ 增加 KILLABLE 状态。

      6. 实时信号可排队、可携带数据、投递有序——用 SIGRTMIN..SIGRTMAX(Linux 32-63);sigqueue 发送 + SA_SIGINFO 接收;超过 RLIMIT_SIGPENDING 返回 EAGAIN。

      7. SIGRTMIN/SIGRTMAX 可能是函数(Linux)——不能用在 #if 预处理;运行时检查可用 sysconf(_SC_RTSIG_MAX)

      8. 实时信号 si_value 仅对 sigqueue 发送的信号有意义——kill 发送的实时信号 si_value 无效。

        • sigsuspend 是「正确」的等待模式——sigprocmask + pause 之间有 race;sigsuspend 原子地「设置 mask + pause + 恢复」是正确模式。

      9. sigwaitinfo 比 handler+sigsuspend 略快——少一次上下文切换;适合多线程「一个线程独占信号」的模式。

      10. signalfd 把信号整合进 epoll——需先 sigprocmask 阻塞信号;典型场景:单线程事件循环统一处理 I/O + 信号。

      11. 标准信号投递顺序「实现定义」——Linux 按编号升序;不要依赖此顺序;实时信号才保证低编号优先。

      12. signal() 跨 UNIX 行为差异大——SysV 旧语义(不可靠);BSD/POSIX 可靠;glibc 默认 BSD;编译时 _BSD_SOURCE 未定义会回退到 SysV;始终用 sigaction()

      13. pause() 必返回 -1 + EINTR——但与 sigprocmask 配合有 race;用 sigsuspend 替代。

      14. sigwaitinfo 必须先 sigprocmask 阻塞——否则信号被 handler 截走,sigwaitinfo 永远等不到。

      15. SIGCONT 特殊——投递到 stopped 进程时即使被阻塞也强制唤醒;投递 SIGCONT 清空 pending stop;投递 stop 清空 pending SIGCONT。

      16. 跨章衔接:第 20-21 章信号基础与 handler;第 23 章 timer + 实时信号;第 26 章 SIGCHLD + wait;第 34 章 SIGCONT/SIGTSTP/SIGHUP 与 job control;第 36 章 RLIMIT_SIGPENDING;第 33 章多线程信号。