第 20 章 信号:基本概念 (Signals: Fundamental Concepts)
核心结论
-
信号是软件中断:通知进程某事件已发生;Linux 上标准信号编号 1-31(另有实时信号 32-64,第 22 章);
<signal.h>中以SIGxxxx符号名引用。 -
信号三态:产生(generate)→ 等待(pending)→ 投递(deliver);可被「信号掩码」阻塞——阻塞期间保持 pending,解除阻塞后立即投递。
-
默认动作五类:ignore(SIGCHLD/SIGURG 等)、terminate(SIGTERM)、core dump(SIGSEGV/SIGABRT/SIGQUIT)、stop(SIGSTOP/SIGTSTP/SIGTTIN/TTOU)、cont(SIGCONT)。
-
信号处置(disposition):signal() 或 sigaction() 设为 SIG_DFL(默认)、SIG_IGN(忽略)或自定义 handler;
sigaction是 SUSv3 推荐、跨平台可靠的方式。 -
信号发送四象限:
kill(pid, sig)中 pid > 0 单进程;pid == 0 同进程组;pid < -1 进程组 |pid|;pid == -1 同 UID 全部进程;sig=0 为「空信号」用于探测进程是否存在。 -
标准信号不排队:同一信号在阻塞期间产生多次仅投递一次;高频信号可能丢失——这是「为什么不能用信号计数」的根本原因。
|
本章主旨
本章是信号三章中的第一章——建立「什么是信号、信号从哪来、信号怎么响应」的总体框架。读者应掌握:信号的三态生命周期(generate/pending/deliver)、信号掩码与阻塞机制、六种默认动作、四大类信号来源(硬件异常、终端特殊字符、软件事件、其他进程),kill() 的四种 pid 语义与权限规则、信号集(sigset_t)API、sigprocmask/sigpending/sigsuspend/pause 的用法。本章与第 21 章(handler 设计)、第 22 章(高级特性:实时信号、signalfd、core dump 等)形成完整信号体系。 |
一、核心概念
本章围绕 7 个核心概念展开:从信号本质入手,到信号类型、信号处置、信号发送、信号掩码、pending 信号,最后是进程等待信号的标准范式。
| 概念 | 定义 + 重要性 | 实现提示 |
|---|---|---|
信号生命周期 |
信号三态:generate(事件发生)→ pending(等待投递)→ deliver(已投递);阻塞期间保持 pending;解除阻塞后立即投递(甚至在 sigprocmask 返回前)。 |
§20.1;标准信号不排队——多次产生只投递一次;同步信号(如 SIGSEGV)发生在进程执行某指令时,可预测;异步信号(如 SIGTERM)时机不可预测。 |
信号类型与默认动作 |
Linux 标准信号 1-31,每个有独特语义;默认动作分 5 类:ignore / term / core / stop / cont;SIGKILL 与 SIGSTOP 无法被阻塞、忽略或捕获——保证管理员始终能结束失控进程。 |
§20.2/Table 20-1;常用信号:SIGTERM(15,优雅终止)、SIGKILL(9,强制)、SIGHUP(1,挂起/重载配置)、SIGCHLD(17,子进程终止)、SIGSEGV(11,段错误)、SIGPIPE(13,管道破裂)。 |
信号处置(Disposition) |
用 signal() 或 sigaction() 把信号处置设为 SIG_DFL(默认)、SIG_IGN(忽略)、自定义 handler;sigaction 是首选(可移植、灵活、可获详细信息)。 |
§20.3/§20.13;handler 形式: |
信号发送(kill/raise/killpg) |
|
§20.5;权限:root 可发任何信号;非特权 sender 需 UID/GID 匹配 receiver(real 或 saved set-user-ID);SIGCONT 特殊——同 session 任意 UID 都可发。 |
信号集(sigset_t) |
用 sigset_t 数据类型表示多个信号的集合;sigemptyset/sigfillset/sigaddset/sigdelset/sigismember 操作;SUSv3 要求 sigset_t 必须可赋值(不一定实现为位图,但 Linux 是)。 |
§20.9;必须用 sigemptyset 或 sigfillset 初始化——不能假设静态变量初值表示空集。 |
信号掩码与阻塞 |
|
§20.10;典型模式:sigprocmask(SIG_BLOCK, &set, &prev) → 临界区 → sigprocmask(SIG_SETMASK, &prev, NULL);pending 信号在解除阻塞后立即投递。 |
等待信号(pause/sigsuspend) |
|
§20.14; |
二、详细笔记
20.1 信号概念与生命周期
What:信号(signal)是「事件已发生」的通知;类比硬件中断——异步打断进程正常执行。
Why:信号是 UNIX 异步事件通知的核心机制——Ctrl-C、kill、子进程退出、定时器到期、硬件异常等都用信号表达。
How:信号三态:
| 态 | 含义 | 关键事实 |
|---|---|---|
产生 (generate) |
事件发生,内核/进程「记下」要发某信号 |
可由内核(硬件异常、终端、软件事件)或其他进程(kill)触发 |
等待 (pending) |
信号已产生但未投递 |
进程信号掩码(signal mask)决定信号是否被阻塞;被阻塞的信号在 pending 集中等待 |
投递 (deliver) |
信号已送到进程,触发默认动作或 handler |
进程解除阻塞后立即投递;SUSv3 要求至少一个被解除阻塞的 pending 信号在 sigprocmask 返回前投递 |
信号来源(§20.1):
-
硬件异常——非法内存访问、总线错误、除零等;通常同步触发。
-
终端特殊字符——Ctrl-C(SIGINT)、Ctrl-Z(SIGTSTP)、Ctrl-\(SIGQUIT)。
-
软件事件——timer 到期、子进程终止、文件描述符就绪、CPU 时间超限等。
-
进程间发送——
kill/raise/killpg;可用作同步原语或简易 IPC。
When:写信号驱动代码——理解三态才能设计正确的「屏蔽-处理-恢复」流程。
Example:Ctrl-C 时的信号流程:
用户按 Ctrl-C
↓
终端驱动发送 SIGINT 给前台进程组
↓
内核把 SIGINT 标记为 pending(如果未阻塞)
↓
下次调度时内核投递 SIGINT
↓
若进程有 handler → 调用 handler;否则默认终止
20.2 信号类型与默认动作
What:Linux 标准信号 1-31;每个有独特的名字、编号与默认动作;其中两个「sure」信号(SIGKILL/SIGSTOP)行为固定。
Why:信号数量众多,各有语义;理解默认动作是「为什么我的程序被杀了」「为什么 Ctrl-Z 让它停了」的前提。
How:常见信号速查(§20.2 + Table 20-1):
| 信号 | 默认 | 来源与语义 |
|---|---|---|
SIGABRT(6) |
core |
|
SIGALRM(14) |
term |
|
SIGBUS(7) |
core |
总线错误(如 mmap 越界) |
SIGCHLD(17) |
ignore |
子进程终止/停止/恢复 |
SIGCONT(18) |
cont |
恢复被 stop 的进程 |
SIGFPE(8) |
core |
算术异常(除零) |
SIGHUP(1) |
term |
终端挂起;daemon 用它重载配置 |
SIGILL(4) |
core |
非法指令 |
SIGINT(2) |
term |
终端中断字符(Ctrl-C) |
SIGKILL(9) |
term |
sure kill——不能阻塞/忽略/捕获 |
SIGPIPE(13) |
term |
管道/FIFO/socket 写时无读端 |
SIGQUIT(3) |
core |
终端 quit 字符(Ctrl-\),产生 core |
SIGSEGV(11) |
core |
段错误——无效内存引用 |
SIGSTOP(19) |
stop |
sure stop——不能阻塞/忽略/捕获 |
SIGSYS(31) |
core |
非法系统调用号 |
SIGTERM(15) |
term |
标准终止信号;kill/killall 默认发它 |
SIGTSTP(20) |
stop |
终端停止字符(Ctrl-Z) |
SIGUSR1(10)/SIGUSR2(12) |
term |
用户自定义信号 |
SIGWINCH(28) |
ignore |
终端窗口大小变化 |
SIGXCPU(24)/SIGXFSZ(25) |
core |
CPU/文件大小超 RLIMIT |
When:进程间约定通信——SIGUSR1/SIGUSR2 是「应用自定义」的专用通道;daemon 用 SIGHUP 重载配置;父进程用 SIGCHLD 感知子进程退出。
Example:标准 kill 流程——kill <pid> 默认发 SIGTERM;kill -9 <pid> 发 SIGKILL。优雅终止应先用 SIGTERM 让程序清理资源,最后才用 SIGKILL。
20.3 signal() 与 20.13 sigaction()
What:signal() 是旧 API,sigaction() 是 POSIX/SUSv3 推荐 API;后者支持阻塞集、flags、获取旧 disposition 等。
Why:signal() 在不同 UNIX 上语义差异大;sigaction() 行为一致——写可移植代码必须用 sigaction。
How:sigaction 用法(§20.13):
// 摘自《The Linux Programming Interface》第 20 章
#include <signal.h>
struct sigaction {
void (*sa_handler)(int); /* handler 地址 / SIG_IGN / SIG_DFL */
sigset_t sa_mask; /* handler 调用期间额外阻塞的信号 */
int sa_flags; /* 标志位 */
void (*sa_restorer)(void); /* 内部使用,应用不要碰 */
};
int sigaction(int sig, const struct sigaction *act, struct sigaction *oldact);
典型用法:
// 摘自《The Linux Programming Interface》第 20 章
struct sigaction sa;
sa.sa_handler = my_handler;
sigemptyset(&sa.sa_mask); /* handler 期间不额外阻塞 */
sa.sa_flags = 0; /* 默认:阻塞触发信号、自动恢复 */
sigaction(SIGINT, &sa, NULL);
When:
-
一次性 handler——
sa_flags = SA_RESETHAND(捕获后恢复默认)。 -
handler 中允许同信号再次打断——
sa_flags |= SA_NODEFER。 -
自动重启被中断的系统调用——
sa_flags |= SA_RESTART(第 21 章详述)。 -
handler 需要更多上下文(发送者 PID、信号来源)——
sa_flags |= SA_SIGINFO(第 21 章)。
Example:完整 sigaction 安装:
struct sigaction sa = {0};
sa.sa_handler = my_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
if (sigaction(SIGINT, &sa, &old_sa) == -1) errExit("sigaction");
/* old_sa 含旧 disposition,可在适当时候恢复 */
20.4 信号处理器(Handler)基础
What:handler 是「信号投递时被自动调用的用户函数」;形式为 void handler(int sig)。
Why:handler 是进程对信号「主动响应」的方式——比默认动作更灵活。
How:第 20 章 Listing 20-1 ouch.c——最简 handler 示例:
// 摘自《The Linux Programming Interface》第 20 章(Listing 20-1)
#include <signal.h>
static void sigHandler(int sig) {
printf("Ouch!\n"); /* UNSAFE: stdio 非 async-signal-safe */
}
int main(int argc, char *argv[]) {
if (signal(SIGINT, sigHandler) == SIG_ERR) errExit("signal");
for (;;) { printf("%d\n", j++); sleep(3); }
}
handler 设计的两大模式:
-
设置全局 flag——主程序周期性检查;适合简单场景。
-
清理后终止或 nonlocal goto——适合错误处理路径。
When:handler 应尽量简单——避免调用非 async-signal-safe 函数(printf、malloc、exit 等);详细见第 21 章。
Example:intquit.c —— 同一 handler 处理 SIGINT 与 SIGQUIT:
static void sigHandler(int sig) {
if (sig == SIGINT) { count++; printf("Caught SIGINT (%d)\n", count); return; }
/* SIGQUIT */
printf("Caught SIGQUIT - that's all folks!\n");
exit(EXIT_SUCCESS);
}
20.5 发送信号:kill/raise/killpg
What:kill(pid, sig) 是核心发送 API;raise(sig) 是「给自己发信号」的便捷封装;killpg(pgrp, sig) 发到进程组。
Why:进程间最常用的同步/控制手段;kill 命名虽然叫 kill,但实际可发任何信号。
How:kill 的 pid 语义:
| pid 取值 | 含义 | 备注 |
|---|---|---|
> 0 |
发到 PID = pid 的进程 |
需权限 |
== 0 |
发到同进程组所有进程 |
包括调用者 |
< -1 |
发到进程组 ID = abs(pid) 的所有进程 |
适合 shell job control |
== -1 |
发到同 UID 所有进程(除 init 与调用者) |
广播信号;root 可发给所有进程 |
权限规则(§20.5):
-
特权进程(
CAP_KILL)可发任何信号给任何进程。 -
init(PID 1)只能被它安装过 handler 的信号发。
-
非特权 sender 需「sender real/effective UID == receiver real/saved set-user-ID」。
-
SIGCONT 特殊:同 session 任意 UID 都可发。
sig = 0(空信号):不发任何信号,仅检查权限——kill(pid, 0) 返回 0 表示「可发信号」、ESRCH 表示「进程不存在」、EPERM 表示「存在但无权」。
When:检查进程是否存在——kill(pid, 0);shell job control——kill(-pgid, sig);进程间事件通知——kill(pid, SIGUSR1)。
Example:第 20 章 Listing 20-3 t_kill.c —— 演示 kill 与空信号:
kill(getLong(argv[1], 0, "pid"), sig);
if (sig == 0) {
if (s == 0) printf("Process exists and we can send it a signal\n");
else if (errno == EPERM) printf("Process exists, but no permission\n");
else if (errno == ESRCH) printf("Process does not exist\n");
}
20.9 信号集(sigset_t)API
What:sigset_t 表示一组信号;五个操作函数:sigemptyset(清空)、sigfillset(填满)、sigaddset(加)、sigdelset(删)、sigismember(测)。
Why:很多信号相关 API(如 sigprocmask、sigaction)需要表示一组信号——sigset_t 是统一抽象。
How:
sigset_t set;
sigemptyset(&set); /* 必须在使用前显式初始化 */
sigaddset(&set, SIGINT);
sigaddset(&set, SIGTERM);
if (sigismember(&set, SIGINT)) { /* SIGINT 在 set 中 */ }
sigfillset(&set); /* 包含所有信号 */
sigdelset(&set, SIGKILL); /* 移除(其实 SIGKILL 本来也加不进去) */
必须用 sigemptyset/sigfillset 初始化——SUSv3 不保证 sigset_t 是位图;不能用 memset(0) 模拟空集。
GNU 扩展:sigandset、sigorset、sigisemptyset。
When:阻塞前初始化 sigset_t → 用 sigaddset 加要阻塞的信号 → 传给 sigprocmask。
Example:printSigset 函数(Listing 20-4)—— 遍历 NSIG 个信号,sigismember 测试,打印所有属于 set 的信号名。
20.10 信号掩码与 sigprocmask
What:sigprocmask(how, set, oldset) 操纵进程的信号掩码(已阻塞信号集);how 取 SIG_BLOCK(加)/ SIG_UNBLOCK(减)/ SIG_SETMASK(覆盖)。
Why:保证临界区不被信号打断;保存/恢复信号掩码实现「临时屏蔽」。
How:典型「屏蔽-恢复」模式(§20.10 Listing 20-5):
sigset_t blockSet, prevMask;
sigemptyset(&blockSet);
sigaddset(&blockSet, SIGINT); /* 准备阻塞 SIGINT */
/* 阻塞 SIGINT,保存旧掩码 */
if (sigprocmask(SIG_BLOCK, &blockSet, &prevMask) == -1)
errExit("sigprocmask1");
/* 临界区代码 —— 不会被 SIGINT 打断 */
/* SIGINT 若在此期间产生,进入 pending 集 */
if (sigprocmask(SIG_SETMASK, &prevMask, NULL) == -1)
errExit("sigprocmask2");
/* 解除阻塞 → pending 的 SIGINT 立即被投递(可能在 sigprocmask 返回前) */
关键事实:
-
SIGKILL 与 SIGSTOP 不能被阻塞——
sigprocmask静默忽略此请求,不报错。 -
SUSv3 要求「解除阻塞的 pending 信号在 sigprocmask 返回前至少投递一个」。
-
sigfillset(&set); sigprocmask(SIG_BLOCK, &set, NULL)阻塞「除 SIGKILL/SIGSTOP 外全部信号」——原子性把握状态的好方式。
When:更新全局数据结构的临界区;事务性操作(创建临时文件 + 写内容 + 改名)应屏蔽信号防止半完成。
Example:第 20 章 Listing 20-6/20-7 sig_sender + sig_receiver——演示「阻塞期间发送 100 万次同一信号,解阻后只投递一次」。
20.11/20.12 pending 信号与「信号不排队」
What:sigpending(&set) 返回当前进程的 pending 信号集;标准信号不排队——多次产生只投递一次。
Why:理解这一点才能解释「为什么 SIGCHLD 多次产生但 wait 只回收一次」「为什么 SIGUSR1 不能用于计数」。
How:
sigset_t pending;
sigpending(&pending);
/* 检查 pending 中是否有某信号 */
if (sigismember(&pending, SIGINT)) { /* SIGINT pending */ }
利用「信号不排队」的特性:
-
改变 pending 信号的 disposition 为 SIG_IGN(或默认 ignore)——该信号从 pending 集移除,不再投递。
-
适合「在信号产生后、handler 调用前取消信号处理」的场景。
When:调试「信号丢失」——可能是发送频率太高、pending 集已满、或 handler 期间重复触发(后者正常,只投递一次)。
Example:第 20 章 Listing 20-7 sig_receiver.c —— 阻塞期间收 100 万 SIGUSR1,解阻后 handler 仅调用 1 次。
20.14 pause() 等待信号
What:pause() 阻塞进程直到任意信号被投递;总是返回 -1,errno = EINTR(如果是被 handler 捕获的信号)。
Why:最简单的「阻塞到信号到达」调用——但与 sigprocmask 配合时有 race(详见第 22 章 sigsuspend)。
How:
/* 经典模式 */
sigset_t mask, oldmask;
sigemptyset(&mask);
sigprocmask(SIG_BLOCK, &mask, &oldmask); /* 此处无意义,只是示意 */
for (;;) {
pause();
/* 此处只会被信号唤醒 */
}
When:简单的「等待一个事件」循环;正确的并发等待应用 sigsuspend(第 22 章)。
Example:第 20 章 Listing 20-2 intquit.c:
for (;;) pause(); /* 阻塞,直到 SIGINT 或 SIGQUIT */
三、关键图表
(本章无独立编号图表)
|
常见信号速查
|
|
信号处置与生命周期
|
四、思维导图
mindmap
root((第 20 章 信号 基本概念))
信号生命周期
generate 产生
pending 等待
deliver 投递
阻塞与解除
不排队
信号类型
1 31 标准信号
32 64 实时信号
SIGKILL SIGSTOP 不可阻挡
默认动作 5 类
ignore term core stop cont
信号处置
signal 旧 API
sigaction 推荐 API
SIG DFL SIG IGN handler
sa mask sa flags
信号发送
kill pid sig
pid 4 象限
sig 0 空信号
raise killpg
权限检查
信号集
sigset t 类型
sigemptyset sigfillset
sigaddset sigdelset
sigismember
5 个操作函数
信号掩码
sigprocmask
SIG BLOCK UNBLOCK SETMASK
阻塞 SIGKILL 静默忽略
解阻立即投递
pending 信号
sigpending 查询
改变 disposition 丢弃
多次产生只投递一次
等待信号
pause 阻塞 EINTR
sigsuspend 原子组合
race 与正确模式
五、重点与易错点
-
「信号是软件中断」——类比硬件中断,可异步打断主程序执行;handler 返回后程序从被打断处继续(除非 handler 用 longjmp)。
-
信号三态:generate → pending → deliver——阻塞期间保持 pending;解阻后立即投递;SUSv3 要求至少一个 pending 信号在 sigprocmask 返回前投递。
-
标准信号不排队——同一信号在阻塞期间产生多次只投递一次;高频信号可能丢失;不能用信号计数;这是实时信号(第 22 章)出现的原因之一。
-
SIGKILL 与 SIGSTOP 不可阻挡——不能被捕获、阻塞、忽略;保证管理员始终能 kill/stop 失控进程;
signal(SIGKILL, …)返回错误。 -
默认动作 5 类:ignore(SIGCHLD/SIGURG)、term(SIGTERM)、core(SIGSEGV/SIGQUIT)、stop(SIGSTOP/SIGTSTP)、cont(SIGCONT);core 类信号便于调试——
ulimit -c unlimited后用 gdb 看 core 文件。 -
kill 的 pid 4 象限——
>0单进程;==0同进程组;←1进程组 |pid|;==-1同 UID 全部(除 init 与自身);shell 用kill -pgid实现 job control。 -
空信号(sig=0)用于进程探测——
kill(pid, 0)不发信号,只检查权限;返回 0 = 可发;ESRCH = 不存在;EPERM = 存在但无权。 -
权限规则——root 可发任何信号;非特权 sender 需 UID 匹配(real/effective == receiver real/saved-set-user-ID);SIGCONT 特殊:同 session 任意 UID 可发。
-
sigaction 是首选 API——
signal()跨 UNIX 行为差异大;sigaction是 SUSv3 标准、可移植、支持 flags(SA_RESTART/SA_NODEFER/SA_RESETHAND/SA_SIGINFO)与 sa_mask。 -
sa_mask 含义——handler 调用期间额外阻塞的信号集合;handler 触发时信号本身也会被自动阻塞(除非 SA_NODEFER);handler 返回后自动解除。
-
sigset_t 必须显式初始化——SUSv3 不保证它是位图;不能用
memset(0)模拟空集;用sigemptyset/sigfillset。 -
pause 与 sigprocmask 配合有 race——两调用之间可能错过信号;正确模式是
sigsuspend(第 22 章)。 -
终端生成信号的 disposition 约定——若 exec 时发现 SIGHUP/SIGINT/SIGQUIT/SIGTTIN/SIGTTOU/SIGTSTP 被设为 SIG_IGN,新程序应保留 SIG_IGN;不要改回默认——否则后台作业会被意外中断(详见第 34 章)。
-
「信号来自硬件还是软件」决定是否能安全处理——硬件异常 SIGSEGV/SIGBUS/SIGFPE/SIGILL 返回时通常会再次触发;正确处理是不正常返回(exit/siglongjmp),详见第 22 章。
-
跨章衔接:第 21 章深入 handler 设计(reentrancy、async-signal-safe、longjmp、SA_SIGINFO);第 22 章深入高级特性(实时信号、sigsuspend、signalfd、core dump);第 26 章 SIGCHLD 与 wait;第 34 章 SIGHUP/SIGTSTP 与 job control;第 9 章 UID/GID 与信号权限。