第 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 三参数 handler:
void handler(int sig, siginfo_t *info, void *ucontext)——info 含发送者 PID/UID、信号来源(硬件/软件/kill/sigqueue)、si_value 等。 -
中断系统调用与 SA_RESTART:handler 打断阻塞系统调用时返回 -1 EINTR;某些系统调用自动重启;其他需要手动循环或使用 SA_RESTART 标志。
|
本章主旨
本章深入信号处理器的设计与工程实践。读者应掌握:handler 设计的两大模式(设 flag / 清理后退出)、为什么必须避免非可重入函数、 |
一、核心概念
本章围绕 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 是安全的,但 |
全局变量与 sig_atomic_t |
handler 与主程序共享的变量必须 |
§21.1.3;只适合单变量 flag;复杂数据结构需要 mutex/lock,但 mutex 非 async-signal-safe——只能在 handler 中 lock、main 中 unlock。 |
siglongjmp/sigsetjmp (Nonlocal goto) |
|
§21.1/§21.2.1;典型用法:handler 中 siglongjmp 跳回主循环入口;用于「在深层调用栈中响应信号后回到预定位置」。 |
SA_SIGINFO 三参数 handler |
|
§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;不是所有阻塞系统调用都自动重启——详见 |
二、详细笔记
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、_Exit、abort、accept、access、aio_error、aio_return、aio_suspend、alarm、bind、cfgetispeed、cfgetospeed、cfsetispeed、cfsetospeed、chdir、chmod、chown、clock_gettime、close、connect、creat、dup、dup2、execle、execve、faccessat、fchdir、fchmod、fchmodat、fchown、fchownat、fcntl、fdatasync、fexecve、fork、fpathconf、fstat、fstatat、fsync、ftruncate、futimens、getegid、geteuid、getgid、getgroups、getpeername、getpgrp、getpid、getppid、getsockname、getsockopt、getuid、kill、link、linkat、listen、lseek、lstat、mkdir、mkdirat、mkfifo、mkfifoat、mknod、mknodat、open、openat、pathconf、pause、pipe、poll、posix_trace_event、pselect、pthread_kill、pthread_self、pthread_sigmask、read、readlink、readlinkat、recv、recvfrom、recvmsg、rename、renameat、rmdir、select、sem_post、send、sendmsg、sendto、setgid、setpgid、setsid、setsockopt、setuid、shutdown、sigaction、sigaddset、sigdelset、sigemptyset、sigfillset、sigismember、sigpending、sigprocmask、sigqueue、sigset、sigsuspend、sleep、sockatmark、socket、socketpair、stat、symlink、symlinkat、tcdrain、tcflow、tcflush、tcgetattr、tcgetpgrp、tcsendbreak、tcsetattr、tcsetpgrp、time、timer_getoverrun、timer_gettime、timer_settime、times、umask、uname、unlink、unlinkat、utime、utimensat、utimes、wait、waitpid、`write`等。
printf、malloc、exit 不在清单内——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_receiver 的 gotSigint 变量即为此模式。
21.2 sigsetjmp/siglongjmp(Nonlocal goto)
What:sigsetjmp(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 替代栈
What:sigaltstack 让 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 函数分类
|
|
sigaction sa_flags 速查
|
四、思维导图
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 不自动重启
五、重点与易错点
-
handler 尽量简单——复杂 handler 易引入 race;两大模式:set flag、cleanup + exit/siglongjmp;handler 是异步执行的「独立逻辑线程」。
-
非可重入函数清单——stdio(printf/scanf)、malloc/free、crypt、getpwnam、gethostbyname、getservbyname 等;handler 调用它们会破坏主程序状态或反之。
-
handler 中输出用 write——
write(STDOUT_FILENO, msg, len);printf/sprintf 在 handler 中不安全——可能产生乱码、崩溃、数据破坏。 -
sig_atomic_t + volatile——共享变量必须
volatile sig_atomic_t;sig_atomic_t 保证原子读写,volatile 防止编译器优化到寄存器;只适合单变量 flag。 -
sigsetjmp + siglongjmp——比 longjmp 安全(保存信号掩码);典型用法:handler 中 siglongjmp 跳回主循环;用
can_jump标志防伪唤醒。 -
SA_SIGINFO 三参数 handler——
sa_sigaction(不是 sa_handler);info 含发送者 PID/UID(si_pid/si_uid)、信号来源(si_code)、伴随数据(si_value);区分 SIGCHLD 子进程退出原因用 si_code。 -
sigaltstack 处理栈溢出场景——SIGSEGV handler 不能在溢出栈上运行;
sigaltstack+SA_ONSTACK提供替代栈;典型用于栈检查、调试。 -
SIGCHLD handler 与僵尸进程——handler 中调
wait回收;不显式回收的子进程会变僵尸;SA_NOCLDWAIT让内核自动回收(不产生僵尸)。 -
EINTR 处理——handler 打断阻塞系统调用时返回 -1 + EINTR;BSD socket 默认不重启;Linux 文件 I/O 部分自动重启;最稳妥是手动循环;用
SA_RESTART标志让 sigaction 自动重启支持的系统调用。 -
不要依赖系统调用自动重启——库代码必须手动处理 EINTR;不同 UNIX 默认行为差异大;用
SA_RESTART也仅支持部分调用(见signal(7))。 -
硬件异常 handler 不能正常返回——SIGSEGV/SIGBUS/SIGFPE/SIGILL 正常返回会再次触发原指令;正确处理是
_exit或siglongjmp;详见第 22 章。 -
siglongjmp 跳到构造函数 / 中间状态——可能绕过析构/清理逻辑(特别是 C++);信号驱动代码应仔细设计状态机。
-
「终止」与「退出」——handler 中
_exit(EXIT_FAILURE)立即终止(不调用 atexit);exit()会执行 atexit handler,可能不安全。 -
跨章衔接:第 20 章信号基础;第 22 章硬件异常处理、core dump、sigsuspend 原子等待;第 26 章 SIGCHLD 与 wait 配合回收僵尸;第 29 章多线程信号;第 34 章 job control 与 SIGHUP/SIGTSTP。