Linux 信号机制

Linux 信号机制

一、 概述

在 Linux/Unix 系统中,信号(Signal)是最古老、最基础的进程间通信(IPC)机制之一。它本质上是一种异步事件通知机制

  • 异步性:信号的产生与进程的正常执行流程是并行的,进程无法预知信号何时到达。
  • 轻量级:每个信号仅用一个整数标识,不携带复杂的数据结构。
  • 核心作用:用于通知进程某个特定事件已经发生(如异常、终止、中断等),协调系统的正常运行。

二、 信号的分类与常见信号

Linux 系统定义了多达 64 种信号,主要分为两大类:

1. 标准信号(1-31号)

传统 UNIX 信号,也称为"不可靠信号"。它们没有优先级,不支持排队,多次发送相同信号可能会被合并(只保留一个)。

  • SIGINT (2) :键盘中断(Ctrl+C),温和地请求终止进程。
  • SIGQUIT (3) :键盘退出(Ctrl+\),强烈请求终止并生成 Core Dump。
  • SIGKILL (9) :强制终止进程。不可捕获、不可忽略
  • SIGSEGV (11):段错误(非法内存访问/野指针),通常由硬件异常触发。
  • SIGCHLD (17):子进程状态改变(退出或停止)时通知父进程。
  • SIGSTOP (19) :强制停止进程。不可捕获、不可忽略

2. 实时信号(34-64号)

POSIX 标准引入的"可靠信号"(范围从 SIGRTMINSIGRTMAX)。

  • 特点 :支持排队、可携带数据(通过 sigqueue 发送),且严格按照发送顺序递达,不会丢失。适用于高实时性 IPC 场景。

三、 信号的生命周期

信号从产生到处理,经历三个核心阶段:

  1. 产生(Generation):当事件发生时,内核为目标进程生成信号,并将其添加到进程的**未决信号集(Pending Signal Set)**中。
  2. 阻塞/屏蔽(Blocking/Masking) :进程可以通过设置**信号屏蔽字(Signal Mask)**来阻塞某些信号。被阻塞的信号会保持未决状态,直到解除阻塞。注意:阻塞不等于忽略,忽略是直接丢弃,而阻塞是暂存排队。
  3. 递达(Delivery):当进程从内核态返回用户态时,内核检查未决信号集。如果存在未被阻塞的信号,则执行相应的处理动作,并将其从 Pending 集合中移除。

四、 信号的处理方式

当信号递达时,进程可以选择以下三种处理方式:

  1. 执行默认动作(SIG_DFL):由内核决定,如终止进程、停止进程或生成 Core Dump。
  2. 忽略信号(SIG_IGN):当它没发生过,直接丢弃。
  3. 捕获信号(Custom Handler):注册自定义处理函数,由内核在信号递达时调用。

五、 信号捕捉的底层机制:三次穿越

信号处理函数(Handler)不是被 main 函数调用的,而是通过内核劫持用户态执行流来实现的。这是一个涉及用户态与内核态多次切换的复杂过程:

  1. 陷入内核 :进程正在 main 函数中执行,发生中断或异常切换到内核态。
  2. 检查与决策 :中断处理完毕返回用户态前,内核检查发现有信号递达。内核决定不恢复 main 的上下文,而是修改指令指针(EIP/RIP)指向 sighandler
  3. 执行 Handler :CPU 切换回用户态,执行 sighandler。此时 sighandlermain 使用不同的堆栈空间,是独立控制流程。
  4. sigreturnsighandler 返回后,自动执行特殊的系统调用 sigreturn 再次进入内核态。
  5. 恢复现场 :内核恢复 main 函数的上下文,再次返回用户态继续执行。

六、 核心 API:sigaction 与自动屏蔽

相比于老旧的 signal() 函数,sigaction 提供了更精细、更安全的控制能力。

自动屏蔽机制(防重入保护)

