Linux信号机制详解_POSIX语义与内核要点_sigaction与备用栈实践
异步通知、kill 与 sigaction 的语义边界,内核侧与进程描述符的耦合关系,网络场景下的 SIGPIPE,备用信号栈(sigaltstack)与 SA_ONSTACK ,以及标准信号与实时信号、EINTR/SA_RESTART 、多线程下的注意事项。可与仓库内 POSIX规范详解_可移植操作系统接口演进与跨平台实践、Linux_nohup命令详解_忽略SIGHUP与后台常驻 对照阅读。
目录
- [POSIX 中的信号是什么](#POSIX 中的信号是什么)
- 标准信号与实时信号(对照)
- 常见信号与默认行为速查
- [kill、SIGKILL 与不可中断睡眠(D)](#kill、SIGKILL 与不可中断睡眠(D))
- [进程状态 R/S/D/Z 与 /proc 粗判](#进程状态 R/S/D/Z 与 /proc 粗判)
- 信号生命周期(概念路径)
- 递送与返回用户态(序列直觉)
- [signal 与 sigaction](#signal 与 sigaction)
- [sigaction 常用标志位](#sigaction 常用标志位)
- [EINTR 与 SA_RESTART](#EINTR 与 SA_RESTART)
- 信号处理函数只能做什么
- [SIGPIPE 与网络服务端](#SIGPIPE 与网络服务端)
- [基于 epoll 的服务器:套接字、SIGPIPE 与 EINTR](#基于 epoll 的服务器:套接字、SIGPIPE 与 EINTR)
- [sigprocmask 与临界区](#sigprocmask 与临界区)
- 内核实现要点(为何不是独立「插件模块」)
- [备用信号栈 sigaltstack 与 SA_ONSTACK](#备用信号栈 sigaltstack 与 SA_ONSTACK)
- [多线程与信号(Linux 要点)](#多线程与信号(Linux 要点))
- 推荐工程模式:专用信号线程与主循环解耦
- [最小示例:sigaction 模板](#最小示例:sigaction 模板)
- 命令与排障速查
- 高频问答
- 常见踩坑汇总
- 免责声明
- 延伸阅读
POSIX 中的信号是什么
在 POSIX 语义下,信号(signal) 是一种异步 通知机制:内核或进程间通过约定编号通知目标进程某类事件已发生,目标进程可以按规范采取默认动作 、忽略 或安装处理函数(handler)。
| 维度 | 说明 |
|---|---|
| 规范来源 | POSIX.1 定义大量接口与语义:kill、sigaction、sigprocmask、pthread_kill(线程模型)等 |
| 实现载体 | Linux、BSD、macOS 等各自实现;接口相似,边界行为需对照手册 |
| 与 IPC | 信号属于 POSIX 规定的进程间通信与控制手段之一(与管道、套接字等并列抽象目的不同) |
实现侧 :Linux 在满足 POSIX 标准信号集的同时,还提供实时信号 (SIGRTMIN~SIGRTMAX)等扩展;排队、携带附加数据的语义与「普通信号」不同,详见下文对照表。
标准信号与实时信号(对照)
| 项目 | 标准信号(如 SIGTERM、SIGINT) |
实时信号(SIGRTMIN~SIGRTMAX) |
|---|---|---|
| 典型语义 | 终止、忽略、停止、core、自定义 handler | 常配合 sigqueue 携带 sigval,偏实时扩展用途 |
| 排队 | 同类标准信号多次产生可能被合并/丢弃(实现相关) | 通常支持排队(仍受资源限制) |
| 编号 | 多为固定小整数 | 区间内连续编号,具体范围运行时用 SIGRTMAX 等确认 |
| 使用建议 | 绝大多数业务与运维场景 | 除「带数据/排队」外,现代 Linux 服务里还常见于 低延迟线程间通知 ,并与 sigwaitinfo/sigtimedwait 、signalfd(2) 等配合,把信号收敛进 poll/epoll 事件循环(与传统「在 handler 里写业务」模型不同) |
具体队列深度、是否合并,以 signal(7) 与当前内核为准。
常见信号与默认行为速查
| 信号 | 典型场景 | 默认行为(常见) |
|---|---|---|
| SIGHUP | 控制终端挂断、守护进程重载配置 | 终止(常被应用自定义;见 nohup 文档) |
| SIGINT | 终端 Ctrl+C |
终止 |
| SIGQUIT | 终端 Ctrl+\ |
终止并常 core |
| SIGTERM | kill 默认(无 -9) |
终止(可捕获,常用于优雅退出) |
| SIGKILL | kill -9 |
终止 ,且不可捕获、不可忽略 |
| SIGSTOP / SIGTSTP | 作业控制暂停 | 停止 ;SIGSTOP 不可捕获、不可忽略 |
| SIGCONT | 继续执行被停止的进程 | 特殊:使停止进程继续 |
| SIGPIPE | 向已关闭的对端写 | 终止(网络服务常整体忽略后配合 EPIPE) |
| SIGCHLD | 子进程状态改变 | 依实现默认可能是忽略或终止;与僵尸、waitpid 强相关 |
| SIGSEGV / SIGBUS | 非法内存访问、总线错误等 | 通常终止并可能 core |
| SIGUSR1 / SIGUSR2 | 用户自定义语义 | 终止(可由程序定义为业务信号) |
编号与符号名以目标系统 signal(7) / kill -l 为准。
kill、SIGKILL 与不可中断睡眠(D)
kill -9对应SIGKILL:语义上不允许被捕获或忽略,用于强制结束可走通常终止路径的进程。- D 状态与「看起来杀不掉」 :目标线程处于
TASK_UNINTERRUPTIBLE(D 状态) 、卡在内核不可中断等待路径(典型如部分块设备 I/O)时,SIGKILL通常仍会被内核登记 (并非「对 D 状态完全无效」一类语义),但进程尚未进入可完成终止、回收与退出路径 ,因此从用户视角常表现为长时间不退出、似未杀死 。更严谨的理解是:终止请求已发生,但目标尚未走到能落实该请求的执行阶段;需等待底层条件变化或结合运维/内核侧手段(极端场景与驱动、块层状态相关,不能一概而论为一行命令解决)。 - 误读提醒 :不宜把上述现象概括成「
kill -9对 D 状态不起作用」------易让初学者误以为SIGKILL被内核丢弃 ;实际是递送/落实终止的时机被推迟。
区分三类常见混淆:
| 现象 | 含义(粗) | 与信号的关系 |
|---|---|---|
| D 状态 | 内核路径中不可中断睡眠 | SIGKILL 已登记 ,但进程可能迟迟走不完终止路径,故表现为「杀不掉」 |
| Z 僵尸 | 子进程已结束,父进程未 wait |
进程几乎不占资源;回收靠父进程或 init/reparent |
| T 停止 | SIGSTOP/Ctrl+Z 等 |
需 SIGCONT 或作业控制命令继续 |
进程状态 R/S/D/Z 与 /proc 粗判
text
+------------------+
| 调度器选中运行 |-----> R (running / runnable)
+----------------+
^
| 让出 CPU、可中断睡眠
v
+------------------+
| S 可中断睡眠 | 例:等 socket 可读(可被打断)
+------------------+
|
| 进入不可中断等待路径
v
+------------------+
| D 不可中断睡眠 | 例:部分块层路径(现象:SIGKILL 也可能长时间看不到进程退出)
+------------------+
在目标机器上可用 ps 或读 /proc/<pid>/stat 第三列(状态字母)做粗判;深入分析需结合 /proc/<pid>/stack、驱动与块设备日志。
信号生命周期(概念路径)
下图省略体系结构与具体调度细节,仅表达常见教材中的「递送时机」直觉。
递送
处理
默认动作
忽略
用户态 handler
挂起
pending / 队列(依信号类型)
产生
用户: kill/tkill
内核: 异常→SIGSEGV 等
终端/作业控制
屏蔽字 blocked 过滤
内核态→用户态路径上检查
要点:handler 在用户态执行(讨论普通进程模型时);内核负责在合适时机把执行流切到已注册的 handler。
递送与返回用户态(序列直觉)
内核 用户态进程 内核 用户态进程 某次系统调用/中断返回路径 alt [需要运行 handler 且未屏蔽] [默认动作终止] 标记 pending / 检查 blocked 构造用户态上下文,进入 handler 执行 handler(须异步信号安全) 返回到被打断的上下文(或按 SA_RESTART 重启 syscall) do_exit 等路径
该图仅为教学抽象;真实路径含线程、vfork、信号嵌套等分支。
signal 与 sigaction
| 接口 | 说明 |
|---|---|
signal() |
历史接口;早期各系统在「handler 执行后是否重置为默认」等行为上存在差异,可移植代码需谨慎。 |
sigaction() |
POSIX 明确语义;可配置 mask、flags(如 SA_RESTART 、SA_ONSTACK)。 |
工程上优先 sigaction() ;具体标志位行为以 sigaction(2) 与目标 libc 为准。
sigaction 常用标志位
下表为「查阅手册前」的速览,以 sigaction(2) 为准。
| 标志(节选) | 常见用途 |
|---|---|
SA_RESTART |
部分被信号中断的系统调用是否自动重启(见下节) |
SA_ONSTACK |
若已通过 sigaltstack() 注册备用栈,则在该栈上运行 handler |
SA_SIGINFO |
使用三参数形式 handler,可取得更多上下文(siginfo_t) |
SA_NOCLDSTOP |
对 SIGCHLD:子进程停止时不通知父进程 |
SA_NOCLDWAIT |
与 SIGCHLD/子进程回收语义相关,避免僵尸(语义细节见手册) |
EINTR 与 SA_RESTART
阻塞型系统调用被信号中断时,可能返回 -1 且 errno == EINTR 。是否自动重启取决于具体系统调用 、内核版本与是否设置 SA_RESTART。
| 场景 | 不含 SA_RESTART 的常见行为 |
备注 |
|---|---|---|
read/write 慢设备 |
常返回 EINTR,应用可重试 |
管道/终端与套接字细节见手册 |
pause |
被信号打断即返回 | 需按业务决定是否循环 |
connect(部分情况) |
可能 EINTR,是否可重启非小事,需查手册与实现 |
网络代码常见显式重试分支 |
accept / epoll_wait 等 |
即使设置了 SA_RESTART ,在许多实现中仍可能 EINTR 返回或不按「自动重启」直觉工作 |
必须以该调用的 man page 与实测为准 |
统一结论 :即使 为相关信号配置了
SA_RESTART,也不能 假定「所有阻塞调用都会自动重启」。connect、accept、epoll_wait等是否重启 ,历史上易因实现而异------编写epoll/网络服务器 时,仍应以EINTR显式重试/继续循环 为默认策略,并与对应man 2 ...对齐。
实践 :网络与存储路径往往手写 EINTR 重试 ;不要假设所有调用都会被 SA_RESTART 覆盖。
信号处理函数只能做什么
handler 中应视为「在中断上下文附近」:禁止调用依赖全局锁或不可重入状态的 libc 常规 API。
| 类别 | 典型安全 | 典型不安全 |
|---|---|---|
| 写入已打开 fd | write(2) 写固定字符串(仍要注意重入与缓冲区) |
fprintf、printf |
| 内存分配 | --- | malloc、free、大多数 new/delete |
| 同步原语 | sem_post(在允许列表中)等少数 |
pthread_mutex_lock(若与持锁路径交错可能死锁) |
printf 死锁直觉(简化):
信号 handler stdio 锁 主流程 信号 handler stdio 锁 主流程 死锁风险(示意) 进入 printf,已持锁 信号到达 handler 内再次 printf,等待同一把锁
常见安全写法:
- 仅修改
volatile sig_atomic_t标志位; - 由主循环或统一线程在安全上下文中做清理;
- 若必须输出:使用
write()写已经存在的固定缓冲区(仍需谨慎长度与重入)。
完整「异步信号安全」函数列表见 signal-safety(7)(Linux)。
SIGPIPE 与网络服务端
向已关闭连接的一端继续写,可能触发 SIGPIPE,默认动作常为终止进程。
write/send 已关闭的对端
内核返回 EPIPE / 并可能递送 SIGPIPE
默认:进程被 SIGPIPE 终止
服务器常:SIG_IGN + 业务处理 EPIPE
常见策略:
c
signal(SIGPIPE, SIG_IGN); /* 或以 sigaction 忽略 */
随后在业务路径检查 write/send 返回值 ,对 EPIPE 做断链处理;仅以忽略信号代替错误处理仍不充分。
基于 epoll 的服务器:套接字、SIGPIPE 与 EINTR
与「阻塞在单套接字上的简易服务器」不同,epoll_wait + 非阻塞 fd 的高并发服务里,信号与 I/O 仍交叉出现在三条线:默认终止类信号 、SIGPIPE/EPIPE 写路径 、被信号打断的系统调用返回 EINTR。
| 主题 | 推荐结论(工程默认) |
|---|---|
SIGPIPE |
进程级 SIG_IGN (或等效 sigaction),避免默认动作直接把进程打死 |
EPIPE/ECONNRESET 等 |
仅在 write/send/read 路径 根据返回值与 errno 关闭连接、摘 fd;不要依赖「忽略信号」代替业务状态机 |
EINTR |
对 epoll_wait/accept/read/write 等:循环或封装重试 ,直到非 EINTR 或明确退出条件;勿假定 SA_RESTART 全覆盖(见上节) |
| handler 与套接字 | 不在信号 handler 内 read/write/close 业务套接字;I/O 留在主事件循环或统一线程模型中处理 |
与「Socket 编程」系列文章的关系:本文只固定信号与多路复用并发模型相交时的结论 ;连接建立、半关闭、SO_NOSIGPIPE(部分平台)等仍属套接字专题,可另文展开并在该文中互链本文 SIGPIPE/EINTR 两节。
sigprocmask 与临界区
sigprocmask()修改当前线程的信号掩码:屏蔽期间信号可被延迟,解除屏蔽后再递送。- 标准信号 与实时信号 在「合并/排队」上语义不同,编写「绝不丢失」逻辑时要用对的 API 与信号类型(见
signal(7))。
屏蔽区间
sigprocmask 阻塞某集合
临界区:修改共享不变式
解除屏蔽 → 之前挂起的信号可能此时递送
用于保护短暂临界区:尽量缩短屏蔽区间;避免在屏蔽区间内调用可能阻塞或再次引发信号复杂性的代码。
内核实现要点(为何不是独立「插件模块」)
从实现视角,Linux 的信号逻辑分散在内核多处 ,而不是类似可随意卸载的单文件「signal.ko」:
| 层次 | 角色(概念) |
|---|---|
| 通用逻辑 | kernel/signal.c:发送、挂起、投递判断等(路径随版本可能调整) |
| 进程模型 | task_struct:pending、blocked、sighand 等字段 |
| 生命周期 | kernel/exit.c 等与终止、SIGKILL 路径交织 |
| 体系结构 | arch/.../signal*.c:用户态栈帧、返回路径 |
text
用户态 handler
^
| 构造/切换上下文
+------+------+
| arch/* |
| signal*.c |
+------+------+
^
|
+------+------+
| kernel/ |
| signal.c |<---- pending / mask / deliver 决策
+------+------+
^
|
+------+------+
| sched / exit |
| 进程状态机 |
+--------------+
因而抽象上称为「信号子系统」即可,源码层面是横切关注点,不是单一独立外围模块。
备用信号栈 sigaltstack 与 SA_ONSTACK
当担心主用户栈空间紧张 (深度递归、大栈帧、在崩溃边缘仍希望运行极简 handler),可使用 备用信号栈:
- 用
sigaltstack()注册一块独立内存区域(大小 often ≥SIGSTKSZ,下限参考MINSIGSTKSZ)。 - 用
sigaction()注册 handler,并设置SA_ONSTACK。
text
高地址
| +----------------------+
| | 主栈(常规用户栈) |
| | ... |
v +----------------------+
| 备用栈 sigaltstack | <-- SA_ONSTACK 时 handler 在此运行
+----------------------+
低地址 (地址方向示意,实际取决于 ABI/映射)
注意:
- handler 仍需遵守异步信号安全约束;备用栈不是「可以在 handler 里随意 printf」的许可证。
- 栈溢出属 UB 范畴:能否在溢出后安全进入 handler 依赖具体崩溃形态;备用栈用于提高「在独立内存上执行极简收尾」的概率,不是万能保险。
- 多线程 :谁接收异步信号、在哪个线程上运行 handler,模型复杂;常见工程策略是专用信号线程 + 统一屏蔽/处理,避免 worker 线程随机执行 handler。
多线程与信号(Linux 要点)
| 要点 | 说明 |
|---|---|
| 投递目标 | 异步信号面向进程传统模型;多线程下「哪条线程运行 handler」受实现与掩码影响 |
| 掩码粒度 | sigprocmask 作用于调用线程 ;pthread_sigmask 与之等价类别(查阅 pthread 文档) |
| 定向发送 | pthread_kill 可向指定线程发送信号 |
| 工程策略 | 屏蔽绝大多数信号→单独线程 sigwait/管道唤醒业务线程,避免在随机线程跑复杂 handler |
推荐工程模式:专用信号线程与主循环解耦
在不把业务逻辑写进 handler 的前提下,常见「可照抄骨架」是:工作线程几乎屏蔽异步信号 ;单独一条线程 用 sigwait/sigwaitinfo 或 signalfd + poll/epoll 统一收敛信号;再经 pipe / eventfd 把「有信号待处理」通知给主 epoll 循环。handler(若仍注册)里只做 往 pipe/eventfd write 一个计数,其余逻辑回到主循环。
text
[异步信号] --> [内核递送]
|
+--------+---------+
| 专用信号线程 |
| sigwait / |
| signalfd+poll |
+--------+---------+
| write(非阻塞)
v
[ pipe / eventfd ]
|
v
[ 主线程 epoll 循环 ] --> accept/read/write 业务
主循环
信号线程
内核
信号
sigwaitinfo / signalfd
write → pipe 或 eventfd
epoll_wait + 业务 fd
要点 :主逻辑线程内不要 依赖「哪个线程会运行异步 handler」;与 pthread_sigmask 、sigfillset/sigwait 的精确配合以 pthread_sigmask(3) 、sigwait(3) 为准。
最小示例:sigaction 模板
下列仅为说明结构;标志位与重试逻辑按业务裁剪。
c
#include <signal.h>
#include <unistd.h>
#include <string.h>
static volatile sig_atomic_t got_term;
static void on_term(int signo) {
(void)signo;
got_term = 1; /* 只做原子标志 */
}
static int setup_handlers(void) {
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = on_term;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
if (sigaction(SIGTERM, &sa, NULL) != 0) return -1;
if (sigaction(SIGINT, &sa, NULL) != 0) return -1;
return 0;
}
static void main_loop(void) {
/* pause() 会被任意「未屏蔽且会唤醒 pause」的信号打断;若日后增加 SIGHUP 等,
* 需纳入 sigprocmask 规划,或在此处对 got_term 与其它标志分支处理。 */
while (!got_term) {
pause(); /* 演示:真实服务应替换为 ppoll/epoll_wait 等 */
}
{
const char msg[] = "exiting...\n";
(void)write(STDOUT_FILENO, msg, sizeof(msg) - 1);
}
}
int main(void) {
if (setup_handlers() != 0) return 1;
main_loop();
return 0;
}
命令与排障速查
| 目的 | 命令(示例) |
|---|---|
| 列出信号名 | kill -l |
| 温和终止 | kill -TERM <pid> |
| 强杀 | kill -KILL <pid> |
| 按名杀进程 | pkill -TERM <pattern>(慎用) |
| 跟踪信号相关系统调用 | strace -e trace='signal' -p <pid> |
| 查看线程 /proc | ls /proc/<pid>/task |
高频问答
kill -9 一定立刻生效吗?
不一定:若线程长时间处于 D 状态 等不可中断阻塞,SIGKILL 往往已登记 但进程迟迟走不完终止路径,故表现为长时间不退出;另有僵尸态、命名空间/cgroup 限制等运维侧因素。
为什么 handler 里不能用 malloc?
可能与 malloc 内部锁或其它不可重入状态交错,形成死锁或堆损坏。
父进程如何感知子进程退出?
可用 SIGCHLD + wait/waitpid 回收;忽略僵尸问题需完整读取子进程退出状态。
POSIX 规定了哪些接口?
见 kill、sigaction、sigprocmask、sigpending、sigsuspend 等;语义以 Open Group 单行标准为权威表述。
实时信号值得用吗?
若需要排队、附带 sigval 、或与 sigwaitinfo/signalfd 把信号并入 epoll ,实时信号是常见选项;务必阅读 sigqueue(3) 、signal(7) 与 signalfd(2) 中的资源与语义边界。
常见踩坑汇总
| 错误做法 | 典型后果 |
|---|---|
在 handler 里 printf/malloc/复杂逻辑 |
与主流程锁交错 → 死锁、堆损坏或未定义行为 |
假定 SA_RESTART 覆盖所有阻塞调用 (含 epoll_wait/connect 等) |
隐蔽 EINTR bug,高负载才偶发 |
| 多线程程序随意注册异步 handler、不设掩码 | 不确定哪条线程执行 handler,难以推理 |
仅 SIGPIPE 忽略 却不检查 write 返回与 EPIPE |
连接状态错误、半关闭处理遗漏 |
新代码仍用 signal() 作为主 API |
历史语义差异 → 可移植性与竞态问题 |
在 handler 内 read/write/close 业务套接字 |
与主 epoll 循环竞态,易崩溃或漏事件 |
免责声明
- 内核数据结构路径、实时信号排队策略随版本演进;生产排障以目标内核版本源码与手册为准。
- 备用栈与栈溢出场景的边界属于底层行为;示例代码需在隔离环境验证。
延伸阅读
- The Open Group Base Specifications --- signal.h
- Linux
signal(7)、sigaction(2)、signal-safety(7)、sigaltstack(2)、sigqueue(2)、sigwait(3)、signalfd(2)、pthread_sigmask(3)、epoll_wait(2) - Linux
proc(5)中进程状态字段说明(理解D状态现象) - 仓库内:POSIX规范详解_可移植操作系统接口演进与跨平台实践