【Linux系统编程】(三十四)初识进程信号:Linux 软中断的核心奥秘


目录

前言

一、从生活场景理解信号:原来信号这么简单

[1.1 快递的故事:完美映射信号处理流程](#1.1 快递的故事:完美映射信号处理流程)

[1.2 生活场景到 Linux 信号的核心结论](#1.2 生活场景到 Linux 信号的核心结论)

[二、技术视角:Linux 进程信号的初体验](#二、技术视角:Linux 进程信号的初体验)

[2.1 第一个实验:Ctrl+C的本质 ------ 向前台进程发送 2 号信号SIGINT](#2.1 第一个实验:Ctrl+C的本质 —— 向前台进程发送 2 号信号SIGINT)

代码实现:sig_hello.c

编译运行

[2.2 第二个实验:修改信号处理方式 ------ 让Ctrl+C不再终止进程](#2.2 第二个实验:修改信号处理方式 —— 让Ctrl+C不再终止进程)

[2.2.1 signal函数介绍](#2.2.1 signal函数介绍)

[2.2.2 代码实现:sig_catch.c](#2.2.2 代码实现:sig_catch.c)

[2.2.3 编译运行](#2.2.3 编译运行)

[2.2.4 实验思考:为什么进程不终止了?](#2.2.4 实验思考:为什么进程不终止了?)

[2.3 关键知识点:前台进程与后台进程的信号差异](#2.3 关键知识点:前台进程与后台进程的信号差异)

实战验证:后台进程不接收Ctrl+C信号

[三、Linux 信号的核心概念:你必须知道的基础定义](#三、Linux 信号的核心概念:你必须知道的基础定义)

[3.1 信号的官方定义](#3.1 信号的官方定义)

[3.2 如何查看 Linux 系统中的所有信号](#3.2 如何查看 Linux 系统中的所有信号)

[3.3 信号的三种处理动作](#3.3 信号的三种处理动作)

[3.3.1 执行默认动作(SIG_DFL)](#3.3.1 执行默认动作(SIG_DFL))

[3.3.2 忽略信号(SIG_IGN)](#3.3.2 忽略信号(SIG_IGN))

[代码验证:忽略 SIGINT 信号](#代码验证:忽略 SIGINT 信号)

[3.3.3 自定义捕捉(Catch)](#3.3.3 自定义捕捉(Catch))

代码验证:一个函数处理多个信号

[3.4 信号的宏定义](#3.4 信号的宏定义)

[四、信号的异步性:进程控制流程的 "意外插曲"](#四、信号的异步性:进程控制流程的 “意外插曲”)

[4.1 什么是信号的异步性](#4.1 什么是信号的异步性)

[4.2 为什么信号不能立即处理](#4.2 为什么信号不能立即处理)

[4.3 信号的处理时机:内核态 → 用户态的切换时刻](#4.3 信号的处理时机:内核态 → 用户态的切换时刻)

[五、常见终端信号的实战:除了 Ctrl+C,还有这些操作](#五、常见终端信号的实战:除了 Ctrl+C,还有这些操作)

[5.1 Ctrl+\:SIGQUIT 信号,终止进程并生成 core dump](#5.1 Ctrl+\:SIGQUIT 信号,终止进程并生成 core dump)

[实战验证:SIGQUIT 信号的默认动作](#实战验证:SIGQUIT 信号的默认动作)

[5.2 Ctrl+Z:SIGTSTP 信号,暂停前台进程](#5.2 Ctrl+Z:SIGTSTP 信号,暂停前台进程)

[实战验证:SIGTSTP 信号的默认动作](#实战验证:SIGTSTP 信号的默认动作)

总结


前言

在 Linux 系统中,进程信号是实现进程间异步通信的核心机制,也是操作系统对进程进行事件通知的重要手段,被称为软中断 。无论是我们日常按下Ctrl+C终止进程,还是程序出现段错误、除零异常,背后都是信号在发挥作用。很多 Linux 初学者会觉得信号晦涩难懂,其实它的逻辑和我们生活中的场景高度相似。本文将从生活视角切入,结合实战代码,由浅入深带你认识进程信号,搞懂信号的识别、产生、处理全流程,为后续深入学习信号的保存、捕捉、阻塞打下坚实基础。下面就让我们正式开始吧!


一、从生活场景理解信号:原来信号这么简单

想要搞懂 Linux 进程信号,不如先从我们身边的小事说起。信号的核心逻辑在生活中随处可见,理解了生活中的 "信号处理",就能轻松迁移到 Linux 系统中。

1.1 快递的故事:完美映射信号处理流程

想象一个场景:你在家打游戏,网购的快递到了,快递员在楼下给你打电话通知取件。这个过程和 Linux 进程处理信号的逻辑几乎一模一样,我们来拆解每一个环节:

  1. 信号识别 :你看到快递员的电话,就知道是快递到了,不需要别人额外解释 ------ 这对应 Linux 进程对信号的内置识别能力 ,内核程序员在设计进程时,已经让进程能识别系统中所有合法信号,比如Ctrl+C对应的 2 号信号SIGINT、段错误对应的 11 号信号SIGSEGV
  2. 信号延迟处理 :你正在打游戏的关键团战,需要 5 分钟后才能下楼取件 ------ 这对应信号的异步特性 ,进程收到信号后,不会立即处理,而是会在合适的时候处理(比如进程从内核态返回到用户态时),因为进程可能正在执行优先级更高的任务。
  3. 信号暂存 :5 分钟内,你虽然没取快递,但心里记着 "有快递要取"------ 这对应信号的暂存机制,进程收到信号后,如果暂时不处理,会将信号保存在进程控制块(PCB)中,直到处理时机到来。
  4. 信号的三种处理方式 :等你打完游戏取到快递后,会有三种选择:
    • 默认处理 :开心地拆开快递,使用里面的商品 ------ 对应进程对信号的默认动作 ,比如SIGINT的默认动作是终止进程,SIGSEGV的默认动作是终止进程并生成核心转储文件。
    • 自定义处理 :快递是零食,你转手送给了女朋友 ------ 对应进程对信号的自定义捕捉,通过注册信号处理函数,让进程收到信号后执行自己定义的逻辑。
    • 忽略处理 :把快递拿回家扔在床头,继续打游戏 ------ 对应进程忽略信号 ,收到信号后不做任何操作,比如SIGCHLD信号的默认动作就是忽略。
  5. 异步特性 :你无法准确知道快递员什么时候会打电话 ------ 这对应信号的异步性,信号对于进程的控制流程来说是异步的,进程无法预知信号何时到来,可能在执行代码的任意位置收到信号。

1.2 生活场景到 Linux 信号的核心结论

从快递的故事,我们可以提炼出 Linux 进程信号的几个核心结论,这是理解信号的关键:

  1. 进程对信号的识别能力是内置的,无需后天学习,内核已经为进程预设了所有合法信号的识别逻辑;
  2. 信号的处理方法在信号产生前就已准备好,进程无论是否收到信号,都知道该如何处理每一种信号(默认 / 忽略 / 自定义);
  3. 信号处理并非立即执行,进程会在合适的时机处理,核心原因是信号具有异步性,且进程可能在执行高优先级任务;
  4. 信号的完整生命周期是:信号产生 → 信号保存 → 信号处理
  5. 信号的处理方式只有三种:默认处理(SIG_DFL)、忽略处理(SIG_IGN)、自定义捕捉(Catch)

二、技术视角:Linux 进程信号的初体验

理解了生活中的信号逻辑,我们回到 Linux 系统本身,通过实战代码终端操作 ,直观感受进程信号的存在和作用,搞懂Ctrl+C终止进程的底层原理,以及如何通过代码修改信号的处理方式。

2.1 第一个实验:Ctrl+C的本质 ------ 向前台进程发送 2 号信号SIGINT

我们先写一个简单的死循环 C 程序,让进程一直运行,然后通过Ctrl+C终止它,看看背后发生了什么。

代码实现:sig_hello.c

cpp 复制代码
#include <iostream>
#include <unistd.h>
using namespace std;

int main()
{
    while (true)
    {
        cout << "I am a process, my PID is: " << getpid() << ", waiting signal!" << endl;
        sleep(1); // 每秒打印一次
    }
    return 0;
}

编译运行

bash 复制代码
# 编译
g++ sig_hello.c -o sig_hello
# 运行
./sig_hello

运行后,终端会不断打印进程 PID 和提示信息,此时按下Ctrl+C,进程会立即终止。这背后的底层逻辑是什么?

  1. 我们按下Ctrl+C时,键盘输入会产生一个硬件中断,被操作系统(OS)捕获;
  2. OS 将这个硬件中断解释为2 号信号 SIGINT(中断信号)
  3. OS 将SIGINT信号发送给当前的前台进程 (也就是我们运行的sig_hello进程);
  4. 前台进程收到SIGINT信号后,执行默认处理动作------ 终止进程。

这就是Ctrl+C终止进程的完整流程,核心是OS 将硬件操作转换为信号,发送给进程并触发默认处理

2.2 第二个实验:修改信号处理方式 ------ 让Ctrl+C不再终止进程

既然进程对信号的处理方式可以自定义,那我们能不能通过代码修改SIGINT信号的处理方式,让按下Ctrl+C后进程不终止,而是执行我们自定义的逻辑?

答案是肯定的,我们可以使用ANSI C 标准的signal函数来修改信号的处理动作,该函数的作用是为指定信号注册自定义处理函数。

2.2.1 signal函数介绍

cpp 复制代码
#include <signal.h>
// 函数指针类型,定义信号处理函数的格式
typedef void (*sighandler_t)(int);
// 注册信号处理函数
sighandler_t signal(int signum, sighandler_t handler);

参数说明

  • signum:要处理的信号编号,比如 2 代表SIGINT,3 代表SIGQUIT
  • handler:信号处理函数的指针,有三种取值:
    1. 自定义函数指针:进程收到信号后执行该函数;
    2. SIG_IGN:忽略该信号;
    3. SIG_DFL:执行该信号的默认处理动作。返回值 :成功返回该信号原来的处理函数指针,失败返回SIG_ERR

2.2.2 代码实现:sig_catch.c

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;

// 自定义信号处理函数
void sig_handler(int signum)
{
    // signum参数会传入当前收到的信号编号
    cout << "我是进程[" << getpid() << "],捕获到信号:" << signum << ",我没有终止!" << endl;
}

int main()
{
    cout << "我是主进程,PID:" << getpid() << endl;
    // 为2号信号SIGINT注册自定义处理函数
    signal(SIGINT, sig_handler);
    
    while (true)
    {
        cout << "进程正在运行,等待信号..." << endl;
        sleep(1);
    }
    return 0;
}

2.2.3 编译运行

bash 复制代码
g++ sig_catch.c -o sig_catch
./sig_catch

运行后,再次按下Ctrl+C,会发现进程不再终止,而是打印我们自定义的信息,比如:

复制代码
我是主进程,PID:12345
进程正在运行,等待信号...
进程正在运行,等待信号...
^C我是进程[12345],捕获到信号:2,我没有终止!
进程正在运行,等待信号...

2.2.4 实验思考:为什么进程不终止了?

因为我们通过signal函数将SIGINT信号的处理方式从默认终止 修改为自定义执行sig_handler函数,进程收到信号后,会执行我们定义的逻辑,而不是默认的终止动作。

这个实验也印证了一个重要点:signal函数只是设置信号的捕捉行为,并不是直接调用处理函数。如果信号没有产生,注册的自定义函数永远不会被执行。

2.3 关键知识点:前台进程与后台进程的信号差异

在上面的实验中,我们提到Ctrl+C产生的信号只能发送给前台进程,这是 Linux 系统的一个重要规则,我们需要明确前台进程和后台进程的信号差异:

  1. 前台进程 :在 Shell 中直接运行的进程,占据终端的输入输出,能接收终端控制键产生的信号 ,比如Ctrl+C(SIGINT)、Ctrl+\(SIGQUIT)、Ctrl+Z(SIGTSTP);
  2. 后台进程 :在命令后加&运行的进程,不占据终端的输入输出,无法接收终端控制键产生的信号 ,比如./sig_catch &
  3. Shell 的进程管理规则: Shell 可以同时运行一个前台进程任意多个后台进程,只有前台进程能接收终端的信号。

实战验证:后台进程不接收Ctrl+C信号

bash 复制代码
# 将进程放到后台运行
./sig_catch &
# 查看后台进程
jobs
# 此时按下Ctrl+C,后台进程不会收到信号,继续运行
# 想要终止后台进程,可以使用kill命令
kill -2 进程PID # 向进程发送2号信号SIGINT

三、Linux 信号的核心概念:你必须知道的基础定义

通过前面的实战,我们已经直观感受了信号的作用,接下来我们正式定义 Linux 进程信号的核心概念,为后续深入学习打下理论基础,这些概念是理解信号的基石。

3.1 信号的官方定义

信号是进程之间事件异步通知的一种方式 ,属于软中断

  • 异步通知:进程无需主动轮询,信号会在任意时刻到来,进程在合适的时机处理即可;
  • 软中断:信号是软件层面模拟硬件中断的机制,硬件中断是发给 CPU 的,而信号是发给进程的,两者的处理流程相似,但作用层级不同。

3.2 如何查看 Linux 系统中的所有信号

Linux 系统为进程预设了大量信号,编号从 1 开始,其中34 号及以上为实时信号 ,34 号以下为常规信号(也叫非实时信号),我们日常开发中主要使用常规信号。

可以通过以下命令查看系统中所有的信号:

bash 复制代码
# 方式1:kill -l (l是list的缩写)
kill -l
# 方式2:man 7 signal (查看信号的详细说明,包括产生条件、默认动作)
man 7 signal

执行kill -l后,终端会输出 Linux 系统的所有信号,部分关键常规信号如下:

信号编号 信号名 产生条件 默认动作
1 SIGHUP 进程所属的终端挂起 终止进程
2 SIGINT 终端按下 Ctrl+C 终止进程
3 SIGQUIT 终端按下 Ctrl+\ 终止进程并生成 core dump
9 SIGKILL kill -9 进程 PID 终止进程(不可捕捉、不可忽略)
11 SIGSEGV 非法内存访问(段错误) 终止进程并生成 core dump
14 SIGALRM 闹钟超时(alarm 函数) 终止进程
15 SIGTERM kill 进程 PID(默认信号) 终止进程(可捕捉、可忽略)
17 SIGCHLD 子进程终止 / 暂停 忽略信号
20 SIGTSTP 终端按下 Ctrl+Z 暂停进程

重要注意 :9 号信号SIGKILL和 19 号信号SIGSTOP是 Linux 系统中不可捕捉、不可忽略的信号,这是为了保证操作系统能绝对控制进程,避免进程通过自定义信号处理方式变成 "无法杀死的僵尸进程"。

3.3 信号的三种处理动作

正如我们在生活场景中总结的,Linux 进程对信号的处理动作只有三种,这是内核为进程预设的基本规则,所有信号都遵循这个规则:

3.3.1 执行默认动作(SIG_DFL)

这是绝大多数信号的默认处理方式,不同信号的默认动作不同,主要包括:

  • Term:终止进程(如 SIGINT、SIGTERM、SIGALRM);
  • Core:终止进程并生成 core dump 文件(如 SIGQUIT、SIGSEGV、SIGFPE),core dump 文件用于事后调试;
  • Stop:暂停进程(如 SIGTSTP、SIGSTOP);
  • Cont:继续运行暂停的进程(如 SIGCONT);
  • Ign:忽略信号(如 SIGCHLD)。

3.3.2 忽略信号(SIG_IGN)

进程收到信号后,不做任何操作,直接忽略。比如我们可以通过**signal(SIGINT, SIG_IGN)**让进程忽略Ctrl+C信号,此时按下Ctrl+C,进程不会有任何反应。

代码验证:忽略 SIGINT 信号

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;

int main()
{
    cout << "进程PID:" << getpid() << ",已忽略SIGINT信号" << endl;
    // 忽略2号信号SIGINT
    signal(SIGINT, SIG_IGN);
    
    while (true)
    {
        cout << "进程正在运行..." << endl;
        sleep(1);
    }
    return 0;
}

编译运行后,无论按多少次Ctrl+C,进程都不会终止,因为进程已经忽略了SIGINT信号。

3.3.3 自定义捕捉(Catch)

通过**signalsigaction函数为信号注册自定义处理函数**,进程收到信号后,执行我们定义的逻辑,这是信号最灵活的处理方式,也是开发中最常用的方式。

前面的sig_catch.c就是自定义捕捉的典型例子,这里再补充一个关键点:自定义处理函数的参数 。自定义信号处理函数的格式是void handler(int signum),其中signum参数会自动传入当前收到的信号编号,这让我们可以用一个函数处理多个信号

代码验证:一个函数处理多个信号

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;

// 一个函数处理多个信号
void sig_handler(int signum)
{
    switch (signum)
    {
        case 2:
            cout << "捕获到SIGINT(2号)信号,Ctrl+C无效!" << endl;
            break;
        case 3:
            cout << "捕获到SIGQUIT(3号)信号,Ctrl+\\无效!" << endl;
            break;
        case 20:
            cout << "捕获到SIGTSTP(20号)信号,Ctrl+Z无效!" << endl;
            break;
        default:
            cout << "捕获到未知信号:" << signum << endl;
            break;
    }
}

int main()
{
    cout << "进程PID:" << getpid() << ",已注册多信号处理函数" << endl;
    // 为2、3、20号信号注册同一个处理函数
    signal(SIGINT, sig_handler);
    signal(SIGQUIT, sig_handler);
    signal(SIGTSTP, sig_handler);
    
    while (true)
    {
        cout << "进程正在运行..." << endl;
        sleep(1);
    }
    return 0;
}

编译运行后,按下Ctrl+CCtrl+\Ctrl+Z,进程都会执行对应的自定义逻辑,而不是默认动作,实现了一个函数处理多个信号的效果。

3.4 信号的宏定义

在 Linux 系统中,所有信号的名称都是宏定义 ,对应的头文件是<signal.h>,宏定义的本质是将信号编号转换为易记的名称,方便开发人员使用。

比如<signal.h>中的部分宏定义:

cpp 复制代码
#define SIGINT 2    // 中断信号
#define SIGQUIT 3   // 退出信号
#define SIGKILL 9   // 杀死信号
#define SIGSEGV 11  // 段错误信号
#define SIGTERM 15  // 终止信号
#define SIGCHLD 17  // 子进程状态变化信号
#define SIGTSTP 20  // 暂停信号
// 信号处理动作的宏定义
#define SIG_DFL ((__sighandler_t) 0)  // 默认动作
#define SIG_IGN ((__sighandler_t) 1)  // 忽略信号

可以看到,**SIG_DFLSIG_IGN**本质上是将 0 和 1 强制转换为信号处理函数的指针类型,让它们能作为signal函数的第二个参数。

四、信号的异步性:进程控制流程的 "意外插曲"

信号的异步性是其最核心的特性之一,也是理解信号的关键难点,我们需要单独拿出来重点讲解,因为它决定了信号的处理时机和方式。

4.1 什么是信号的异步性

所谓异步,是相对于同步而言的:

  • 同步执行:进程的代码按顺序执行,执行完一行再执行下一行,流程完全可控;
  • 异步事件 :信号会在任意时刻到来,进程无法预知信号的产生时间,可能在执行代码的任意位置收到信号,比如进程在执行循环、函数调用、系统调用时,都可能收到信号。

举个例子:进程正在执行for循环的第 100 次迭代,此时 OS 向进程发送了SIGINT信号,进程会在合适的时机暂停循环,处理信号,处理完成后再继续执行循环的第 100 次迭代,这就是异步性的体现。

4.2 为什么信号不能立即处理

很多初学者会有疑问:进程收到信号后,为什么不立即处理,而是要等到 "合适的时机"?主要有两个原因:

  1. 进程可能在执行高优先级任务:比如进程正在执行内核态的代码(如系统调用),此时处理信号可能会破坏内核数据结构,导致系统崩溃,因此需要等进程从内核态返回到用户态时再处理;
  2. 信号处理的原子性:如果进程在执行一个不可中断的操作(如修改全局变量),此时处理信号可能会导致数据不一致,因此需要等操作完成后再处理。

4.3 信号的处理时机:内核态 → 用户态的切换时刻

Linux 进程的运行状态分为用户态内核态

  • 用户态:进程执行自己的用户代码(如我们写的 C/C++ 代码),权限较低,不能直接访问硬件和内核数据;
  • 内核态:进程执行内核代码(如系统调用、中断处理),权限较高,可以访问硬件和内核数据。

进程的一生会在用户态内核态之间不断切换,比如:

  • 进程调用readwritesleep等系统调用时,会从用户态切换到内核态;
  • 系统调用执行完成后,进程会从内核态切换回用户态。

信号的处理时机进程从内核态返回到用户态的瞬间,OS 会检查进程的 PCB 中是否有未处理的信号,如果有,就会先处理信号,处理完成后再返回到用户态执行进程的正常代码。

这是 Linux 系统规定的唯一信号处理时机,也是保证信号处理安全性的关键。

五、常见终端信号的实战:除了 Ctrl+C,还有这些操作

除了Ctrl+C对应的SIGINT信号,我们在终端中常用的Ctrl+\Ctrl+Z也会产生对应的信号,分别是SIGQUIT(3 号)和SIGTSTP(20 号),我们来实战验证这两个信号的作用,进一步加深对信号的理解。

5.1 Ctrl+\:SIGQUIT 信号,终止进程并生成 core dump

SIGQUIT信号的默认动作是终止进程并生成 core dump 文件,core dump 文件是进程的内存镜像文件,包含了进程终止时的内存数据、寄存器状态等信息,用于事后调试(Post-mortem Debug)。

实战验证:SIGQUIT 信号的默认动作

bash 复制代码
# 运行之前的死循环程序
./sig_hello
# 按下Ctrl+\,进程终止并生成core dump文件
^\Quit (core dumped)
# 查看当前目录下的core文件
ls -l core*

注意:Linux 系统默认关闭 core dump 功能,若没有生成 core 文件,可以通过以下命令开启:

bash 复制代码
# 设置core文件的最大大小为1024KB(临时生效,重启Shell后失效)
ulimit -c 1024
# 查看core文件限制
ulimit -a

5.2 Ctrl+Z:SIGTSTP 信号,暂停前台进程

SIGTSTP信号的默认动作是暂停前台进程 ,将进程从前台切换到后台,状态变为Stopped,暂停的进程可以通过fg命令恢复到前台,或通过bg命令让其在后台继续运行。

实战验证:SIGTSTP 信号的默认动作

bash 复制代码
# 运行死循环程序
./sig_hello
# 按下Ctrl+Z,进程被暂停
^Z[1]+  Stopped                 ./sig_hello
# 查看后台暂停的进程
jobs
# 将暂停的进程恢复到前台运行
fg %1
# 将暂停的进程在后台继续运行
bg %1
# 终止后台进程
kill -9 进程PID

我们也可以通过**signal函数自定义SIGQUITSIGTSTP**信号的处理方式,让它们不再执行默认动作,代码和前面的sig_catch.c类似,只需将信号编号改为 3 和 20 即可。


总结

Linux 进程信号是操作系统的核心知识点,也是面试的高频考点,想要真正掌握信号,不能只看理论,一定要多写代码、多做实验,从实战中理解信号的逻辑。本文的所有代码都可以直接编译运行,建议大家亲手敲一遍,感受信号的魅力。后续我会继续更新信号的进阶内容,敬请关注!

相关推荐
盟接之桥2 小时前
盟接之桥说制造:制造业的精致之道,致制造人
大数据·linux·运维·人工智能·windows·安全·制造
晔子yy2 小时前
AI编程时代:发挥Rules约束在Vibe-Coding的重要作用
开发语言·人工智能·后端
夏乌_Wx2 小时前
从零开始实现一个自己的 Shell:mybash 项目实战
linux·c语言·后端
kong79069282 小时前
SpringBoot原理
java·spring boot·后端
那我掉的头发算什么2 小时前
【图书管理系统】基于Spring全家桶的图书管理系统(下)
java·数据库·spring boot·后端·spring·mybatis
Codefengfeng11 小时前
CTF工具篇
linux·运维·服务器
上海合宙LuatOS12 小时前
LuatOS核心库API——【i2c】I2C 操作
linux·运维·单片机·嵌入式硬件·物联网·计算机外设·硬件工程
毅炼13 小时前
Java 集合常见问题总结(3)
java·开发语言·后端
一文解千机14 小时前
wine 优化配置及显卡加速,完美运行Electron 编译的程序(新榜小豆芽、作家助手、小V猫等)
linux·ubuntu·electron·wine·wine优化配置·wine显卡加速·wine大型游戏