信号的概念和机制
从"进程之间怎么打招呼"讲到"SIGCHLD 回收子进程",一条线串完 Linux 信号机制。
0. 信号的概念和机制
一句话理解:信号是内核或进程发给另一个进程的异步通知,收到后进程有三种选择:执行默认动作、忽略、或执行你注册的处理函数。
完整案例:按 Ctrl+C 验证 SIGINT 的默认动作。
#include <stdio.h>
#include <unistd.h>
int main(void) {
printf("PID = %d,按 Ctrl+C 测试默认动作\n", getpid());
while (1) pause(); // pause() 挂起,直到收到一个信号
return 0;
}运行后按 Ctrl+C,进程终止。pause() 本身返回 -1 并设置 errno = EINTR,但这里进程已经被 SIGINT 的默认动作终止了,走不到后面。
注意:信号不排队。常规信号在未决期间产生多次,最终只记一次。不能拿信号来"数数"。
1. 与信号相关的概念
四个关键状态要分清:
产生:信号被发出。来源包括终端按键(Ctrl+C)、硬件异常、kill 等系统调用、内核事件(如子进程退出)。
未决(pending):信号产生了,但还没有被递达给进程。内核在进程的未决信号集里把对应位置 1。
递达(delivery):信号被实际处理了,未决位清 0。
阻塞 / 屏蔽(block):进程告诉内核"这类信号先别递达"。被阻塞的信号产生后停在未决状态,不会丢失,解除阻塞后会补递达。
忽略(ignore):和阻塞不同。忽略是信号已经递达了,但你选择不做反应(SIG_IGN);阻塞是递达这一步被推迟了。
小图:
[产生] ──► [未决集置位] ──► [被阻塞?]
│
┌──────────────┴──────────────┐
│ 是 │ 否
▼ ▼
[停在未决集等待] ──解除阻塞──► [递达]
│
▼
[默认动作 / 忽略 / 处理函数]2. 信号屏蔽字和未决信号集
进程的 task_struct 里有两张位图,各 64 位(对应 1~64 号信号):
阻塞信号集(mask / 屏蔽字):
位1 位2 位3 ... 位64
0 1 0 0 <- 第 2 位为 1,表示屏蔽 2 号信号(SIGINT)
未决信号集(pending):
位1 位2 位3 ... 位64
0 1 0 0 <- 2 号信号产生了,但被阻塞,卡在这里完整案例:屏蔽 SIGINT,按 Ctrl+C 验证进程不退,再查未决集。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int main(void) {
sigset_t block_set, old_set, pending;
sigemptyset(&block_set);
sigaddset(&block_set, SIGINT);
// 屏蔽 SIGINT,保存原来的屏蔽字
if (sigprocmask(SIG_BLOCK, &block_set, &old_set) == -1) {
perror("sigprocmask block");
return 1;
}
printf("SIGINT 已屏蔽。现在按 Ctrl+C,进程不会退出。\n");
sleep(5);
// 查看当前未决信号集
if (sigpending(&pending) == -1) {
perror("sigpending");
return 1;
}
if (sigismember(&pending, SIGINT))
printf("SIGINT 正在未决集中等待。\n");
else
printf("未决集中没有 SIGINT。\n");
// 恢复原来的屏蔽字,此时 SIGINT 会立刻递达,进程终止
printf("即将恢复屏蔽字,SIGINT 会马上递达。\n");
sigprocmask(SIG_SETMASK, &old_set, NULL);
// 如果 SIGINT 默认动作是终止,下面这行不会执行
printf("这行通常看不到。\n");
return 0;
}重点:SIGKILL(9)和 SIGSTOP(19)不能被阻塞、不能被捕捉、不能被忽略。这是内核防止进程把自己锁死的硬规则。
3. 信号四要素和常规信号一览
一个信号由四样东西标识:编号、名称、默认动作、触发事件。
常用信号速查:
| 编号 | 名称 | 默认动作 | 典型触发 |
|---|---|---|---|
| 2 | SIGINT | 终止 | Ctrl+C |
| 3 | SIGQUIT | 终止 + core | Ctrl + \ |
| 9 | SIGKILL | 终止 | kill -9,不可捕捉 |
| 14 | SIGALRM | 终止 | alarm / setitimer(ITIMER_REAL) |
| 15 | SIGTERM | 终止 | kill 默认信号 |
| 17 | SIGCHLD | 忽略 | 子进程退出或停止 |
| 19 | SIGSTOP | 暂停 | 不可捕捉 |
1~31 是常规信号,不排队;32~64 是实时信号,支持排队。
4. kill 函数和 kill 命令
函数原型:
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);参数:
pid:目标,四种含义见下表。sig:信号编号,用SIGxxx宏;0 表示不发信号,只做检查。- 返回 0 成功,-1 失败并设置 errno:
EINVAL信号编号非法,EPERM无权限,ESRCH进程不存在。
pid 的四种含义(man page 原文归纳):
| pid | 发给谁 |
|---|---|
| > 0 | 进程 ID 为 pid 的单个进程 |
| = 0 | 调用者所在进程组中的所有进程 |
| = -1 | 调用者有权限发送的所有进程,但排除 init(PID 1) |
| < -1 | 进程组 ID 为 -pid 中的所有进程 |
sig = 0 的特殊用法:不发任何信号,只做错误检查(进程是否存在、有无权限)。这是"探活"的标准做法:返回 0 表示进程存在且有权限;返回 -1 且 errno == ESRCH 表示进程不存在,EPERM 表示存在但无权限。
完整案例:探活 + 发信号。
#include <stdio.h>
#include <signal.h>
#include <errno.h>
#include <string.h>
int check_alive(pid_t pid) {
if (kill(pid, 0) == 0) return 1; // 存在且有权限
if (errno == EPERM) return 2; // 存在但无权限
return 0; // 不存在或其他错误
}
int main(void) {
pid_t target = 1; // init
int st = check_alive(target);
printf("pid %d 探活结果: %d\n", target, st);
// 给自己发 SIGTERM(默认动作终止),正常情况下进程就此结束
kill(getpid(), SIGTERM);
printf("这行不会执行\n");
return 0;
}易错:kill 名字有误导性,它可以发任何信号,不只是"杀"。普通用户只能给自己名下的进程发信号(或有 CAP_KILL 能力)。
5. alarm 函数
原型:
#include <unistd.h>
unsigned int alarm(unsigned int seconds);行为:seconds 秒后,内核给当前进程发 SIGALRM(14 号)。默认动作终止进程。一个进程同一时刻只有一个 alarm 闹钟,第二次调用会覆盖第一次,不是叠加。返回上一个闹钟的剩余秒数;之前没设过则返回 0。alarm(0) 取消闹钟,返回旧闹钟剩余时间。
完整案例:验证 alarm 返回旧闹钟剩余时间,以及覆盖行为。
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void alrm_handler(int sig) {
(void)sig;
write(STDOUT_FILENO, "SIGALRM 到了\n", sizeof("SIGALRM 到了\n") - 1); // write 是异步信号安全的
}
int main(void) {
signal(SIGALRM, alrm_handler);
unsigned int r1 = alarm(10);
printf("第一次 alarm(10),返回: %u\n", r1); // 之前没有闹钟,应为 0
sleep(2);
unsigned int r2 = alarm(3); // 覆盖:旧的 10 秒闹钟还剩约 8 秒
printf("第二次 alarm(3),返回旧闹钟剩余: %u\n", r2); // 约 8
pause(); // 等待新的 3 秒闹钟触发
return 0;
}重点:alarm 精度只到秒,且是单次触发。需要周期触发或微秒精度,用 setitimer。
6. setitimer 函数
原型:
#include <sys/time.h>
int setitimer(int which, const struct itimerval *new_value,
struct itimerval *old_value);参数:
which:用哪个计时器,三种取值见下。new_value:新的定时设置,不能传 NULL(Linux 上返回EFAULT)。old_value:输出参数,带回原来的定时设置,可以传 NULL。- 返回 0 成功,-1 失败并设置 errno。
timeval 的两个字段:tv_sec 秒,tv_usec 微秒(0~999999)。timeval 表示一个时间长度(或时间点),由"秒 + 微秒"两部分拼成:timeval 总时间 = tv_sec 秒 + tv_usec 微秒
结构体:
struct itimerval {
struct timeval it_interval; // 周期:每隔多久触发一次
struct timeval it_value; // 首次:多久后第一次触发
};which 三种类型:
ITIMER_REAL:真实时间,发 SIGALRMITIMER_VIRTUAL:用户态 CPU 时间,发 SIGVTALRMITIMER_PROF:用户态 + 内核态 CPU 时间,发 SIGPROF
关键规则:it_value 为 0 → 定时器停止;it_value 非 0 但 it_interval 为 0 → 只触发一次;两者都非 0 → 周期触发。
完整案例:每 1 秒触发一次 SIGALRM,触发 5 次后自己停掉。
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <sys/time.h>
#include <unistd.h>
static volatile sig_atomic_t count = 0;
void tick(int sig) {
(void)sig;
count++;
}
int main(void) {
// 第一步:设置拦截
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = tick;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sigaction(SIGALRM, &sa, NULL);
// 第二部分:设置时间
struct itimerval tm;
tm.it_value.tv_sec = 1; // 1 秒后第一次
tm.it_value.tv_usec = 0;
tm.it_interval.tv_sec = 1; // 之后每 1 秒一次
tm.it_interval.tv_usec = 0;
// 为当前进程设置一个“间隔定时器”
if (setitimer(ITIMER_REAL, &tm, NULL) == -1) {
perror("setitimer");
return 1;
}
while (count < 5) pause();
// 关掉定时器:it_value 置 0
struct itimerval off;
memset(&off, 0, sizeof(off));
setitimer(ITIMER_REAL, &off, NULL);
printf("共收到 %d 次 SIGALRM\n", count);
return 0;
}易错:alarm 本质上就是低精度、非周期的 ITIMER_REAL 定时器,同一个进程里 alarm 和 setitimer(ITIMER_REAL) 不能"各管各的"同时生效,它们操作的是同一个底层定时器。
7. 午后回顾
这一节原课是中途串讲。回头看,需要牢牢钉住的三个点:
- 产生 ≠ 递达。中间隔着"未决集 + 阻塞集"这道闸门。
- kill 的 pid 有正、零、-1、小于 -1 四种含义,
sig = 0用来探活。 - alarm 单次、秒级、覆盖式;setitimer 可周期、微秒级、三种计时方式。
8. 信号集操作函数
sigset_t 是一个不透明类型(可能是数组或结构体),不能直接赋值或比较,必须通过函数操作。
原型(以 man page 为准):
#include <signal.h>
int sigemptyset(sigset_t *set); // 清空,所有位 0
int sigfillset(sigset_t *set); // 填满,所有位 1
int sigaddset(sigset_t *set, int signum); // 加入
int sigdelset(sigset_t *set, int signum); // 删除
int sigismember(const sigset_t *set, int signum); // 判断;是返回 1,否返回 0,出错 -1参数:set 是待操作的信号集指针;signum 是信号编号,必须是 1~64 之间的合法信号,否则返回 -1 并置 errno = EINVAL(sigismember 遇到非法编号也返回 -1)。
返回值:前四个成功返回 0,失败返回 -1 并设置 errno;sigismember 返回 1/0/-1。
完整案例:构造一个包含 SIGINT 和 SIGQUIT 的集合,验证增删查。
#include <stdio.h>
#include <signal.h>
int main(void) {
sigset_t set;
sigemptyset(&set);
printf("清空后,SIGINT 在集合中吗? %d\n", sigismember(&set, SIGINT)); // 0
sigaddset(&set, SIGINT);
sigaddset(&set, SIGQUIT);
printf("加入后,SIGINT 在集合中吗? %d\n", sigismember(&set, SIGINT)); // 1
printf("加入后,SIGQUIT 在集合中吗? %d\n", sigismember(&set, SIGQUIT));// 1
sigdelset(&set, SIGINT);
printf("删除后,SIGINT 在集合中吗? %d\n", sigismember(&set, SIGINT)); // 0
return 0;
}重点:sigset_t 变量在传给 sigaddset / sigdelset / sigismember 之前,必须先用 sigemptyset 或 sigfillset 初始化。否则内容是随机的,结果未定义。
9. 信号操作函数使用原理分析
核心函数:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);参数:
how:修改方式,三种取值见下。set:新的屏蔽字;传 NULL 表示不改,只查询当前值。oldset:输出修改前的屏蔽字,可以传 NULL。- 返回 0 成功,-1 失败并设置 errno。
注意它改的是"调用线程"的屏蔽字。多线程程序里 POSIX 要求用 pthread_sigmask,两者在 Linux 上的行为是一致的。
how 三种取值(这是最容易混的地方):
| how | 效果 |
|---|---|
SIG_BLOCK | 把 set 里的信号加入当前屏蔽字;原来屏蔽的仍屏蔽,只加不减。 |
SIG_UNBLOCK | 从当前屏蔽字中移除 set 里的信号,只减不增 |
SIG_SETMASK | 当前屏蔽字直接替换为 set,不在 set 里的全部解除屏蔽 |
关键区别:SIG_BLOCK 和 SIG_UNBLOCK 不碰 set 之外的信号;SIG_SETMASK 会整体覆盖,set 之外的一律解除屏蔽。
原理:sigprocmask 改的是进程 PCB 里的阻塞信号集。内核在每次从内核态返回用户态之前,检查"未决集 ∩ 非阻塞集",有交集就先递达信号,再回到用户代码。
oldset 的典型用途:保存修改前的屏蔽字,干活结束后恢复原样,避免污染进程的信号环境。
10. 信号集操作函数练习
练习目标:屏蔽 SIGINT,按 Ctrl+C 后查未决集,再解除屏蔽观察立即递达。
这个练习在第 2 节已经给过完整代码,这里补充一个更接近真实调试习惯的版本:在阻塞期间循环打印未决集,直观看到"信号卡在 pending 里"。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
static void show_pending(const char *tag) {
sigset_t p;
// sigpending 函数用于检索当前进程的未决信号集,即那些已经产生但尚未被处理的信号。
sigpending(&p);
printf("[%s] 未决集中的常规信号: ", tag);
int found = 0;
for (int i = 1; i <= 31; i++) {
if (sigismember(&p, i)) {
printf("%d ", i);
found = 1;
}
}
if (!found) printf("(空)");
printf("\n");
}
int main(void) {
sigset_t block, old;
sigemptyset(&block);
sigaddset(&block, SIGINT);
sigprocmask(SIG_BLOCK, &block, &old);
printf("SIGINT 已阻塞。5 秒内按 Ctrl+C。\n");
for (int i = 0; i < 5; i++) {
show_pending("阻塞中");
sleep(1);
}
printf("解除阻塞...\n");
sigprocmask(SIG_SETMASK, &old, NULL);
// SIGINT 在这里递达。如果没注册处理函数,默认终止,下面看不到
printf("如果 SIGINT 默认终止,这行不会执行。\n");
return 0;
}运行后在 5 秒内按一次 Ctrl+C,会看到未决集里出现 2,解除阻塞后进程终止(或执行你注册的处理函数)。
11. signal 实现信号捕捉
原型:
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);参数:signum 是信号编号,不能是 SIGKILL / SIGSTOP;handler 有三种特殊值——SIG_IGN(忽略)、SIG_DFL(恢复默认)、函数地址(自定义处理)。成功返回旧的处理方式,失败返回 SIG_ERR 并设置 errno。
完整案例:捕获 SIGINT,Ctrl+C 不再终止,打印信息后继续运行。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void on_int(int sig) {
(void)sig;
write(STDOUT_FILENO, "捕获到 SIGINT,进程继续运行\n", sizeof("捕获到 SIGINT,进程继续运行\n") - 1);
}
int main(void) {
if (signal(SIGINT, on_int) == SIG_ERR) {
perror("signal");
return 1;
}
printf("PID = %d,按 Ctrl+C 测试;Ctrl+\\ 退出\n", getpid());
while (1) pause();
return 0;
}为什么实际项目优先用 sigaction:signal 在不同 Unix 和历史版本里行为不一致(比如处理函数执行后是否自动复位),而 sigaction 行为确定、功能完整(能控制 sa_mask、SA_RESTART 等)。下面的 sigaction 案例才是推荐写法。
12. sigaction 实现信号捕捉
原型:
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);参数:
signum:信号编号,不能是SIGKILL/SIGSTOP(返回EINVAL)。act:新的处理方式,传 NULL 表示只查询。oldact:输出原来的处理方式,可以传 NULL。- 返回 0 成功,-1 失败并设置 errno。
结构体关键成员(字段顺序在不同架构上可能不一样,写代码时只用成员名就行):
struct sigaction {
union {
void (*sa_handler)(int); // 简单处理函数
void (*sa_sigaction)(int, siginfo_t *, void *); // 带信息的处理函数,配合 SA_SIGINFO
};
sigset_t sa_mask; // 处理函数执行期间额外屏蔽的信号
int sa_flags; // 行为标志,见下表
void (*sa_restorer)(void); // glibc 恢复现场用的 trampoline,应用层不要设置
};sa_handler 和 sa_sigaction 是同一个 union 的两个名字,只能用一个。设置 sa_sigaction 却没加 SA_SIGINFO,内核会按单参数函数去调,等于把 siginfo_t* 当 int 传进去,行为未定义,通常直接崩。
sa_flags 逐项说明(结构体必须先零初始化,struct sigaction sa{};,否则 sa_flags 是栈上的随机位):
| 标志 | 作用 | 备注 |
|---|---|---|
SA_RESTART | 被打断的慢速系统调用自动重启 | 最常用;对 sleep / nanosleep 无效 |
SA_SIGINFO | 改用 sa_sigaction 三参数版本 | 要拿 siginfo_t 时必选 |
SA_NOCLDSTOP | 子进程暂停/继续时不产生 SIGCHLD | 只对 SIGCHLD 有意义 |
SA_NOCLDWAIT | 子进程退出不产生僵尸,内核自动回收 | 只对 SIGCHLD;之后 wait 会返回 ECHILD |
SA_NODEFER | 处理函数执行期间不自动屏蔽当前信号 | 默认是屏蔽的,加上才能同种信号嵌套,危险 |
SA_RESETHAND | 进入处理函数前把该信号复位成 SIG_DFL | 只能生效一次的处理函数 |
SA_ONSTACK | 在 sigaltstack 注册的备用栈上执行处理函数 | 栈溢出导致的 SIGSEGV 必须用它 |
SA_INTERRUPT | 历史标志,Linux 上无实际效果 | 不用管 |
SA_RESTORER | 供 glibc 从处理函数返回时恢复现场 | 应用层绝对不能设 |
SA_NOMASK / SA_ONESHOT | SA_NODEFER / SA_RESETHAND 的旧名字 | 需要定义 _GNU_SOURCE |
几种实际会用到的组合:
SA_RESTART | SA_NOCLDSTOP:回收子进程的标准组合。SA_SIGINFO | SA_RESTART:既要发送者信息,又不想到处写 EINTR 判断。SA_RESETHAND:只想让第一次信号走自定义逻辑,之后交回默认动作。SA_ONSTACK:单独写没意义,必须先用sigaltstack注册过备用栈。
sa_mask 的核心作用:处理函数执行期间,OS 会把 sa_mask 里的信号临时加入进程的阻塞集,处理函数返回后自动恢复原来的阻塞集。默认情况下,正在处理的信号本身会自动屏蔽,sa_mask 用来额外堵住其他信号,防止嵌套打断。
完整案例:捕获 SIGINT,处理期间额外屏蔽 SIGQUIT。
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
void on_int(int sig) {
(void)sig;
write(STDOUT_FILENO, "[SIGINT 处理开始,持续 3 秒]\n", sizeof("[SIGINT 处理开始,持续 3 秒]\n") - 1);
sleep(3); // 故意耗时,测试 sa_mask 效果
write(STDOUT_FILENO, "[SIGINT 处理结束]\n", sizeof("[SIGINT 处理结束]\n") - 1);
}
int main(void) {
struct sigaction act, old;
memset(&act, 0, sizeof(act));
act.sa_handler = on_int;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, SIGQUIT); // 处理 SIGINT 期间也屏蔽 SIGQUIT
act.sa_flags = 0;
if (sigaction(SIGINT, &act, &old) == -1) {
perror("sigaction");
return 1;
}
printf("PID = %d\n", getpid());
printf("按 Ctrl+C 进入处理;处理期间按 Ctrl+\\ 会被压住,处理完才递达。\n");
while (1) pause();
return 0;
}测试:按 Ctrl+C 后 3 秒内按 Ctrl + \,Ctrl + \ 不会打断处理函数;3 秒后 SIGQUIT 递达,执行默认动作(终止 + core)。
注意:sa_mask 的屏蔽是临时的,处理函数返回后进程原有的屏蔽字恢复原样。
13. 信号捕捉的特性
特性一:同种信号自动屏蔽。 处理 SIGINT 期间又来 SIGINT,不会嵌套调用处理函数,而是挂起,等当前处理函数返回后再处理。
特性二:常规信号不排队。 处理期间来了 5 个同种常规信号,最终只记 1 个,处理函数返回后只再执行一次。
特性三:处理函数是并发流。 它和主程序共享全局变量、堆、文件描述符。最安全的做法是:处理函数里只设置一个 volatile sig_atomic_t 标志,主循环检查标志后再做实际工作。
特性四(易错):处理函数里不能随便调用非异步信号安全的函数。 printf、malloc、exit 都不安全;write、_exit、signal、sigprocmask 等是异步信号安全的。调试时用 write 代替 printf,虽然不方便,但不会把程序搞崩。
完整案例:用 volatile sig_atomic_t 标志配合主循环,而不是在处理函数里直接干活。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
static volatile sig_atomic_t got_int = 0;
void on_int(int sig) {
(void)sig;
got_int = 1; // 只做一个赋值,安全
}
int main(void) {
struct sigaction sa;
sa.sa_handler = on_int;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sigaction(SIGINT, &sa, NULL);
while (1) {
if (got_int) {
got_int = 0;
printf("主循环检测到 SIGINT 标志,在这里做实际工作\n");
}
pause(); // 等待信号
}
return 0;
}14. 内核实现信号捕捉简析
简化流程:
用户进程正在用户态执行
│
▼
信号产生,内核在"返回用户态之前"检查
未决集 ∩ 非阻塞集
│
▼
发现待处理信号,且用户注册了处理函数
│
▼
内核在用户栈上"搭临时框架",
把返回地址指向处理函数
│
▼
从内核态回到用户态,
开始执行你的 handler(仍在用户态)
│
▼
handler 返回时执行 sigreturn 系统调用
│
▼
内核恢复被打断的上下文
(寄存器、栈、原屏蔽字)
│
▼
继续执行原程序关键点:处理函数在用户态执行,不是内核态。内核只负责"安排跳转"和"恢复现场"。所以处理函数里可以调用普通函数,但必须遵守异步信号安全规则。
15. 借助信号捕捉回收子进程
问题:子进程退出后,父进程不 wait,子进程变成僵尸(zombie),占着进程表项不释放。
核心做法:捕获 SIGCHLD,在处理函数里循环 waitpid(-1, ..., WNOHANG)。
为什么必须用 while:SIGCHLD 是常规信号,不排队。多个子进程几乎同时退出,父进程可能只收到一个 SIGCHLD。如果只 wait 一次,剩下的僵尸就漏掉了。while + WNOHANG 保证把所有已经退出的子进程一次回收干净,没有可回收的立即返回 0,循环结束。
sigsuspend的作用
sigsuspend 解决的是 sigprocmask + pause 之间的竞态窗口问题:解屏蔽和挂起是两步,信号若在这个间隙到达就会被消费掉,导致 pause 永久睡死;sigsuspend 把“换屏蔽字 + 挂起等待”合成一个原子操作,保证信号不会在这个窗口里丢失,唤醒后自动恢复原屏蔽字。
第 1 步:把进程的屏蔽字临时替换成 empty(= 解除所有屏蔽)
→ 如果此刻已有 pending 的 SIGCHLD,它立刻被投递 → handler 跑起来
→ handler 跑完返回
第 2 步:进程挂起睡觉(sleep)
→ 如果第 1 步没有 pending 信号,就睡在这里等
→ 等到某个信号到来 → 投递 → handler 跑
→ handler 返回后,sigsuspend 从睡中醒来
第 3 步:把屏蔽字恢复成调用前的样子(= SIGCHLD 又被屏蔽了)
→ sigsuspend 返回 -1,errno = EINTR完整案例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include <sys/wait.h>
#include <unistd.h>
static volatile sig_atomic_t reaped = 0;
void on_chld(int sig) {
(void)sig;
int saved_errno = errno; // 处理函数可能改变 errno,先保存
pid_t pid;
int status;
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
reaped++;
// 处理函数里避免 printf/malloc,这里为了演示简单用 write
// 理论上这里用snprintf也不是异步信号安全函数。
// 因为这里是信号接收到处理的部分。建议把下面三行移动到main内输出。
char buf[64];
int n = snprintf(buf, sizeof(buf), "回收子进程 pid=%d\n", (int)pid);
write(STDOUT_FILENO, buf, n);
}
errno = saved_errno; // 恢复 errno
}
int main(void) {
// 先屏蔽 SIGCHLD,注册处理函数,再 fork
// 避免 fork 之后、sigaction 之前子进程就退出导致竞态
sigset_t mask, old;
sigemptyset(&mask);
sigaddset(&mask, SIGCHLD);
sigprocmask(SIG_BLOCK, &mask, &old);
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = on_chld;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP; // SA_NOCLDSTOP:只关心退出,不关心暂停
sigaction(SIGCHLD, &sa, NULL);
// 创建 5 个子进程,让它们在不同时间退出
for (int i = 0; i < 5; i++) {
pid_t pid = fork();
if (pid == 0) {
// 子进程
sleep(1 + i); // 错开退出时间
_exit(0);
}
}
// 注册完毕,解除对 SIGCHLD 的阻塞
sigset_t empty;
sigemptyset(&empty);
while (reaped < 5) {
sigsuspend(&empty); // 原子解屏蔽 + 睡到被信号唤醒
}
// 等足够久,让所有子进程退出并被回收
sleep(8);
printf("总共回收 %d 个子进程\n", (int)reaped);
return 0;
}为什么先屏蔽再 sigaction 再 fork:如果先 fork,子进程可能在父进程还没 sigaction 时就已经退出,SIGCHLD 先到但处理函数还没注册,这次通知就"错过"了(不会排队)。先屏蔽 SIGCHLD,注册好处理函数,再 fork,最后解除阻塞,就能保证不丢通知。这是一个典型的信号竞态防范手法。
另一个易错:处理函数里 waitpid 循环结束后,记得恢复 errno,因为 waitpid 成功时也可能改动 errno,而处理函数返回后主程序可能依赖原来的 errno。
16. 慢速系统调用中断
什么是慢速系统调用:可能无限期阻塞的调用,典型如读终端/管道/套接字的 read、write、accept、wait、pause。本地磁盘 I/O 不算慢速,通常不会被信号中断。
现象:进程阻塞在 read 上,来了信号,处理函数执行完后,read 会怎样?
两种结果,取决于 sa_flags 是否包含 SA_RESTART:
| 条件 | read 的行为 |
|---|---|
| 有 SA_RESTART | 自动重启,继续阻塞等待,对用户代码"透明" |
| 没有 SA_RESTART | 返回 -1,errno = EINTR,用户代码需要自己判断 |
但 sleep 是例外:无论有没有 SA_RESTART,sleep 都不会被自动重启。它被信号中断后,返回剩余未睡眠的秒数(成功返回,不是 -1)。
完整案例:对比有/无 SA_RESTART 时 read 的行为。
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include <unistd.h>
void on_int(int sig) {
(void)sig;
write(STDOUT_FILENO, "\n[SIGINT handler 执行]\n", sizeof("\n[SIGINT handler 执行]\n") - 1);
}
int main(int argc, char *argv[]) {
int use_restart = (argc > 1); // 带参数运行 = 使用 SA_RESTART
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = on_int;
sigemptyset(&sa.sa_mask);
sa.sa_flags = use_restart ? SA_RESTART : 0;
sigaction(SIGINT, &sa, NULL);
printf("SA_RESTART = %s\n", use_restart ? "开" : "关");
printf("现在阻塞在 read 上。按 Ctrl+C,观察 read 的返回。\n");
char buf[128];
ssize_t n = read(STDIN_FILENO, buf, sizeof(buf) - 1);
if (n == -1) {
if (errno == EINTR)
printf("read 返回 -1,errno = EINTR(被中断,未重启)\n");
else
perror("read");
} else {
buf[n] = '\0';
printf("read 返回 %zd,内容: %s\n", n, buf);
}
return 0;
}运行 ./a.out(无 SA_RESTART)然后按 Ctrl+C,会看到 read 返回 -1,errno = EINTR。运行 ./a.out restart(有 SA_RESTART)按 Ctrl+C,处理函数打印后 read 继续等输入,你输入字符后它正常返回。
易错:不检查 EINTR 的代码在某些信号环境下会意外失败。想省事就加 SA_RESTART;想精确控制超时或退出,就不加,在循环里判断 errno == EINTR 决定是否重试。
17. 总结
把这一章串成一条线:
- 信号是异步通知。产生后先进入未决集,阻塞集决定"放不放行",放行后递达,执行默认动作、忽略或处理函数。
- 屏蔽字用
sigprocmask改,未决集用sigpending查。SIG_BLOCK只增不减,SIG_UNBLOCK只减不增,SIG_SETMASK整体覆盖。 - 发信号用 kill(或 alarm / setitimer)。kill 的 pid 有四种含义,
sig = 0探活。 - 捕捉优先用
sigaction。sa_mask实现处理期间的临时额外屏蔽,SA_RESTART控制慢速系统调用是否自动重启。 - 回收子进程靠
SIGCHLD+ while 循环waitpid(-1, ..., WNOHANG)。先屏蔽再注册再 fork,避免竞态;循环回收,防漏僵尸。 - 慢速系统调用可能被信号打断。
SA_RESTART自动重启大多数慢速调用,但 sleep 永不重启,返回剩余秒数。
18. 复习:子进程借助信号回收
自测清单(每一条都能对应到上面的案例):
- [ ] 能画出"产生 → 未决 → 阻塞/递达"的关系图吗?
- [ ]
kill(0, SIGINT)和kill(-1, SIGINT)分别发给谁? - [ ]
alarm(10)后alarm(3)的返回值大约是多少?旧闹钟还有效吗? - [ ]
SIG_BLOCK和SIG_SETMASK最关键的区别是什么? - [ ]
sa_mask的屏蔽是永久的还是临时的? - [ ] SIGCHLD 处理函数里为什么必须用 while 循环 waitpid?
- [ ] 处理函数里为什么用 write 而不是 printf?
- [ ]
SA_RESTART对 read 和对 sleep 的效果有什么不同?
如果这八条都能说清楚,这一章的核心就抓住了。剩下的细节在写代码时翻 man 手册确认原型和标志位即可。
补充篇
上面是正文,从这一节开始是补充:参数细节、几个正文没提到的函数、以及 C++17 下的写法。
示例统一用g++ -std=c++17 -Wall编译。
19. 其他常用短函数
#include <signal.h>
int raise(int sig); // 给自己发信号,等价 kill(getpid(), sig)
void abort(void); // 发 SIGABRT,并且保证进程最终终止
#include <unistd.h>
int pause(void); // 挂起直到收到信号,永远返回 -1,errno = EINTR
unsigned int sleep(unsigned int seconds);
#include <time.h>
int nanosleep(const struct timespec *req, struct timespec *rem);raise:单线程里就是 kill(getpid(), sig);多线程里 glibc 用 tgkill 发给调用它的那个线程,所以是"发给自己这个线程"。返回 0 成功,-1 失败。
abort:不管 SIGABRT 有没有被捕获、有没有被阻塞,进程最终都会死。它的内部流程是先解除阻塞、把处理方式复位成 SIG_DFL、再 raise,最后还有一层兜底。所以不要指望用 SIGABRT 的处理函数"拦住" abort。
pause:只有 -1 这一种返回值,没有"成功返回"。想等信号又不想丢信号,用 sigsuspend(20 节)。
sleep 和 nanosleep:被信号打断后返回的是剩余时间,不是 -1。sleep 返回剩余秒数,nanosleep 把剩余时间写进 rem(rem 可以传 NULL)。两者都不受 SA_RESTART 影响,永远不会自动重启,这一点最容易记反。
// 睡满给定时间,被打断就接着睡剩下的部分
#include <cerrno>
#include <cstdio>
#include <ctime>
void sleep_full(double seconds) {
struct timespec req{};
req.tv_sec = static_cast<std::time_t>(seconds);
req.tv_nsec = static_cast<long>((seconds - static_cast<double>(req.tv_sec)) * 1e9);
struct timespec rem{};
while (nanosleep(&req, &rem) == -1 && errno == EINTR) {
req = rem; // 内核把剩余时间写进了 rem,接着睡
}
}
int main() {
sleep_full(0.5);
std::puts("done");
return 0;
}timespec 的两个字段:tv_sec 秒,tv_nsec 纳秒(0~999999999)。
顺带两个容易漏的:
int sigaltstack(const stack_t *ss, stack_t *old_ss);
// stack_t: ss_sp 备用栈起始地址, ss_size 字节数, ss_flags = 0 表示启用、SS_DISABLE 表示关闭sigaltstack 给线程注册一块备用栈,配合 SA_ONSTACK,处理函数就跑在这块栈上。栈溢出引起的 SIGSEGV 必须这么写,否则处理函数一进去又踩爆栈,直接变成无声的 core。
int sigsetjmp(sigjmp_buf env, int savesigs); // savesigs 非 0 才保存屏蔽字
void siglongjmp(sigjmp_buf env, int val);从处理函数里跳回主流程时,必须用带 sig 的版本。普通 longjmp 不恢复屏蔽字,跳出来以后屏蔽字会停在"处理函数执行期间"的状态,后续信号全被莫名堵住。这类写法现在算老派,新代码更推荐标志位 + 主循环,或者 sigwait / signalfd。
20. sigsuspend 函数
#include <signal.h>
int sigsuspend(const sigset_t *mask);参数:mask 是临时使用的新屏蔽字,不是"要解除的信号集"。这点最容易写反——想等 SIGINT,就要传"当前屏蔽字去掉 SIGINT"的集合,而不是只含 SIGINT 的集合。
行为:把调用线程的屏蔽字设成 mask 后挂起;收到一个不在 mask 里的信号、且处理函数返回之后,屏蔽字自动恢复成调用前的值,函数返回 -1 且 errno = EINTR。没有别的返回值。
它解决的问题:sigprocmask(SIG_UNBLOCK) 和 pause() 之间存在窗口期,信号正好落在这个缝里,pause 就永远等下去。sigsuspend 把这两步合成一个原子操作,没有窗口。
#include <csignal>
#include <cstdio>
#include <unistd.h>
namespace {
volatile std::sig_atomic_t g_stop = 0;
void on_term(int) { g_stop = 1; }
}
int main() {
struct sigaction sa{};
sa.sa_handler = on_term;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0; // 不加 SA_RESTART,sigsuspend 才会被打断
sigaction(SIGTERM, &sa, nullptr);
sigset_t blocked;
sigemptyset(&blocked);
sigaddset(&blocked, SIGTERM);
sigprocmask(SIG_BLOCK, &blocked, nullptr);
sigset_t wait_mask;
sigprocmask(SIG_BLOCK, nullptr, &wait_mask); // 传 NULL 只查询,取出当前屏蔽字
sigdelset(&wait_mask, SIGTERM); // 等待期间放开 SIGTERM
std::puts("等 SIGTERM ...");
while (!g_stop) {
sigsuspend(&wait_mask); // 总是返回 -1
}
std::puts("收到,退出");
return 0;
}典型用途就是"等一个信号,但等待窗口里不想丢别的信号"。如果要同时等"信号或文件可读",用下一节的 signalfd + poll。
21. sigqueue 与实时信号
kill 只能发编号,不能带数据。要带数据用 sigqueue:
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);
union sigval {
int sival_int; // 传一个整数
void *sival_ptr; // 传一个指针,只对同一进程内的接收者有意义
};参数:pid 的语义和 kill 完全一致;sig 建议用实时信号 SIGRTMIN ~ SIGRTMAX;value 是随信号一起送过去的数据。返回 0 成功,-1 失败。
接收端要用三参数版本,并且 sa_flags 必须带 SA_SIGINFO:
void handler(int sig, siginfo_t *info, void *ucontext);
// ucontext 是被打断时的机器上下文,普通场景用不到,忽略即可siginfo_t 常用字段:
| 字段 | 含义 |
|---|---|
si_signo | 信号编号 |
si_code | 来源:SI_USER(kill/raise)、SI_QUEUE(sigqueue)、SI_TKILL、SI_KERNEL 等 |
si_pid / si_uid | 发送者的 pid / uid |
si_value | sigqueue 带来的那个 union sigval |
si_status | SIGCHLD 时表示子进程的退出状态 |
实时信号和常规信号最实际的区别:实时信号排队,发 5 个收 5 次;常规信号不排队,发 5 个可能只处理 1 次。
#include <csignal>
#include <cstdio>
#include <sys/wait.h>
#include <unistd.h>
namespace {
volatile std::sig_atomic_t g_count = 0;
volatile std::sig_atomic_t g_last = 0;
void on_rt(int, siginfo_t* info, void*) {
g_count++;
g_last = info->si_value.sival_int;
}
}
int main() {
const int signo = SIGRTMIN;
struct sigaction sa{};
sa.sa_sigaction = on_rt;
sa.sa_flags = SA_SIGINFO;
sigemptyset(&sa.sa_mask);
if (sigaction(signo, &sa, nullptr) == -1) {
std::perror("sigaction");
return 1;
}
sigset_t set, old;
sigemptyset(&set);
sigaddset(&set, signo);
sigprocmask(SIG_BLOCK, &set, &old); // 先堵住,免得子进程发得太快
pid_t pid = fork();
if (pid == 0) {
for (int i = 0; i < 5; ++i) {
union sigval v{};
v.sival_int = i;
if (sigqueue(getppid(), signo, v) == -1) std::perror("sigqueue");
}
_exit(0); // 子进程不要用 exit,避免刷新父进程的缓冲
}
waitpid(pid, nullptr, 0);
sigprocmask(SIG_SETMASK, &old, nullptr); // 放开,5 个信号依次递达
sleep(1);
std::printf("收到 %d 次,最后一次 value = %d\n", static_cast<int>(g_count), static_cast<int>(g_last));
return 0;
}把 SIGRTMIN 换成 SIGUSR1 再跑一遍,g_count 基本是 1。这就是"常规信号不排队"的实证。
处理函数里只做了一个赋值,没调 printf,是因为 printf 不是异步信号安全的(见 26 节)。
22. sigwait 与 signalfd
处理函数是异步的,栈回溯不好看。另一条路是先阻塞,再同步地把信号取出来。
#include <signal.h>
int sigwait(const sigset_t *set, int *sig);
int sigwaitinfo(const sigset_t *set, siginfo_t *info);
int sigtimedwait(const sigset_t *set, siginfo_t *info,
const struct timespec *timeout);参数:set 是"我要接收的信号集合",里面的信号必须事先被阻塞,否则会先走默认动作或处理函数,轮不到 sigwait;sig 输出收到的编号;timeout 传 NULL 表示一直等。成功返回 0,失败返回错误码本身(不是设置 errno,这一点和别的函数不一样)。
sigwait 适合"专门开一个线程收信号"的模型。已经用 poll/epoll 的事件循环更适合 signalfd——信号变成一个可读的 fd,和其他事件一起等。
#include <cerrno>
#include <csignal>
#include <cstdio>
#include <poll.h>
#include <sys/signalfd.h>
#include <unistd.h>
int main() {
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGTERM);
// 必须先阻塞,否则信号走默认动作,signalfd 读不到
if (sigprocmask(SIG_BLOCK, &mask, nullptr) == -1) {
std::perror("sigprocmask");
return 1;
}
int fd = signalfd(-1, &mask, SFD_CLOEXEC); // 第一个参数传 -1 表示新建
if (fd == -1) {
std::perror("signalfd");
return 1;
}
std::puts("signalfd 就绪,按 Ctrl+C 测试");
for (;;) {
pollfd pfd{fd, POLLIN, 0};
if (poll(&pfd, 1, -1) == -1) {
if (errno == EINTR) continue;
std::perror("poll");
break;
}
signalfd_siginfo si{};
ssize_t n = read(fd, &si, sizeof si);
if (n != static_cast<ssize_t>(sizeof si)) continue;
std::printf("收到信号 %u,来自 pid %u\n", si.ssi_signo, si.ssi_pid);
if (si.ssi_signo == SIGTERM) break;
}
close(fd);
return 0;
}signalfd 返回的是一个只读 fd。一次 read 有可能取回多个 signalfd_siginfo,严谨写法要按 n / sizeof(signalfd_siginfo) 循环取,上面为简洁只取了一条。
sigwait 的多线程用法,骨架是这样:
#include <pthread.h>
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigaddset(&set, SIGTERM);
pthread_sigmask(SIG_BLOCK, &set, nullptr); // 每个线程都要做,包括 worker
int signo = 0;
while (sigwait(&set, &signo) == 0 && signo != SIGTERM) {
// 处理 SIGINT
}23. 补充:多线程下的信号
- 信号是发给进程的:
kill(pid, sig)、终端按键都是进程级。内核会从这个进程里挑一个没阻塞该信号的线程递达,挑谁不确定。 - 如果所有线程都阻塞了这个信号,它就 pending 挂着,等某个线程解除阻塞或用
sigwait取走。 - 每个线程有自己的信号屏蔽字。
pthread_sigmask只改当前调用线程,不影响别的线程。 - 多线程程序按 POSIX 应该用
pthread_sigmask。Linux 上sigprocmask和它实现一致,实际也只影响当前线程,但别混着理解。 - 新线程继承创建它的线程的屏蔽字。所以主线程要在创建业务线程之前先把关心的信号阻塞掉,晚了就继承不到。
- 阻塞 ≠ 忽略:阻塞是先挂着 pending;忽略是
SIG_IGN直接丢掉。 handler 在哪个线程执行不确定,因为进程级信号可能被任意未阻塞线程收走。所以 handler 里:
- 不要碰线程局部存储 TLS(Thread Local Storage,线程局部存储);
- 不要加锁;
- 不要
printf、malloc这类非 async-signal-safe 函数; - 只调 async-signal-safe 函数。
- 常规套路:主线程启动时先阻塞关心的信号 → 创建专门线程用
sigwait收信号,或用signalfd→ 业务线程继承阻塞状态,全程不被打扰。 pthread_kill(tid, sig)是唯一能定向到指定线程的接口。kill(pid, sig)不能指定线程;raise(sig)本质是发给当前线程。我容易错的点:
- 以为
sigprocmask会改所有线程:不会,只改当前线程。 - 以为
kill能发给某个线程:不能,它是进程级。 - 以为 handler 总在主线程跑:不一定。
- 以为"阻塞信号"就是"忽略信号":不是。
- 主线程先创建了业务线程再阻塞信号:已经晚了,业务线程继承了旧屏蔽字。
- 以为
24. 补充:fork 和 exec 对信号状态的影响
| 状态 | fork 之后 | exec 之后 |
|---|---|---|
| 处理函数 | 继承 | 复位成 SIG_DFL(原来是 SIG_IGN 的仍是 SIG_IGN) |
| 屏蔽字 | 继承 | 保留 |
| 未决集 | 清空 | 保留 |
| 备用栈 | 继承 | 关闭(SS_DISABLE) |
实践含义:
- 子进程继承父进程的屏蔽字。父进程为了防竞态先屏蔽 SIGCHLD 再 fork,子进程身上也是屏蔽的,别忘了在子进程里恢复。
- exec 之后自定义处理函数全部失效,这是好事,不然新程序会跑进旧程序的函数。但被忽略的信号会一路继承,
nohup就是靠忽略 SIGHUP 实现的。 - 没有"重置全部信号"的 API,只能遍历 1~64 逐个
signal(i, SIG_DFL),SIGKILL / SIGSTOP 会失败,忽略错误即可。
25. C++17 实践:RAII 封装和优雅退出
先明确 handler 的限制:只能是普通函数指针,不能是 std::function,不能捕获 lambda。所以惯用法是"全局原子标志 + 主循环干活"。
用 std::atomic<bool> 比 volatile sig_atomic_t 更清楚。C++ 标准保证 lock-free 的原子类型可以在处理函数里安全使用,而 std::atomic<bool> 在所有主流平台上都是 lock-free,这一点还能用 static_assert 在编译期验证。
#include <atomic>
#include <csignal>
#include <cstdio>
#include <ctime>
#include <unistd.h>
namespace {
std::atomic<bool> g_quit{false};
void on_signal(int) {
g_quit.store(true, std::memory_order_relaxed);
}
// 构造时安装,析构时恢复原来的处理方式
class SigactionGuard {
public:
SigactionGuard(int signo, void (*handler)(int), int flags = 0) : signo_(signo) {
struct sigaction sa{};
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = flags;
ok_ = sigaction(signo_, &sa, &old_) == 0;
}
~SigactionGuard() {
if (ok_) sigaction(signo_, &old_, nullptr);
}
SigactionGuard(const SigactionGuard&) = delete;
SigactionGuard& operator=(const SigactionGuard&) = delete;
bool ok() const { return ok_; }
private:
int signo_;
bool ok_{false};
struct sigaction old_{};
};
} // namespace
int main() {
static_assert(std::atomic<bool>::is_always_lock_free, "处理函数里只能用 lock-free 原子");
SigactionGuard g1(SIGINT, on_signal);
SigactionGuard g2(SIGTERM, on_signal);
if (!g1.ok() || !g2.ok()) {
std::perror("sigaction");
return 1;
}
int round = 0;
while (!g_quit.load(std::memory_order_relaxed)) {
std::printf("工作第 %d 轮\n", ++round);
struct timespec ts{0, 200 * 1000 * 1000}; // 200ms
nanosleep(&ts, nullptr); // 被打断也无所谓,下一轮再看标志
}
std::puts("收到退出信号,做清理后退出");
return 0;
}几个要点:
- 主循环用短睡眠 + 检查标志,不要用
pause()。pause()遇到无关信号会立刻返回,容易空转。 - 析构时恢复旧设置,避免"对象没了但信号还指着它的函数"。
- 如果需要"阻塞等待 + 立刻响应",用 self-pipe:处理函数里只
write(fd, &b, 1),主线程poll这个 pipe 的读端。write是异步信号安全的,这是唯一能在处理函数里安全叫醒主线程的手段。signalfd本质上是同一思路的更现代版本。
26. 补充:EINTR 重试和常见坑
只要有不带 SA_RESTART 的处理函数,或者用了 sigsuspend / sigwait,就得处理 EINTR。封装成模板最省事:
#include <cerrno>
template <typename F>
auto retry_on_eintr(F&& f) -> decltype(f()) {
decltype(f()) r;
do {
r = f();
} while (r == -1 && errno == EINTR);
return r;
}
// 用法
// ssize_t n = retry_on_eintr([&] { return read(fd, buf, sizeof buf); });只对"可以重来"的调用这么做:read / write / accept / wait 安全;已经消费了一半数据、或者带副作用的调用,重试前要自己判断。
常见坑清单,都是实际会遇到的:
| 现象 | 原因 |
|---|---|
| 进程被无声杀掉,什么都没打印 | 往已关闭的 socket / pipe 写数据触发了 SIGPIPE;忽略它,改判 EPIPE |
| 僵尸进程越积越多 | SIGCHLD 处理函数里只 wait 一次;要 while + WNOHANG |
| 信号"丢了" | 常规信号不排队;要么用实时信号,要么用标志位 |
| fork 后子进程行为不对 | 子进程继承了父进程的屏蔽字 |
sleep 提前醒来还返回正数 | 被信号打断,返回的是剩余秒数,不是错误码 |
加了 SA_RESTART 但 read 还是返回 EINTR | 该调用本身不支持重启(poll 的超时、部分 socket 操作),或者是在 sigsuspend 里 |
| 处理函数里 printf 偶尔卡死 | printf 不是异步信号安全的,会死在 stdio 的锁上 |
| 行为随机、时好时坏 | sigset_t 忘了 sigemptyset,sigaction 结构体忘了清零 |
system() / popen() 之后 SIGCHLD 处理函数像失效了 | 它们内部会改 SIGCHLD 的处理方式,用完不一定恢复 |
SIGPIPE 的标准处理:
struct sigaction sa{};
sa.sa_handler = SIG_IGN; // 忽略后 write 返回 -1 / EPIPE,进程不会被杀
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGPIPE, &sa, nullptr);27. 补充复习清单
- [ ]
sa_flags里SA_RESTART、SA_SIGINFO、SA_NODEFER、SA_RESETHAND各控制什么? - [ ]
sigsuspend的参数是"新屏蔽字"还是"要放开的信号"? - [ ] 想在处理函数里拿到发送者的 pid,需要哪两个条件?
- [ ] 实时信号和常规信号在排队上的差别?怎么用几行代码验证?
- [ ] 多线程程序里信号会递给哪个线程?怎么把信号集中到一个线程处理?
- [ ] exec 之后哪些信号状态会保留,哪些不会?
- [ ] 为什么处理函数里能改
std::atomic<bool>,却不能调用malloc或printf?
