【Linux信号】Linux进程信号

🎬 个人主页艾莉丝努力练剑
专栏传送门 :《C语言》《数据结构与算法》《C/C++干货分享&学习过程记录
Linux操作系统编程详解》《笔试/面试常见算法:从基础到进阶》《Python干货分享

⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平


🎬 艾莉丝的简介:


文章目录

  • 前言
  • [1 ~> 信号的"生活化"初识](#1 ~> 信号的“生活化”初识)
  • [2 ~> 信号的产生方式:它是怎么来的?](#2 ~> 信号的产生方式:它是怎么来的?)
    • [2.1 终端按键产生](#2.1 终端按键产生)
    • [2.2 系统调用产生](#2.2 系统调用产生)
    • [2.3 软件条件产生](#2.3 软件条件产生)
    • [2.4 硬件异常产生](#2.4 硬件异常产生)
  • [3 ~> 核心原理:信号是如何被"记住"的?](#3 ~> 核心原理:信号是如何被“记住”的?)
  • [4 ~> 信号处理的"黄金时机":内核态切换](#4 ~> 信号处理的“黄金时机”:内核态切换)
    • [4.1 两种状态的区别](#4.1 两种状态的区别)
    • [4.2 处理流程("倒 8 字"模型)](#4.2 处理流程(“倒 8 字”模型))
  • [5 ~> 实战演练:捕捉与屏蔽](#5 ~> 实战演练:捕捉与屏蔽)
    • [5.1 实验:让 Ctrl + C 失效](#5.1 实验:让 Ctrl + C 失效)
    • [5.2 信号集操作:封印信号](#5.2 信号集操作:封印信号)
  • [6 ~> 编写安全代码的"三大准则"](#6 ~> 编写安全代码的“三大准则”)
    • [6.1 可重入函数](#6.1 可重入函数)
    • [6.2 volatile 关键字](#6.2 volatile 关键字)
    • [6.3 优雅回收子进程:SIGCHLD](#6.3 优雅回收子进程:SIGCHLD)
  • [7 ~> 遇到的问题与解决思路](#7 ~> 遇到的问题与解决思路)
  • [8 ~> 总结与延伸](#8 ~> 总结与延伸)
  • 结尾

前言

在 Linux 的多任务世界里,进程并不是孤立运行的。它们需要一种机制来应对突发事件------比如用户按下了 Ctrl + C,或者程序不小心除以了 0。这种"异步通信"的机制就是信号(Signal

最近通过对 Linux 进程信号的学习,我发现它不仅是系统编程的基石,更像是一套完整的"紧急应变系统"。今天这篇博客,我将带大家从信号的产生、保存、处理到内核底层原理进行全方位的深度拆解。


1 ~> 信号的"生活化"初识

为了直观理解信号,我们可以对比一个生活场景:

你在网上买了快递,在快递寄达前,你其实已经知道该怎么处理它了(拆开、送人或扔掉)。快递员给你打电话(信号到来)是异步的,你不知道他什么时候会打。电话响时,你可能正在忙(比如打游戏),无法立即去取,但你会"记住"这件事,等手头忙完(合适的时间)再去处理。

核心结论:

  • 识别信号:进程识别信号是内核预设的特性。

  • 处理预设:信号还没产生,进程就已经知道对应的处理方法了。

  • 异步性:信号可能在进程执行的任何时刻到来。


2 ~> 信号的产生方式:它是怎么来的?

信号不会凭空产生,通常有以下四种渠道:

2.1 终端按键产生

这是我们最常用的方式:

Ctrl + C:对应 2) SIGINT(中断信号),默认动作是终止前台进程。

Ctrl + \:对应 3) SIGQUIT (退出信号),默认动作是终止并产生 Core Dump 文件(核心转储,用于事后调试)。

Ctrl + Z:对应 20) SIGTSTP(停止信号),将进程挂起。

2.2 系统调用产生

  • kill 命令 / 函数:可以向指定 PID 发送任意信号。

  • raise 函数:进程"自残",给自己发信号。

  • abort 函数:发送 6 ) SIGABRT 使进程异常终止。

2.3 软件条件产生

  • 闹钟机制 :使用 alarm 函数。当时间到达,内核向进程发送 14 ) SIGALRM

  • 管道破裂 :读端关闭的管道继续写入时,产生 13 ) SIGPIPE

2.4 硬件异常产生

  • 除 0 错误 :CPU 运算单元出错,内核发送 8) SIGFPE。

  • 野指针 / 越界 :MMU 内存管理单元出错,内核发送 11) SIGSEGV。


3 ~> 核心原理:信号是如何被"记住"的?

信号的产生是异步的,进程可能正在处理更重要的事情。那么信号在被递达前存在哪呢?

在内核的 task_struct(进程控制块) 中,维护了三张至关重要的表:

  • Pending 位图:记录收到了哪些信号。每一位代表一个信号,1 表示信号已到达但未处理。

  • Block 位图:记录哪些信号被阻塞。如果某位为 1,说明即使收到该信号,进程也会暂时"封印"它,不予递达。

  • Handler 表:这是一个函数指针数组,记录了每个信号对应的处理动作。

信号的状态术语:

  • 递达 (Delivery):实际执行信号处理动作。

  • 未决 (Pending):从信号产生到递达之间的状态。

  • 阻塞 (Block):进程可以选择阻塞某个信号。一旦被阻塞,信号将保持在未决状态,直到解除阻塞。


4 ~> 信号处理的"黄金时机":内核态切换

这是最硬核的部分:信号到底是什么时候被处理的?

结论:当进程从内核态(Kernel Mode)返回到用户态(User Mode)的时候。

4.1 两种状态的区别

  • 用户态:执行用户代码,权限受限。

  • 内核态:执行 OS 代码,拥有最高权限。通过系统调用、硬件中断或异常切换。

4.2 处理流程("倒 8 字"模型)

  • 陷入内核:因为系统调用(如 read)或时间片到了,进入内核态。

  • 信号检测:在准备返回用户态前,检查 pending 位图。

  • 处理信号:如果是自定义捕捉,OS 会切换回用户态执行 handler。执行完后,通过特殊的系统调用再次进入内核。

  • 返回原点:最后才返回用户态的主控制流继续执行。


5 ~> 实战演练:捕捉与屏蔽

5.1 实验:让 Ctrl + C 失效

我们可以通过 signal() 或更强大的 sigaction() 来拦截信号。

cpp 复制代码
#include <iostream>
#include <signal.h>
#include <unistd.h>

void handler(int signum) {
    std::cout << "捕捉到信号: " << signum << ",嘿嘿,杀不死我!" << std::endl;
}

int main() {
    // 注册信号捕捉
    signal(SIGINT, handler); 
    
    while(true) {
        std::cout << "进程运行中 [PID:" << getpid() << "]..." << std::endl;
        sleep(1);
    }
    return 0;
}

5.2 信号集操作:封印信号

如果我想在一段敏感代码执行期间屏蔽所有信号,可以使用信号集操作:

cpp 复制代码
sigset_t set, oset;
sigemptyset(&set);
sigaddset(&set, SIGINT); // 准备屏蔽 2 号信号

// 1. 设置阻塞位图
sigprocmask(SIG_BLOCK, &set, &oset); 

// 2. 执行关键任务...

// 3. 恢复原始屏蔽状态
sigprocmask(SIG_SETMASK, &oset, NULL);

6 ~> 编写安全代码的"三大准则"

在信号处理中,有些坑如果不踩一次真的很难记住:

  • 可重入函数
  • volatile
  • SIGCHLD信号

6.1 可重入函数

问题:如果你的主控制流正在 insert 链表,信号突然来了,handler 里也调用了 insert,链表结构就会被破坏。

准则:在信号处理函数里,避免调用不可重入函数(如 mallocprintf,标准 IO 库等)。

6.2 volatile 关键字

在优化级别较高时,编译器可能为了效率把一个全局变量放在寄存器里。

现象:handler 改了内存里的变量,但主循环还在看寄存器旧值,导致死循环。

对策:使用 volatile 修饰变量,强制要求编译器每次从内存读取。

6.3 优雅回收子进程:SIGCHLD

以前我们需要循环 waitpid 来回收子进程。

黑科技:子进程退出时会发 SIGCHLD 给父进程。如果我们不关心退出状态,直接执行 signal(SIGCHLD, SIG_IGN);。在 Linux 下,这能让子进程退出后自动释放,彻底告别僵尸进程!


7 ~> 遇到的问题与解决思路

Q:为什么捕获了"除 0 异常"后,程序会陷入死循环打印?
排查: 除 0 异常由 CPU 寄存器状态触发。当从内核态返回执行 handler 后,CPU 里的异常标志位并没有被清除。返回后 CPU 再次检测到异常,再次发信号。
解决: 硬件异常信号通常不建议只做简单的捕获,应该在 handler 中直接 exit() 或由系统默认动作终止。


8 ~> 总结与延伸

通过本次学习,我深刻体会到信号是 Linux 异步处理机制的灵魂:

  • 产生:来源多样(按键、软件、硬件)。

  • 保存:通过 task_struct 中的位图管理。

  • 处理:利用内核态切换的时机进行递达。

  • 安全:注意可重入性与 volatile 可见性。


结尾

uu们,本文的内容到这里就全部结束了,艾莉丝在这里再次感谢您的阅读!

结语:希望对学习Linux相关内容的uu有所帮助,不要忘记给博主"一键四连"哦!

往期回顾

【Linux:文件】文件基础IO进阶

🗡博主在这里放了一只小狗,大家看完了摸摸小狗放松一下吧!🗡 ૮₍ ˶ ˊ ᴥ ˋ˶₎ა

相关推荐
cqsztech2 小时前
基于ORACLE LINUX 10.1 MYSQL 8.4 源码安装
linux·mysql·oracle
齐齐大魔王2 小时前
linux-系统函数
linux·运维·microsoft
m0_564876842 小时前
微调学习。
学习
Lugas Luo2 小时前
利用 Claude 辅助 Linux 嵌入式开发的高阶工作流Top Steps
linux
学工科的皮皮志^_^2 小时前
RS485学习
经验分享·笔记·单片机·嵌入式硬件·学习
lisw052 小时前
生成式学习:AI时代的学习新范式!
人工智能·学习·机器学习
这辈子谁会真的心疼你2 小时前
怎么修改视频的拍摄信息?详细的修改过程
java·服务器·音视频
C^h2 小时前
RT thread中断管理学习记录
单片机·嵌入式硬件·学习
the sun342 小时前
我的第一个字符驱动:基于Linux2.4之前版本的古法编程
linux·驱动开发