当某个信号的处理函数被调用时,内核会自动将当前信号加入进程的信号屏蔽字。当处理函数返回时,自动恢复原来的屏蔽字。

  • 作用:防止同一个信号在处理过程中再次打断自己(避免递归调用导致栈溢出)。
  • 额外屏蔽 :通过 sa_mask 字段,还可以在处理当前信号时,顺便屏蔽其他指定的信号。

七、 可重入函数与并发安全

由于 Handler 是在用户空间异步执行的,如果处理不当,极易引发灾难。

什么是可重入(Reentrant)?

一个函数在被执行的过程中被中断,并在处理完中断后再次进入该函数,依然能够正确运行,不破坏任何共享数据。

为什么 Handler 里不能随便调 printf/malloc?

  • 不可重入的危险 :像 malloc() 内部维护着全局的空闲内存链表,printf() 内部有全局缓冲区。如果主程序正在修改这些全局状态时被 Handler 打断,且 Handler 也调用了这些函数,会导致数据结构被破坏、内存泄漏甚至段错误。
  • 最佳实践 :在 Handler 中,只设置一个全局的 volatile sig_atomic_t 类型的标志位。把复杂的业务逻辑留给主循环去处理。

八、 volatile 关键字的底层意义

在信号处理中,标志变量必须用 volatile 修饰。

  • 编译器的盲区 :编译器在分析 main 函数的控制流时,是"线性"的。它看不见未来某个时刻内核会通过中断强行插入执行 handler
  • 防止致命优化 :如果不加 volatile,编译器可能会将 while(!flag) 优化为只读取一次 flag 放入寄存器,导致即使 Handler 修改了内存中的 flag,主循环也永远看不到,陷入死循环。volatile 强制编译器每次都从内存中读取该变量。

九、 SIGCHLD 与僵尸进程防范

只要编写服务端程序,就必须处理子进程退出产生的僵尸进程。现代编程中有两个最佳实践:

  1. 不关心退出状态 :在 fork() 前执行 signal(SIGCHLD, SIG_IGN);,内核会自动回收子进程。
  2. 需要监控状态 :注册 Handler,内部必须使用 while(waitpid(-1, NULL, WNOHANG) > 0);。因为多个子进程同时退出时,信号可能会合并丢失,必须用循环一次性清理干净。

十、 中断、信号与系统调用

信号机制并非孤立存在,它是整个操作系统"事件驱动"架构的一环:

  • 硬件中断:外设主动敲门(如键盘按下),触发 CPU 暂停当前工作,执行内核驱动。
  • 软件信号:内核在进程从内核态返回用户态时,强行劫持执行流,让进程去跑 Handler。
  • 系统调用 :用户程序主动触发(通过 syscall 指令和寄存器传参),请求内核办事,办完直接返回。

这三者共同构成了计算机系统底层通信与调度的基石。


文章里提到的进程间通信(IPC)还没展开,要不要我接着帮你整理一份?

相关推荐
正点原子2 小时前
【正点原子Linux连载】 第十章 pinctrl和gpio子系统实验 摘自【正点原子】ATK-DLRK3568嵌入式Linux驱动开发指南
linux·运维·驱动开发
气泡水。3 小时前
RHCSA笔记实记
linux·笔记
波特率1152003 小时前
linux当中的六大进程间通信方式
linux·嵌入式·ipc
冠希陈、3 小时前
Nginx部署SSL,导致多站点串站问题
运维·nginx·ssl
RisunJan3 小时前
Linux命令-ssh-keyscan(从主机收集公钥)
linux·ssh
こ进制掌控者3 小时前
局域网电脑SMB访问NAS,提示用户名或密码不正确。
运维·服务器
mit6.8244 小时前
devops
运维·flask·devops
向上的车轮4 小时前
GitHub Actions 自动化运维实战:Rust 全栈项目 CI/CD 至华为云
运维·自动化·github
祈禾4 小时前
Redis三大特殊数据类型
运维·服务器·数据库·redis·笔记·缓存