Linux信号机制详解 POSIX语义与内核要点 sigaction与备用栈实践

Linux信号机制详解_POSIX语义与内核要点_sigaction与备用栈实践

异步通知、killsigaction 的语义边界,内核侧与进程描述符的耦合关系,网络场景下的 SIGPIPE,备用信号栈(sigaltstack)与 SA_ONSTACK ,以及标准信号与实时信号、EINTR/SA_RESTART 、多线程下的注意事项。可与仓库内 POSIX规范详解_可移植操作系统接口演进与跨平台实践Linux_nohup命令详解_忽略SIGHUP与后台常驻 对照阅读。

目录

  1. [POSIX 中的信号是什么](#POSIX 中的信号是什么)
  2. 标准信号与实时信号(对照)
  3. 常见信号与默认行为速查
  4. [kill、SIGKILL 与不可中断睡眠(D)](#kill、SIGKILL 与不可中断睡眠(D))
  5. [进程状态 R/S/D/Z 与 /proc 粗判](#进程状态 R/S/D/Z 与 /proc 粗判)
  6. 信号生命周期(概念路径)
  7. 递送与返回用户态(序列直觉)
  8. [signal 与 sigaction](#signal 与 sigaction)
  9. [sigaction 常用标志位](#sigaction 常用标志位)
  10. [EINTR 与 SA_RESTART](#EINTR 与 SA_RESTART)
  11. 信号处理函数只能做什么
  12. [SIGPIPE 与网络服务端](#SIGPIPE 与网络服务端)
  13. [基于 epoll 的服务器:套接字、SIGPIPE 与 EINTR](#基于 epoll 的服务器:套接字、SIGPIPE 与 EINTR)
  14. [sigprocmask 与临界区](#sigprocmask 与临界区)
  15. 内核实现要点(为何不是独立「插件模块」)
  16. [备用信号栈 sigaltstack 与 SA_ONSTACK](#备用信号栈 sigaltstack 与 SA_ONSTACK)
  17. [多线程与信号(Linux 要点)](#多线程与信号(Linux 要点))
  18. 推荐工程模式:专用信号线程与主循环解耦
  19. [最小示例:sigaction 模板](#最小示例:sigaction 模板)
  20. 命令与排障速查
  21. 高频问答
  22. 常见踩坑汇总
  23. 免责声明
  24. 延伸阅读

POSIX 中的信号是什么

在 POSIX 语义下,信号(signal) 是一种异步 通知机制:内核或进程间通过约定编号通知目标进程某类事件已发生,目标进程可以按规范采取默认动作忽略安装处理函数(handler)

维度 说明
规范来源 POSIX.1 定义大量接口与语义:killsigactionsigprocmaskpthread_kill(线程模型)等
实现载体 Linux、BSD、macOS 等各自实现;接口相似,边界行为需对照手册
与 IPC 信号属于 POSIX 规定的进程间通信与控制手段之一(与管道、套接字等并列抽象目的不同)

实现侧 :Linux 在满足 POSIX 标准信号集的同时,还提供实时信号SIGRTMINSIGRTMAX)等扩展;排队、携带附加数据的语义与「普通信号」不同,详见下文对照表。


标准信号与实时信号(对照)

项目 标准信号(如 SIGTERMSIGINT 实时信号(SIGRTMINSIGRTMAX
典型语义 终止、忽略、停止、core、自定义 handler 常配合 sigqueue 携带 sigval,偏实时扩展用途
排队 同类标准信号多次产生可能被合并/丢弃(实现相关) 通常支持排队(仍受资源限制)
编号 多为固定小整数 区间内连续编号,具体范围运行时用 SIGRTMAX 等确认
使用建议 绝大多数业务与运维场景 除「带数据/排队」外,现代 Linux 服务里还常见于 低延迟线程间通知 ,并与 sigwaitinfo/sigtimedwaitsignalfd(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_RESTARTSA_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

阻塞型系统调用被信号中断时,可能返回 -1errno == EINTR 。是否自动重启取决于具体系统调用 、内核版本与是否设置 SA_RESTART

场景 不含 SA_RESTART 的常见行为 备注
read/write 慢设备 常返回 EINTR,应用可重试 管道/终端与套接字细节见手册
pause 被信号打断即返回 需按业务决定是否循环
connect(部分情况) 可能 EINTR,是否可重启非小事,需查手册与实现 网络代码常见显式重试分支
accept / epoll_wait 即使设置了 SA_RESTART ,在许多实现中仍可能 EINTR 返回或不按「自动重启」直觉工作 必须以该调用的 man page 与实测为准

统一结论即使 为相关信号配置了 SA_RESTART ,也不能 假定「所有阻塞调用都会自动重启」。connectacceptepoll_wait 等是否重启 ,历史上易因实现而异------编写 epoll/网络服务器 时,仍应以 EINTR 显式重试/继续循环 为默认策略,并与对应 man 2 ... 对齐。

实践 :网络与存储路径往往手写 EINTR 重试 ;不要假设所有调用都会被 SA_RESTART 覆盖。


信号处理函数只能做什么

handler 中应视为「在中断上下文附近」:禁止调用依赖全局锁或不可重入状态的 libc 常规 API。

类别 典型安全 典型不安全
写入已打开 fd write(2) 写固定字符串(仍要注意重入与缓冲区) fprintfprintf
内存分配 --- mallocfree、大多数 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),可使用 备用信号栈

  1. sigaltstack() 注册一块独立内存区域(大小 often ≥ SIGSTKSZ ,下限参考 MINSIGSTKSZ)。
  2. 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/sigwaitinfosignalfd + 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_sigmasksigfillset/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 规定了哪些接口?

killsigactionsigprocmasksigpendingsigsuspend 等;语义以 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 循环竞态,易崩溃或漏事件

免责声明

  • 内核数据结构路径、实时信号排队策略随版本演进;生产排障以目标内核版本源码与手册为准。
  • 备用栈与栈溢出场景的边界属于底层行为;示例代码需在隔离环境验证。

延伸阅读

相关推荐
cui_ruicheng2 小时前
Linux进程间通信(三):System V IPC与共享内存
linux·运维·服务器
蚰蜒螟2 小时前
深入 Linux 内核同步机制:从 futex 到 spinlock 的完整旅程
linux·windows·microsoft
运维全栈笔记2 小时前
Linux安装配置Tomcat保姆级教程:从部署到性能调优
linux·服务器·中间件·tomcat·apache·web
REDcker3 小时前
浏览器端Web程序性能分析与优化实战 DevTools指标与工程清单
开发语言·前端·javascript·vue·ecmascript·php·js
dllmayday3 小时前
Linux 上用终端连接 WiFi
linux·服务器·windows
ACP广源盛139246256733 小时前
IX8024与科学大模型的碰撞@ACP#筑牢科研 AI 算力高速枢纽分享
运维·服务器·网络·数据库·人工智能·嵌入式硬件·电脑
峥无5 小时前
Linux系统编程基石:静态库·动态库·ELF文件·进程地址空间全景图
linux·运维·服务器
用户2367829801685 小时前
从 chmod 755 说起:Unix 文件权限到底是怎么算的?
linux
码云数智-大飞5 小时前
本地部署大模型:隐私安全与多元优势一站式解读
运维·网络·人工智能