___信号


【Linux系统编程】初识进程信号:从生活实例到内核崩溃原理

文章目录

  • 【Linux系统编程】初识进程信号:从生活实例到内核崩溃原理
    • [1. 什么是信号?从生活说起](#1. 什么是信号?从生活说起)
      • [💡 核心特征总结](#💡 核心特征总结)
    • [2. Linux 中的信号定义](#2. Linux 中的信号定义)
    • [3. 信号的三种处理方式](#3. 信号的三种处理方式)
    • [4. 信号的生命周期](#4. 信号的生命周期)
      • [⚠️ 重要区分:信号 vs 信号量](#⚠️ 重要区分:信号 vs 信号量)
    • [5. 核心 API:signal 函数与自定义处理](#5. 核心 API:signal 函数与自定义处理)
      • [🛠️ 函数原型](#🛠️ 函数原型)
      • [🔍 参数解析](#🔍 参数解析)
      • [💻 实战演示:重定义 Ctrl+C](#💻 实战演示:重定义 Ctrl+C)
    • [6. 深度解析:为什么除零和野指针会导致程序崩溃?](#6. 深度解析:为什么除零和野指针会导致程序崩溃?)
    • [7. 补充说明](#7. 补充说明)
    • [8. 总结一张图](#8. 总结一张图)

1. 什么是信号?从生活说起

在深入代码实现之前,我们先通过生活中的常见场景来理解"信号"这一概念。想象一下日常生活中的这些例子:

  • 红绿灯 → 指示司机停车或通行
  • 闹钟/铃声 → 提醒起床或下课时间
  • 敲门声 → 提示有访客到来
  • 肚子叫 → 身体发出的饥饿信号
  • 面部表情 → 传达情绪的无声语言

💡 核心特征总结

通过这些生活实例,我们可以提炼出关于"信号"的几个关键特征:

  1. 预设的处理方法

    当信号产生时,我们通常已经知道该如何应对(比如听到闹钟就知道要起床)。这表明信号的处理方式在信号产生之前就已经确定。

  2. 灵活的响应时机

    收到信号并不意味着必须"立即"中断当前活动去处理。如果手头有优先级更高的事务,我们可以选择在合适的时机再行处理。

  3. 内置的识别机制

    我们之所以能够识别这些信号,是因为大脑已经建立了相应的映射关系。在计算机系统中,进程识别信号的能力是内核程序员预先设计好的内置特性。

2. Linux 中的信号定义

将上述生活经验迁移到操作系统中,我们可以这样定义信号:

信号 (Signal) 是外部实体(其他进程、用户或硬件)向进程发送的一种异步事件通知机制。

其主要作用体现在三个方面:

  1. 事件通知:告知进程发生了特定事件
  2. 并发无关:多种事件可以独立、同时发生,互不干扰
  3. 行为控制:可能导致进程终止、异常退出或执行特定指令

3. 信号的三种处理方式

当进程接收到信号后,通常有以下三种应对策略:

  • 默认处理 (Default Action)

    系统预设的标准响应方式。绝大多数信号的默认处理都是终止进程(例如按下 Ctrl+C 发送的 SIGINT 信号)

  • 忽略 (Ignore)

    进程选择对收到的信号不予理睬,不执行任何操作

  • 自定义处理 (Custom Handler)

    也称为信号捕捉。程序员可以通过编写代码告诉进程:"当你收到这个特定信号时,不要执行默认操作,而是运行我定义的函数"

4. 信号的生命周期

信号在系统中并非瞬时完成,而是经历一个完整的生命周期,主要分为三个阶段:

  1. 信号产生 (Generation):事件的起源阶段,可能由用户按键、硬件故障或系统调用触发

  2. 信号保存 (Pending):信号产生后并不会立即被处理,而是被记录在内核中,处于"待处理"状态

    💭 思考 :为什么需要这个中间阶段?

    💡 答案:因为进程可能正在执行关键任务(如临界区),信号无法被即时处理,必须先暂存起来

  3. 信号处理 (Delivery/Handling):当进程处于合适的处理时机(通常是从内核态返回用户态时),内核会检查并递送信号,进程随即开始执行相应的处理动作

⚠️ 重要区分:信号 vs 信号量

在学习过程中,务必区分这两个概念:

  • 信号 (Signal):一种事件通知机制,用于告知进程发生了什么
  • 信号量 (Semaphore):一种同步互斥机制,用于控制资源的并发访问

一句话总结:它们就像"老婆"和"老婆饼"的关系------名称相似,但本质完全不同,没有任何关联!

5. 核心 API:signal 函数与自定义处理

这是 C 语言标准库(libc)提供的用于捕获和修改信号行为的接口

🛠️ 函数原型

c 复制代码
#include <signal.h>

typedef void (*sighandler_t)(int); // 函数指针类型
sighandler_t signal(int signum, sighandler_t handler);

🔍 参数解析

  • signum目标信号编号 (例如 SIGINT
  • handler处理函数指针。传入一个函数地址,告诉系统当收到该信号时去执行哪个函数
  • 返回值 :返回该信号之前的旧的处理动作

💻 实战演示:重定义 Ctrl+C

默认情况下,按下 Ctrl+C 会导致进程直接终止。我们可以通过 signal 函数改变这一行为

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

// 自定义的处理函数
void handler(int signo) {
    // 这里可以写任何你想做的逻辑,比如清理资源、打印日志等
    std::cout << "捕捉到了信号: " << signo << std::endl;
}

int main() {
    // 【关键步骤】重定义 SIGINT 的行为
    // 将 SIGINT (2号信号) 的处理动作修改为 handler 函数
    signal(SIGINT, handler);

    while(true) {
        std::cout << "test sig..., pid: " << getpid() << std::endl;
        sleep(1);
    }
    return 0;
}

运行结果分析:

此时再按 Ctrl+C,进程不会退出 ,而是执行 handler 里的代码,打印出"捕捉到了信号"

⚠️ 注意细节

  • 9号信号 (SIGKILL) 不可被自定义或忽略,它是 OS 留给管理员的"终极武器"
  • 若对所有信号都自定义为"不退出",进程可能变成无法杀死的僵尸进程

6. 深度解析:为什么除零和野指针会导致程序崩溃?

我们在写代码时,经常会遇到程序突然挂掉的情况,比如经典的 Floating point exception 或者 Segmentation fault

很多初学者会问:为什么我的代码里只是写了一个除法,或者访问了一个指针,程序就自己崩了?是谁杀死了它?

答案其实很硬核:是操作系统(OS)杀死了你的进程。 而 OS 使用的武器,就是 信号(Signal)

🚨 现象复现

错误类型 代码示例 终端输出 对应信号
除以 0 int c = a / b; (b=0) Floating point exception 8号信号 SIGFPE
野指针 *p = 100; (p非法) Segmentation fault 11号信号 SIGSEGV

🔍 深度解析:OS 是如何发现错误的?

这涉及到一个 "硬件 → OS → 进程" 的三级联动机制

第一步:硬件层面的"报警"

CPU 和 MMU(内存管理单元)是执行指令的一线人员

  • 除以 0:CPU 在执行除法指令时,ALU(算术逻辑单元)检测到除数为 0,硬件电路直接报错(Exception)
  • 野指针:CPU 试图访问某个虚拟地址,MMU 查页表发现该地址没有映射到物理内存(或者权限不足),MMU 直接报错(Page Fault / Segmentation Fault)

此时,硬件中断发生了!

第二步:OS 接管现场

硬件报错后,CPU 会陷入内核态,跳转到 OS 预设的中断处理程序。OS 作为"管理者",收到硬件的报错后,会分析当前正在运行的是哪个进程(通过 PCB/task_struct)

OS 的逻辑是:"谁在运行时把硬件搞坏了?就是这个进程!"

第三步:发送信号(修改位图)

OS 不会直接帮进程修好错误,而是决定惩罚这个进程。它通过 发送信号 来通知进程

  • 数据结构 :在进程的 task_struct 结构体中,有一个成员叫 sig(或者 pending 信号集)
  • 本质操作 :这是一个 位图(Bitmap)
    • 假设进程 PID 为 100
    • OS 找到 PID 100 的 task_struct
    • 将位图中第 8 位(SIGFPE)或第 11 位(SIGSEGV)置为 1

这就是"发送信号"的本质:修改目标进程 PCB 中的信号位图

📉 信号的递送与处理流程

信号被"记录"在位图中后,并不代表立刻执行

  1. 保存 :信号被保存在内核空间的 task_struct
  2. 检测:当进程从内核态返回用户态时(例如系统调用结束、中断处理结束),OS 会检查该进程的信号位图
  3. 处理
    • OS 发现第 11 位是 1(有 SIGSEGV)
    • 查看该信号的默认处理动作(Default Action)
    • 对于 SIGSEGV 和 SIGFPE,默认动作是 Term (Terminate) 并生成 Core Dump
  4. 结果:进程被强制杀死,终端打印出 "Segmentation fault"

7. 补充说明

  • 键盘输入 :仅控制前台进程,因后台进程无法接收键盘输入
    • Ctrl+C:发 2号信号(默认终止进程)
    • Ctrl+\:发 3号信号(默认终止进程)
    • Ctrl+Z:发 19号信号(默认暂停进程)
  • Bash 进程特性 :Bash 进程通常会忽略大部分信号(除了 SIGKILL 等),故自身不对信号做常规响应,防止 Shell 意外退出
  • 查看帮助 :可以使用 man 7 signal 查看所有信号的默认动作

8. 总结一张图

text 复制代码
代码出错 (div 0 / bad ptr)
      ↓
硬件检测异常 (CPU/MMU Trap)
      ↓
OS 中断处理程序 (Kernel Mode)
      ↓
找到当前进程 PCB (task_struct)
      ↓
修改信号位图 (Set bit 8 or 11)  <-- 发送信号的本质
      ↓
进程恢复运行,OS 检查到位图有信号
      ↓
执行默认动作 (SIG_DFL) → 终止进程
      ↓
终端显示: Floating point exception / Segmentation fault

`

相关推荐
qq_163135751 小时前
Linux 【04-more命令超详细教程】
linux
sevencheng7982 小时前
【ADB】adb命令行常用按键模拟代码
linux·adb·模拟按键,返回键,音量键
暗影天帝2 小时前
BPI-R3 Mini 刷 Yuzhii DHCPD U-Boot 教程
linux
小赖同学啊3 小时前
智能连接器集群化高可用生产方案
linux·运维·人工智能
Cinema KI3 小时前
Linux第一个系统程序-进度条
linux·服务器
Moshow郑锴3 小时前
Ubuntu 26.04 更换阿里云源镜像
linux·运维·ubuntu
Jason_chen4 小时前
Linux 6.2 串口机制深度解析:AI驱动的自适应通信与零信任串口安全架构
linux
ShineWinsu4 小时前
对于Linux:线程概念与分页存储管理的解析
linux·运维·服务器·面试·线程·进程·虚拟空间地址
用户3946235365245 小时前
Uboot - DM框架
linux
鹤落晴春6 小时前
RH124问答5:管理本地用户和组
linux·运维·服务器