第 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_SIGINFOhandler 接收。 -
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; |
§22.1;触发条件: |
硬件异常信号(同步信号) |
SIGBUS/SIGFPE/SIGILL/SIGSEGV/SIGSYS——硬件异常触发;同步、可预测;不能正常 return(会再次触发);Linux 2.6+ 阻塞时立即终止。 |
§22.4/§22.5;正确处理: |
实时信号 |
SIGRTMIN..SIGRTMAX(Linux 32-63);可排队(多次发送多次投递)、可携带数据(sigval)、投递顺序按编号;用 |
§22.8; |
sigsuspend 原子等待 |
|
§22.9;正确模式:sigprocmask 阻塞 → 临界区 → sigsuspend(原 mask);不要用 pause + 两段 sigprocmask。 |
sigwaitinfo 同步等待 |
|
§22.10;通常配合 sigprocmask 阻塞相关信号——否则信号会被 handler 截走; |
signalfd 文件描述符化信号 |
|
§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=0、RLIMIT_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 已改用此状态。
When:ps 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 原子等待
What:sigsuspend(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 同步等待
What:sigwaitinfo(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 信号文件描述符化
What:signalfd(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 格式说明符
|
|
信号投递模式对比
|
四、思维导图
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
手动循环处理
五、重点与易错点
-
core dump 命名由
/proc/sys/kernel/core_pattern控制——含格式说明符;以|开头把 core 通过 stdin 传给指定程序(systemd-coredump 即此模式)。 -
set-user-ID 进程不产生 core(默认)——防止敏感信息泄露;调试时
prctl(PR_SET_DUMPABLE, 1)或修改/proc/sys/fs/suid_dumpable。 -
ulimit -c控制 core 大小——ulimit -c 0完全禁止 core;现代推荐ulimit -c unlimited+ systemd-coredump 集中管理。 -
硬件异常不能正常返回——SIGSEGV/SIGBUS/SIGFPE/SIGILL 返回 handler 会再次触发原指令;正确处理是
_exit或siglongjmp;Linux 2.6+ 阻塞此类信号会立即终止。 -
Linux 进程睡眠状态 INTERRUPTIBLE(S) vs UNINTERRUPTIBLE(D)——D 状态进程不能被 kill -9 杀死(典型 NFS 挂死);Linux 2.6.25+ 增加 KILLABLE 状态。
-
实时信号可排队、可携带数据、投递有序——用
SIGRTMIN..SIGRTMAX(Linux 32-63);sigqueue发送 + SA_SIGINFO 接收;超过RLIMIT_SIGPENDING返回 EAGAIN。 -
SIGRTMIN/SIGRTMAX可能是函数(Linux)——不能用在#if预处理;运行时检查可用sysconf(_SC_RTSIG_MAX)。 -
实时信号 si_value 仅对 sigqueue 发送的信号有意义——kill 发送的实时信号 si_value 无效。
-
sigsuspend 是「正确」的等待模式——
sigprocmask + pause之间有 race;sigsuspend 原子地「设置 mask + pause + 恢复」是正确模式。
-
-
sigwaitinfo 比 handler+sigsuspend 略快——少一次上下文切换;适合多线程「一个线程独占信号」的模式。
-
signalfd 把信号整合进 epoll——需先 sigprocmask 阻塞信号;典型场景:单线程事件循环统一处理 I/O + 信号。
-
标准信号投递顺序「实现定义」——Linux 按编号升序;不要依赖此顺序;实时信号才保证低编号优先。
-
signal()跨 UNIX 行为差异大——SysV 旧语义(不可靠);BSD/POSIX 可靠;glibc 默认 BSD;编译时_BSD_SOURCE未定义会回退到 SysV;始终用sigaction()。 -
pause()必返回 -1 + EINTR——但与 sigprocmask 配合有 race;用sigsuspend替代。 -
sigwaitinfo 必须先 sigprocmask 阻塞——否则信号被 handler 截走,sigwaitinfo 永远等不到。
-
SIGCONT 特殊——投递到 stopped 进程时即使被阻塞也强制唤醒;投递 SIGCONT 清空 pending stop;投递 stop 清空 pending SIGCONT。
-
跨章衔接:第 20-21 章信号基础与 handler;第 23 章 timer + 实时信号;第 26 章 SIGCHLD + wait;第 34 章 SIGCONT/SIGTSTP/SIGHUP 与 job control;第 36 章 RLIMIT_SIGPENDING;第 33 章多线程信号。