信号处理深度解读
# 05.信号处理深度解读
信号是 Linux 最古老的 IPC 机制,也是嵌入式系统崩溃捕获和优雅退出最常用的手段。本节把信号生命周期拆开,教你写出不被信号杀死的嵌入式应用。
# 目录介绍
- 5.1 案例引入
- 5.2 信号的生命周期
- 5.3 sigaction 的正确用法
- 5.4 关键信号的嵌入式实战
- 5.5 信号与线程
- 5.6 signalfd:把信号变成文件描述符
- 5.7 关键结论与速查表
# 5.1 案例引入
# 5.1.1 Qt 应用收到 SIGTERM 后没来得及保存配置
某工业 HMI 软件在系统关机时,systemd 发送 SIGTERM 给 Qt 进程,要求 90 秒内退出。开发者在信号处理器中调用 QSettings::sync() 保存配置——然后在信号处理器里触发了 malloc(QString 内部分配内存)。
信号处理器中调用 malloc → 如果主程序正在 malloc 中持有锁 → 死锁。设备关机时最后一次配置保存丢失,下次上电用户发现所有参数回到出厂值。
出问题的代码:
// ❌ 信号处理器中干的事太多
void sigterm_handler(int sig) {
// QSettings::sync() → QMap::insert() → malloc → 如果主线程正持锁 → 死锁
settings->sync();
// printf → fprintf → malloc (stdout 的缓冲区) → 同样危险
printf("shutting down...\n");
// fclose → free → 死锁风险
fclose(log_file);
exit(0);
}
修复方案——"信号处理器只设标志位"模式:
// ✅ 信号处理器只做一件事:写 flag
volatile sig_atomic_t shutdown_flag = 0;
void sigterm_handler(int sig) {
shutdown_flag = 1; // ← 唯一的操作:原子类型赋值
}
// 主循环检查 flag
int main() {
signal(SIGTERM, sigterm_handler);
signal(SIGINT, sigterm_handler);
while (!shutdown_flag) {
process_events();
}
// 安全:在主控制流中做清理
settings->sync(); // ✅ 不在信号处理器内,malloc 安全
fclose(log_file);
printf("clean shutdown\n");
return 0;
}
为什么 volatile sig_atomic_t 是信号处理器的铁律?
| 属性 | sig_atomic_t | 普通 int |
|---|---|---|
| 读写原子性 | POSIX 保证 | 不保证(64位可能半个字) |
| 信号处理安全 | ✅ | ❌ |
volatile 作用 | 禁止编译器缓存 | 无 |
| 能否用于条件判断 | ✅ | ⚠ 未定义行为 |
sig_atomic_t 的典型大小:32 位系统上 int(4 字节),可以安全传递 exit code 或简单的计数器。
# 5.1.2 核心问题
- 信号处理器里到底能调用哪些函数?(async-signal-safe 白名单)
- 为什么
signalfd是比sigaction更现代的信号处理方式? - SIGPIPE 在嵌入式 socket 编程中的"沉默杀手"效应
- 进程级信号和线程级信号怎么协作?多线程程序里信号发给谁?
# 5.2 信号的生命周期
# 5.2.1 产生:硬件异常 vs 软件触发
信号从产生到处理的三类来源:
| 来源 | 触发方式 | 典型信号 | 例子 |
|---|---|---|---|
| 硬件异常 | CPU 检测到非法操作 → 内核翻译为信号 | SIGSEGV, SIGILL, SIGFPE, SIGBUS | 空指针解引用、除零、对齐错误 |
| 软件触发 | 进程/内核调用 kill/raise/abort | 全部可发送 | kill(pid, SIGTERM) |
| 终端事件 | 终端驱动检测到事件 → 通知前台进程组 | SIGINT, SIGTSTP, SIGHUP | Ctrl+C、Ctrl+Z、终端断开 |
| 软件条件 | 管道破裂、子进程退出、定时器到期 | SIGPIPE, SIGCHLD, SIGALRM | 写已关闭的 socket、子进程退出 |
硬件异常的处理链路(以 SIGSEGV 为例):
1. CPU 执行: mov [rax], rdi (rax = NULL)
2. MMU: 页表项 Present bit = 0 → 触发 Page Fault
3. CPU: 保存上下文 → 切换内核栈 → 调 do_page_fault()
4. 内核: 检查 VMA (vm_area_struct)
若 rax 地址不在任何 VMA 中 → 向当前进程递送 SIGSEGV
5. 内核: 准备返回用户态 → 检测 pending 信号 → 调用 sig_handler
6. 用户态: 信号处理器执行(或默认:coredump + 终止)
siginfo_t 在硬件异常时提供的调试信息:
void sigsegv_handler(int sig, siginfo_t *info, void *ucontext) {
printf("CRASH: signal=%d at addr=%p\n", sig, info->si_addr);
// info->si_addr = 引发异常的地址(NULL 或野指针)
// info->si_code = SEGV_MAPERR (空指针) / SEGV_ACCERR (权限错误)
}
# 5.2.2 递达:pending 队列与信号屏蔽字
信号的三种状态:
产生 → 未决 (pending) 或 阻塞 (blocked) → 递达 (delivered) → 处理
- 未决(pending):信号已产生但还未递达(内核中的 bitmap 记录)
- 阻塞(blocked):进程显式屏蔽该信号(信号屏蔽字
sigprocmask) - 递达(delivered):信号被交给进程处理
内核中的数据结构(task_struct 中):
// 简化版
struct task_struct {
struct sigpending pending; // 进程级 pending 信号集
sigset_t blocked; // 当前阻塞的信号集
struct sighand_struct *sighand; // 信号处理器表 (sigaction 数组)
};
struct sigpending {
struct list_head list; // 带附加数据的信号链表(实时信号)
sigset_t signal; // pending 信号位图
};
信号屏蔽字与 9 个不可屏蔽信号:
// SIGKILL(9) 和 SIGSTOP(19) 永远不能被屏蔽、忽略或捕捉
sigset_t set;
sigfillset(&set);
sigprocmask(SIG_BLOCK, &set, NULL); // 屏蔽所有信号
kill(getpid(), SIGKILL); // 立刻死亡,sigprocmask 挡不住
kill(getpid(), SIGSTOP); // 立刻暂停,挡不住
完整的不可屏蔽信号列表:SIGKILL, SIGSTOP,以及某些实现中的 SIGCONT(部分屏蔽)。
# 5.2.3 处理:默认/忽略/捕捉三种动作
每个信号有三种处置方式:
| 处置 | API | 效果 |
|---|---|---|
| 默认(SIG_DFL) | - | 内核按信号类型的标准行为处理(终止/coredump/忽略/暂停) |
| 忽略(SIG_IGN) | signal(sig, SIG_IGN) | 信号被丢弃,子进程继承此设置 |
| 捕捉 | sigaction(sig, &act, NULL) | 调用用户定义的处理函数 |
系统默认处置的分类表:
| 默认动作 | 信号 | 是否产生 core dump |
|---|---|---|
| 终止 (Term) | SIGTERM, SIGINT, SIGHUP, SIGALRM, SIGPIPE | 否 |
| Coredump + 终止 (Core) | SIGSEGV, SIGABRT, SIGILL, SIGFPE, SIGBUS, SIGQUIT | 是 |
| 忽略 (Ignore) | SIGCHLD, SIGURG, SIGWINCH | - |
| 暂停 (Stop) | SIGTSTP, SIGTTIN, SIGTTOU | - |
| 恢复 (Continue) | SIGCONT | - |
exec 后信号处置的继承规则:exec 后,被忽略的信号继续保持忽略(防止忽略 SIGCHLD 后 exec 的子程序僵尸满天飞),被捕捉的信号恢复为默认。
# 5.2.4 信号的丢失与排队
传统 Unix 信号(1-31)不排队——同一信号多次产生,只记录一次 pending:
进程 A 收到 3 次 SIGTERM:
t1: SIGTERM → pending bitmap [SIGTERM] = 1
t2: SIGTERM → pending bitmap [SIGTERM] 已经是 1,丢弃
t3: SIGTERM → 同上,丢弃
进程最终只处理 1 次 SIGTERM
实时信号(SIGRTMIN ~ SIGRTMAX,34-64)会排队:
#define MY_SIGNAL (SIGRTMIN + 1) // 实时信号
union sigval value;
value.sival_int = 42;
sigqueue(getpid(), MY_SIGNAL, value); // 带数据 + 排队的信号
// 信号处理器中
void rt_handler(int sig, siginfo_t *info, void *ctx) {
printf("got value: %d\n", info->si_value.sival_int);
}
实时信号 vs 传统信号:
| 特性 | 传统信号 (1-31) | 实时信号 (34-64) |
|---|---|---|
| 是否排队 | ❌ 只记一个 bit | ✅ 每条都入队 |
| 递送顺序 | 无保证 | 低编号优先(同编号按 FIFO) |
| 携带数据 | 否 | 是(si_value 可以是 int 或指针) |
| 来源识别 | 否 | 是(si_pid + si_uid) |
| 典型用途 | SIGTERM/SIGINT | 线程间通知、自定义 IPC |
队列深度限制:/proc/sys/kernel/rt_sigqueue_max(通常为 1024),超出后 sigqueue 返回 EAGAIN。
# 5.3 sigaction 的正确用法
# 5.3.1 struct sigaction 字段详解
sigaction 是当代信号处理的标准接口——signal() 的行为在不同 Unix 变体间不一致(BSD 自动重启系统调用,System V 不重启),POSIX 已将其标记为废弃。
struct sigaction {
void (*sa_handler)(int); // 简单处理器 (SIG_DFL/SIG_IGN/函数指针)
void (*sa_sigaction)(int, siginfo_t *, void *); // 扩展处理器 (SA_SIGINFO 标志下使用)
sigset_t sa_mask; // 执行处理器期间额外屏蔽的信号
int sa_flags; // 行为标志
void (*sa_restorer)(void); // 已废弃 (glibc 内部使用)
};
sa_mask 的作用——阻止信号嵌套:
// 默认:在处理 SIGTERM 期间,SIGTERM 被自动屏蔽(防止嵌套)
// sa_mask 可以额外屏蔽其他信号
struct sigaction act = {0};
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, SIGINT); // 处理 SIGTERM 时也屏蔽 SIGINT
sigaddset(&act.sa_mask, SIGQUIT); // 也屏蔽 SIGQUIT
act.sa_handler = handler;
sigaction(SIGTERM, &act, NULL);
# 5.3.2 SA_SIGINFO 与 siginfo_t 扩展信息
设置 SA_SIGINFO 后,处理器使用三参数签名,能获取丰富的上下文信息:
void sig_handler(int sig, siginfo_t *info, void *ucontext) {
printf("Signal: %d, sender PID: %d, addr: %p\n",
sig, info->si_pid, info->si_addr);
}
siginfo_t 的关键字段:
| 字段 | 含义 | 适用信号 |
|---|---|---|
si_signo | 信号编号 | 所有 |
si_code | 信号来源代码(SI_USER/SI_KERNEL/SEGV_MAPERR 等) | 所有 |
si_pid | 发送进程的 PID | SI_USER / SI_QUEUE |
si_uid | 发送进程的 UID | SI_USER / SI_QUEUE |
si_addr | 引发异常的地址 | SIGSEGV, SIGBUS |
si_status | 子进程退出状态 | SIGCHLD |
si_value | sigqueue 传递的值 | 实时信号 |
ucontext 参数——访问信号发生时的 CPU 寄存器:
#include <ucontext.h>
void crash_handler(int sig, siginfo_t *info, void *ucontext) {
ucontext_t *uc = (ucontext_t *)ucontext;
// x86_64 下获取 RIP(PC):
void *ip = (void *)uc->uc_mcontext.gregs[REG_RIP];
// ARM 下获取 PC:
// void *pc = (void *)uc->uc_mcontext.arm_pc;
printf("CRASH at IP=%p, addr=%p\n", ip, info->si_addr);
// 可写入 crash log 文件用于事后分析
_exit(128 + sig); // 只能用 _exit,不能用 exit!
}
# 5.3.3 SA_RESTART 与被中断的系统调用
慢系统调用(read/write/sleep/select/poll/wait)被信号中断后有两种行为:
| 标志 | 被信号中断后 |
|---|---|
未设 SA_RESTART | 系统调用返回 -1,errno = EINTR |
设 SA_RESTART | 内核自动重启系统调用(继续等) |
// 没有 SA_RESTART
char buf[1024];
int n = read(fd, buf, sizeof(buf));
if (n < 0 && errno == EINTR) {
// 被信号中断,要手动重试
n = read(fd, buf, sizeof(buf));
}
// 有 SA_RESTART
struct sigaction act = {.sa_handler = handler, .sa_flags = SA_RESTART};
sigaction(SIGTERM, &act, NULL);
int n = read(fd, buf, sizeof(buf)); // 被中断后内核自动重试
SA_RESTART 不能重启所有调用——以下调用永远不被重启(POSIX 规定):
poll, ppoll, select, pselect, epoll_wait, epoll_pwait,
sleep, nanosleep, clock_nanosleep,
connect (对 socket), accept
嵌入式最佳实践——不依赖 SA_RESTART,永远手动检查 errno == EINTR:
while ((n = read(fd, buf, sizeof(buf))) < 0) {
if (errno == EINTR) continue; // 被信号中断,重试
perror("read");
break; // 真正的错误
}
# 5.3.4 SA_NODEFER/SA_ONSTACK
SA_NODEFER——默认在信号处理器执行期间,当前信号被自动屏蔽(防止递归)。设置此标志取消自动屏蔽,允许信号嵌套:
// 一般不推荐——递归信号处理器容易堆栈溢出
act.sa_flags |= SA_NODEFER;
SA_ONSTACK——在独立栈上执行信号处理器(必设!):
// 分配信号处理器专用栈
stack_t ss;
ss.ss_sp = malloc(SIGSTKSZ); // 8-16 KB 足够
ss.ss_size = SIGSTKSZ;
ss.ss_flags = 0;
sigaltstack(&ss, NULL);
// 告知内核用此栈处理信号
act.sa_flags |= SA_ONSTACK;
sigaction(SIGSEGV, &act, NULL);
为什么需要 SA_ONSTACK?
场景:线程栈已满(stack overflow)→ 触发 SIGSEGV
没有独立信号栈 → 内核试图在已满的栈上执行 SIGSEGV 处理器
→ 又触发 SIGSEGV → 嵌套 → 进程直接终止(coredump 不完整)
有 SA_ONSTACK: 内核切换到备用栈 → 信号处理器正常执行 → 可以记录 crash log
# 5.4 关键信号的嵌入式实战
# 5.4.1 SIGSEGV 崩溃捕获与 core dump
嵌入式崩溃捕获的标准流程:
static void crash_handler(int sig, siginfo_t *info, void *ucontext) {
// 步骤 1:立即屏蔽所有信号 —— 防止嵌套崩溃
sigset_t all;
sigfillset(&all);
sigprocmask(SIG_BLOCK, &all, NULL);
// 步骤 2:只调 async-signal-safe 函数写入 crash log
int crash_fd = crash_fd_global; // 在 main 中提前 open 好的
char buf[256];
int len = snprintf(buf, sizeof(buf),
"CRASH: sig=%d addr=%p pid=%d\n",
sig, info->si_addr, getpid());
write(crash_fd, buf, len); // ← write 是信号安全的!
// 步骤 3:如果需要 coredump,恢复默认处置后给自己重新发同信号
signal(sig, SIG_DFL);
raise(sig); // ← 触发默认行为:coredump
// 此时代码不会执行到这里
}
// 在 main 中初始化 crash fd 和备用栈
int crash_fd;
int main() {
crash_fd = open("/data/crash.log", O_WRONLY | O_CREAT | O_APPEND, 0644);
struct sigaction act = {0};
act.sa_sigaction = crash_handler;
act.sa_flags = SA_SIGINFO | SA_ONSTACK;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, SIGSEGV);
sigaddset(&act.sa_mask, SIGBUS);
sigaddset(&act.sa_mask, SIGFPE);
sigaddset(&act.sa_mask, SIGILL);
sigaddset(&act.sa_mask, SIGABRT);
sigaction(SIGSEGV, &act, NULL);
sigaction(SIGBUS, &act, NULL);
sigaction(SIGFPE, &act, NULL);
sigaction(SIGILL, &act, NULL);
sigaction(SIGABRT, &act, NULL);
// ...
}
嵌入式 core dump 配置:
# 开启 coredump,限制大小
ulimit -c unlimited
# 指定 coredump 文件路径
echo "/data/coredump/core.%e.%p.%t" > /proc/sys/kernel/core_pattern
# 嵌入式系统(闪存空间有限)常用过滤
echo 0x7f > /proc/self/coredump_filter # 只 dump 匿名私有映射 + ELF 头
# 5.4.2 SIGPIPE 的默认行为与嵌入式网络程序
SIGPIPE 产生的条件:向一个已经断开连接的 socket/fd 写数据。
int fd = socket(...); connect(fd, ...);
// 对端关闭连接
close(peer_fd);
// 本端第一次写——触发 RST
write(fd, buf, 10); // 返回 -1, errno = ECONNRESET
// 本端第二次写——触发 SIGPIPE(默认动作:杀死进程!)
write(fd, buf, 10); // ← 进程被 SIGPIPE 杀死!
嵌入式设备上的典型事故:
HMI 程序通过 socket 连后台服务器
服务器端异常重启 → 所有客户端连接断开
HMI 的定时器线程还在往 socket 写数据
→ 第一次写收到 RST
→ 第二次写触发 SIGPIPE → 整个 HMI 进程被杀
→ 司机面前的屏幕黑屏!
三种修复方案:
// 方案 A:忽略 SIGPIPE(全局生效)
signal(SIGPIPE, SIG_IGN);
// write 会返回 -1,errno = EPIPE
// 方案 B:为 socket 设 MSG_NOSIGNAL(只影响此 socket)
int flags = MSG_NOSIGNAL;
send(fd, buf, len, flags); // 用 send 代替 write
// 或者:
int set = 1;
setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &set, sizeof(set));
// 方案 C:检查 POLLRDHUP(epoll/poll 事件)提前感知对端断开
struct pollfd pfd = {.fd = fd, .events = POLLIN | POLLRDHUP};
建议:嵌入式网络程序永远在 main 第一行加 signal(SIGPIPE, SIG_IGN)。
# 5.4.3 SIGCHLD 与子进程回收
SIGCHLD 的默认动作是忽略——但这不是说子进程不会变僵尸。忽略 SIGCHLD 只影响信号处理器,不影响僵尸回收。
// 方法 A:waitpid 循环回收(已在进程管理篇详述)
void sigchld_handler(int sig) {
while (waitpid(-1, NULL, WNOHANG) > 0);
}
// 方法 B:SIG_IGN —— 告诉内核自动回收(Linux 专有行为)
signal(SIGCHLD, SIG_IGN);
// 此后 fork 的子进程退出时 kernel 自动做 wait,无僵尸
// 注意:SIG_IGN 与实际注册 SIG_IGN 后调用 waitpid 的区别
// SIG_IGN + SIGCHLD: waitpid 返回 -1, errno = ECHILD(找不到子进程)
SA_NOCLDWAIT——另一个自动回收选项:
struct sigaction act = {.sa_handler = SIG_DFL, .sa_flags = SA_NOCLDWAIT};
sigaction(SIGCHLD, &act, NULL);
// 效果类似 SIG_IGN —— 内核自动回收,不产生僵尸
# 5.4.4 SIGTERM/SIGINT 优雅退出模板
"优雅退出"的完整状态机:
收到 SIGTERM/SIGINT
→ 设 shutdown_flag = 1
→ 主循环退出
→ 通知各模块释放资源(每个模块有 N 秒的清理时间)
→ 保存配置(flush / fsync)
→ close 所有 fd(包括 socket)
→ 等待所有子线程 join
→ exit(0)
volatile sig_atomic_t shutdown = 0;
static void term_handler(int sig) { shutdown = 1; }
int main() {
struct sigaction act = {.sa_handler = term_handler};
sigemptyset(&act.sa_mask);
// 不设 SA_RESTART —— 让阻塞的 accept/select 返回 EINTR
sigaction(SIGTERM, &act, NULL);
sigaction(SIGINT, &act, NULL);
// 主循环
while (!shutdown) {
int fd = accept(listen_fd, NULL, NULL);
if (fd < 0) {
if (errno == EINTR) continue; // 被信号中断,检查 shutdown flag
perror("accept"); break;
}
handle_client(fd);
}
// 延期清理
printf("Shutting down...\n");
save_config();
join_all_threads();
close_all_fds();
return 0;
}
systemd 通知协议(sd_notify):
// 在信号处理器中通知 systemd 正在关闭(systemd 会等足够长时间)
sd_notify(0, "STOPPING=1");
// 清理完后通知 systemd
sd_notify(0, "STATUS=Shutdown complete");
# 5.4.5 SIGSTOP/SIGCONT 与嵌入式调试
SIGSTOP 是"进程暂停"神器——在 GDB 不适用或无法连接的嵌入式系统上,用信号做临时光效观察:
# 暂停进程
kill -STOP $(pidof hmi_app)
# 此刻进程完全冻结(所有线程)
# 可以安全地:
# - 读 /proc/<pid>/fd/* → 看打开的文件
# - 读 /proc/<pid>/maps → 看内存布局
# - 读 /proc/<pid>/status → 看 CPU 时间、内存占用
# - cat /proc/<pid>/stack → 看内核栈(卡在哪个系统调用)
# 恢复运行
kill -CONT $(pidof hmi_app)
SIGCONT 的特殊行为:收到 SIGCONT 时,被 SIGSTOP/TSTP 暂停的进程自动恢复运行,且挂起的 SIGCONT 被丢弃(避免重复)。
# 5.5 信号与线程
# 5.5.1 进程级信号与线程级信号
所有的信号都是进程级的——信号不直接"发给某个线程",而是"发给进程"。内核任选一个不阻塞该信号的线程来递送。
线程选择规则:
1. 优选当前没有阻塞该信号的线程
2. 如果多个线程都不阻塞 → 任选其一(通常选主线程或最近运行的)
3. 如果所有线程都阻塞 → 信号留在 pending 队列,直到有线程解除阻塞
pthread_kill——向指定线程发信号:
pthread_kill(thread_id, SIGTERM); // 只向这个线程发送
pthread_kill(thread_id, 0); // 检测线程是否存在(不送信号)
# 5.5.2 pthread_sigmask 线程信号屏蔽
每个线程拥有独立的信号屏蔽字——可以不同线程屏蔽不同信号:
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGTERM);
// 只有主线程响应 SIGTERM
pthread_sigmask(SIG_BLOCK, &set, NULL); // 工作线程屏蔽 SIGTERM
pthread_sigmask(SIG_UNBLOCK, &set, NULL); // 主线程解除屏蔽
sigprocmask vs pthread_sigmask:
sigprocmask在多线程程序中行为未定义——永远只用pthread_sigmaskpthread_sigmask等价于单线程下的sigprocmask
# 5.5.3 专用信号处理线程的模式
经典模式——把信号处理集中到一个线程:
void *signal_thread(void *arg) {
sigset_t set;
sigfillset(&set);
pthread_sigmask(SIG_BLOCK, &set, NULL); // 本线程屏蔽所有信号
int sig;
while (1) {
// sigwait 等待任意被屏蔽的信号
if (sigwait(&set, &sig) == 0) {
// 在这里安全地处理——已经不是异步信号上下文!
if (sig == SIGTERM || sig == SIGINT) {
shutdown_flag = 1;
break;
}
}
}
return NULL;
}
int main() {
// 步骤 1:阻塞 SIGTERM/SIGINT(防止意外递送给其他线程)
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGTERM);
sigaddset(&set, SIGINT);
pthread_sigmask(SIG_BLOCK, &set, NULL);
// 步骤 2:创建信号处理线程(继承屏蔽集)
pthread_t tid;
pthread_create(&tid, NULL, signal_thread, NULL);
// 步骤 3:创建其他工作线程
// ...
pthread_join(tid, NULL);
}
sigwait 的优势:信号在普通函数上下文中处理——可以调 printf、malloc、pthread_mutex_lock——完全没有 async-signal-safe 限制。
# 5.6 signalfd:把信号变成文件描述符
# 5.6.1 signalfd 与 select/epoll 集成
signalfd 是 Linux 特有 API——把信号转换为文件描述符,可读时表示有信号到达:
#include <sys/signalfd.h>
// 步骤 1:阻塞需要用 signalfd 处理的信号
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGTERM);
sigaddset(&mask, SIGINT);
sigprocmask(SIG_BLOCK, &mask, NULL);
// 步骤 2:创建 signalfd
int sfd = signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC);
// 步骤 3:与 epoll 集成
struct epoll_event ev = {.events = EPOLLIN, .data.fd = sfd};
epoll_ctl(epfd, EPOLL_CTL_ADD, sfd, &ev);
// 步骤 4:事件循环中读取信号
while (1) {
int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
for (int i = 0; i < n; i++) {
if (events[i].data.fd == sfd) {
struct signalfd_siginfo fdsi;
read(sfd, &fdsi, sizeof(fdsi)); // 读一个信号 ← 读完后自动清 pending
if (fdsi.ssi_signo == SIGTERM) { graceful_shutdown(); }
}
// 处理其他 fd 事件...
}
}
signalfd 与 epoll 集成的核心价值——信号和 I/O、socket、timer 都在同一个事件循环中统一处理,不需要异步信号上下文。
# 5.6.2 signalfd 与 sigaction 的选择
| 维度 | sigaction | signalfd |
|---|---|---|
| 可调用函数 | 仅 async-signal-safe | 任何函数 |
| 线程安全 | 复杂(需屏蔽 + 专用线程) | 简单(epoll 天然线程局部) |
| 与 epoll 集成 | ❌ 需额外机制 | ✅ 原生 |
| 信号排队 | 否(需实时信号) | 自动排队 |
| 信号丢失风险 | 高(传统信号不排队) | 低(read 消费) |
| 复杂度 | 低(单信号简单处理时) | 中(需要事件循环) |
| 可移植性 | ✅ POSIX | ❌ Linux 专有 |
| 嵌入式适用 | ✅ 传统脚本启动的 daemon | ✅ 有 epoll 事件循环的嵌入式应用 |
选择建议:
单线程 daemon + 简单 SIGTERM 处理 → sigaction 足够
有 epoll 事件循环的程序 → signalfd(统一 I/O 和信号)
Qt 应用 → QSocketNotifier + signalfd
纯嵌入式裸 socket 事件循环 → signalfd + epoll 最佳
需跨平台移植 → sigaction
# 5.7 关键结论与速查表
核心结论五条:
- 信号处理器只做三件事:写
volatile sig_atomic_t旗标、调_exit()、调write()写 crash log。绝对不调printf/malloc/exit - SIGPIPE 是嵌入式网络程序的沉默杀手——
main第一行signal(SIGPIPE, SIG_IGN)是防御性标配 SA_ONSTACK是崩溃捕获的必选项——无备用栈时栈溢出 = 崩溃处理器自身也崩溃- 多线程程序用
pthread_sigmask屏蔽 + 专用线程sigwait——告别 async-signal-safe 限制 - 有 epoll 就用
signalfd——信号和 I/O 统一事件循环,linux 专有但极其优雅
async-signal-safe 函数速查表(信号处理器中可安全调用的函数):
| 类别 | 函数 |
|---|---|
| 退出 | _exit |
| I/O | write, read, open, close, fsync |
| 信号 | signal, sigaction, kill, raise, sigprocmask |
| 时间 | time, clock_gettime |
| 进程 | getpid, getppid, getuid, geteuid |
| 文件系统 | unlink, rename, mkdir, rmdir, stat, chmod |
| socket | socket, bind, connect, accept, send, recv |
| 管道 | pipe |
信号速查表:
| 信号 | 编号 | 默认动作 | 典型用途 | 可捕捉 |
|---|---|---|---|---|
SIGHUP | 1 | Term | 终端断开 / 配置重载 | ✅ |
SIGINT | 2 | Term | Ctrl+C | ✅ |
SIGQUIT | 3 | Core | Ctrl+\ | ✅ |
SIGILL | 4 | Core | 非法指令 | ✅ |
SIGABRT | 6 | Core | abort() | ✅ |
SIGBUS | 7 | Core | 总线错误(未对齐访问) | ✅ |
SIGFPE | 8 | Core | 算术异常(除零) | ✅ |
SIGKILL | 9 | Term | 强制杀死 | ❌ |
SIGSEGV | 11 | Core | 段错误 | ✅ |
SIGPIPE | 13 | Term | 写破裂管道/socket | ✅ |
SIGTERM | 15 | Term | 优雅退出 | ✅ |
SIGCHLD | 17 | Ign | 子进程状态改变 | ✅ |
SIGCONT | 18 | Cont | 恢复暂停进程 | ⚠️ 部分 |
SIGSTOP | 19 | Stop | 强制暂停 | ❌ |
SIGTSTP | 20 | Stop | Ctrl+Z | ✅ |
SIGUSR1 | 10 | Term | 用户定义 1 | ✅ |
SIGUSR2 | 12 | Term | 用户定义 2 | ✅ |
SIGWINCH | 28 | Ign | 终端窗口大小变化 | ✅ |
常见陷阱速查表:
| 陷阱 | 根因 | 正确做法 |
|---|---|---|
| 信号处理器中调 printf | printf 内部有锁 → 死锁 | 用 write() + 自建缓冲区 |
| 信号处理器中调 malloc | malloc 内部有锁 → 死锁 | 用预分配内存或栈变量 |
| SIGPIPE 杀死进程 | 对端关闭后第二次写触发 SIGPIPE | signal(SIGPIPE, SIG_IGN) |
SA_ONSTACK 未设 | 栈溢出信号处理器在已满栈执行 | 预先分配备用信号栈 |
SA_RESTART 过度依赖 | 不是所有调用都能被重启 | 永远检查 errno == EINTR |
| fork 后 SIGCHLD 僵尸 | 未 wait 子进程 | SIGCHLD 处理器 + WNOHANG |
多线程下用 sigprocmask | 行为未定义 | 用 pthread_sigmask |
下一步:在你的 Qt 应用中用 signalfd + QSocketNotifier 替换现有的 signal() 调用,实现信号与 Qt 事件循环的统一处理——然后验证 SIGTERM 下的配置保存是否 100% 可靠。
延伸阅读:
man 7 signal—— 信号概览man 2 sigaction—— 信号处理器注册man 2 signalfd—— Linux 信号 fd 接口man 7 signal-safety—— async-signal-safe 函数完整列表- 《The Linux Programming Interface》(Kerrisk) —— 第 20-22 章