【Linux】进程信号(1)理解信号及信号产生的方式

目录

[一 快速认识信号](#一 快速认识信号)

[1 理解信号是什么?为什么要有?](#1 理解信号是什么?为什么要有?)

[2 自定义捕捉信号](#2 自定义捕捉信号)

[3 信号产生的方式](#3 信号产生的方式)

(1)通过系统命令,kill产生

(2)通过键盘产生信号

[(3) 系统调用](#(3) 系统调用)

(4)异常(产生信号方式)

(5)软件条件软件条件)

[4 键盘怎么向目标进程发送信号](#4 键盘怎么向目标进程发送信号)


一 快速认识信号

1 理解信号是什么?为什么要有?

1-31号信号是普通信号

信号:用户或系统,异步的给进程发送就绪事件方式,异步通知机制

我们来解释一下什么是异步:

例如:同步:骑手打电话让你下楼取餐,必须等你拿到才离开,双方互相等待、步调一致。

异步:骑手把外卖放门口发个消息就走,不用等你处理,你有空再去拿,双方互不等待、各做各的

所以:

同步:发通知的人要等你回应、处理完才走,双方步调绑定、互相等待。
异步:发通知的人发完就走,不等你处理,你有空再处理,双方互不等待、各自执行

人是能识别信号的:1 认识它 2 触发事件,即使信号没有发生,也知道该怎么做。例如:遇到红绿灯,闹钟想起来的时候....

结论:进程能识别信号

基本结论:

你怎么能识别信号呢?识别信号是内置的,进程识别信号,是内核程序员写的内置特性

信号产生之后,你知道怎么处理吗?知道 。如果信号没有产生,你知道怎么处理信号吗?知道。所以,信号的处理方法,在信号产生之前,已经准备好了。

处理信号,立即处理吗?我可能正在做优先级更高的事情,不会立即处理?什么时候?合适的时候。

信号到来 | 信号保存 | 信号处理

怎么进行信号处理啊?a. 默认 b. 忽略 c. 自定义,后续都叫做信号捕捉

因为有时信号不会被立即处理(操作系统可能有更重要的事做),所以就要求进程有临时保存信号的能力

信号处理的方式有三种:默认,忽略,自定义

在进程中要有特定的数据类型保存对应信号:位图!!!

保存什么信号? → 记录信号编号

信号的状态? → 记录这个信号是否已经产生 / 被收到

为了高效实现这两点,内核采用了 位图(bit array) 的设计。

位图的原理:

底层实现:用一个整数(比如 long int,32 位 / 64 位)来存储

每一个比特位对应一个信号编号(比如第 3 位对应信号 3)

比特位的值为 1,表示收到了该信号;值为 0,表示未收到

例如:

复制代码
二进制: 0000 0000 0000 0000 0000 0000 0000 1001

第 0 位为 1:收到了信号 0(保留信号)

第 3 位为 1:收到了信号 3(SIGQUIT)

其余位为 0:未收到对应信号

信号具体化:就是一个数字,可以指导操作系统修改进程中的PCB信息

发送一个信号的操作,归根结底就是一次内存数据的修改:

目标:找到目标进程的 PCB(进程控制块)。

操作:修改 PCB 中信号位图的指定位。

结果:将该信号的状态置为 1(未决 / 待处理),表示 "我给你发了个信号"

位图在哪?进程PCB中,PCB是内核数据结构;能够给进程写入信号的,只有操作系统

信号的产生:让操作系统给目标进程写信号

2 自定义捕捉信号

signal 系统调用:收到某个信号时,用哪种方式处理

cpp 复制代码
signal(int signum, void (*handler)(int));

参数含义:

signum:要处理的信号编号(如 SIGINT、SIGQUIT)

handler:信号的处理方式,有 3 种选择:

SIG_DFL:使用系统默认动作(通常终止进程)

SIG_IGN:忽略该信号

自定义函数名:收到信号时自动调用这个函数(调用signal时,可以把自己写的函数【返回值void,参数int】,把函数的入口地址设置进来)

向这种给一个函数,把另一个函数以参数的形式设置进来,叫做回调

signal设置好了,不是立即调用,而是在未来处理信号的时候使用

signal的本质是对知道信号来进行自定义处理的一种延时处理

信号的处理工作是进程做的

在Linux中,不同的信号的自定义捕捉方法一样

cpp 复制代码
signal(信号, SIG_IGN);   // 忽略
signal(信号, SIG_DFL);   // 默认

SIG_IGN:Ignore → 忽略

收到信号,不做任何处理,直接丢掉。

SIG_DFL:Default → 默认动作

收到信号,执行系统原本的行为(大部分是终止进程)

3 信号产生的方式

(1)通过系统命令,kill产生

(2)通过键盘产生信号

Ctrl + C:发送 2 号信号 SIGINT

Ctrl + \:发送 3 号信号 SIGQUIT

Ctrl + Z:发送 20 号信号 SIGTSTP

信号名 编号 按键 默认动作 作用说明
SIGINT 2 Ctrl + C Terminate(终止) 终端中断信号,请求进程正常退出
SIGQUIT 3 Ctrl + \ Core Dump(退出 + 生成 core 文件) 终端退出信号,进程会崩溃并生成调试用的 core 文件
SIGTSTP 20 Ctrl + Z Stop(暂停) 终端停止信号,让进程暂停,放入后台

Ctrl + C 能终止进程,本质是发送了 2 号信号,而 2 号信号的默认处理动作就是终止进程

我们来看一下信号的动作:Action那一列

Term和Core都表示进程退出

信号的动作的具体使用动作,我们在后面讲解

保留问题:键盘怎么能向目标进程发送信号

(3) 系统调用

kill:Linux/Unix 系统中用于向进程发送信号的核心系统调用

cpp 复制代码
#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);

pid表示发送给哪一个进程,sig表示要发送几号信号

rasie:进程给自己发送一个指定信号

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

int raise(int sig);

abort:作用是让进程立刻异常终止

cpp 复制代码
#include <stdlib.h>

void abort(void);

abort向调用进程,发送指定信号:6号 SIGABRT(动作是core)-->通常是用来进行异常终止,可以设置捕捉动作,但是最后还是会终止进程

上面三个系统调用是层层递进的:

函数 作用 本质
kill 给任意进程发任意信号 系统调用
raise 给自己发任意信号 kill(getpid(), sig)
abort 强制异常终止当前进程 最终发 SIGABRT 信号

不管发送信号的方式有多少种,最终,必须是谁完成真正的信号发送动作呢?
只能是操作系统

通过键盘产生信号不是直接键盘产生的,那么必然是操作系统先收到键盘的输入

(4)异常(产生信号方式)

信号捕捉函数一直被触发

软件层面,如果代码出现异常,进场就会自己收到对应的进程

进程为什么会把异常当作信号?进程异常如何被操作系统识别?

信号 11 (SIGSEGV) 是 Linux/Unix 系统上的 段错误(Segmentation Fault),表示程序访问了非法内存地址,被操作系统强制终止

两个问题:

a.进程为什么异常了之后,会自动退出----因为收到了信号,对进程的默认处理动作是终止进程;信号只能是操作系统发送的

b.为什么是这样?

1. 硬件层面:CPU 触发异常

CPU 是程序执行的核心硬件,当进程执行非法操作时,会直接触发 CPU 内部的硬件异常:

除 0 错误:执行a/0这类非法运算时,CPU 的运算单元会检测到错误,在 x86 架构下,会通过EFLAGS/RFLAGS寄存器(记录进程硬件上下文的状态寄存器)的相关标志位(如溢出标志位 OF)标记异常

野指针 / 非法内存访问:进程访问未分配、无权限的内存地址时,CPU 的内存管理单元(MMU)会触发页错误异常,同样会通知内核。
2. 内核层面:操作系统捕获异常并发送信号

操作系统是软硬件资源的管理者,必须感知并处理 CPU 的硬件异常:

内核会通过中断 / 异常处理机制,捕获 CPU 上报的硬件错误;

内核通过全局指针task_struct *current,精准定位到当前正在执行的、触发异常的进程;

内核根据异常类型,向该进程发送对应的致命信号(如除 0 对应SIGFPE,野指针对应SIGSEGV)。
3. 进程层面:信号的默认处理与自定义

进程收到信号后,会执行对应的处理动作:

默认行为:绝大多数致命信号的默认处理就是终止进程,这就是我们看到的「进程异常自动退出」;
自定义行为:进程可以通过signal()/sigaction()等系统调用,自定义信号的捕捉处理逻辑,替换默认的终止动作(比如打印崩溃日志、现场保存后再退出)

为什么进程开始死循环了?

因为溢出标志位没有恢复到0,CPU硬件上一直报错,导致进程一直在处理信号

结论:C/C++程序一旦出现异常(一般都是引起了硬件异常,操作系统识别硬件异常,给目标进程发送特定对应信号,所以进程收到和进程处理本质是让系统恢复正常),程序会崩溃的原因是进程退出了

终止方式:

我们来学习一下前面提到的进程的动作
term:正常终止(外部原因进程被杀掉,进程内部无错误)

core:进程内部有问题,引起异常,需要自己debug;操作系统会将进程的退出时的相关内存数据,转存到磁盘上,这个过程叫做核心转储(形成core文件)。在云服务器上,核心转储这个配置是默认关闭的 为什么默认关闭? 因为AI云服务器中有大量的core文件,会出现未定义的问题,有风险。 那我们如何打开?

bash 复制代码
ulimit -c xxx
ulimt -c o

core.pid:在当前目录下,形成一个二进制文件

我们之前学到的,Linux 中进程退出状态用一个 32 位整数存储,如下图:

核心标志位:第 8 位为 1 时,代表进程崩溃时生成了 core dump,可通过waitpid()等系统调用获取该状态;若为 0:进程被信号杀死,但未生成 core dump

核心转储的核心作用是 "留存进程崩溃现场,支撑事后精准调试"。 它是 Linux 系统调试内存错误、生产级故障的 "终极手段",没有 core dump,很多底层、偶发的崩溃类问题几乎无法定位。结合 GDB 等工具,core 文件能将抽象的崩溃问题转化为具体的代码行、变量状态,大幅提升调试效率。

(5)软件条件

管道通信的一种场景:读端关闭,写端一直写,操作系统就会把写进程杀掉--->操作系统会给子进程发送一个信号:13号SIGPIPE(收到 SIGPIPE 后进程直接退出,无 core dump(默认动作是 Term,非 Core))

管道是一种基于文件系统的通信方式,所以属于软件范畴;当软件条件不满足是,操作系统会给目标发送信号,杀掉进程

系统调用:alarm 设置一个若干秒之后的闹钟;若干秒之后,操作系统就会发送14号信号SIGALRM信号

bash 复制代码
unsigned int alarm(unsigned int seconds);

功能:在 seconds 秒后,操作系统向调用进程发送 14 号信号 SIGALRM。

返回值:返回上一个闹钟的剩余时间(若未设置过则返回 0),多次调用会重置闹钟时间

信号名 编号 触发条件 信号来源 默认动作 核心用途
SIGPIPE 13 管道读端关闭,写端持续写入 操作系统(软件条件不满足) 终止进程 管道 / 套接字通信异常处理
SIGALRM 14 alarm() 定时时间到 操作系统(系统调用触发) 终止进程 超时控制、定时任务

alarm是一个一次性闹钟,对你这个进程,只会有一个闹钟

bash 复制代码
int n = alarm(0);

这句代码表示取消闹钟,返回剩余时间

理解闹钟:背后机理是定时器

应用场景:操作系统具有设定延时触发的能力

操作系统内部可不可以同时存在多种闹钟(定时器)?操作系统要不要管理这些闹钟?

可以存在;要管理!如何管理:先描述,再组织

bash 复制代码
struct timer {
    int timeout;    // 未来的超时时间(绝对时间,如jiffies)
    pid_t who;      // 目标进程PID(标识该定时器属于哪个进程)
    void (*callback)(); // 超时触发的回调函数(如发送SIGALRM信号)
    // 其他链表/堆节点指针、优先级等字段
};

当操作系统管理了闹钟之后:就能实现

快速插入新定时器(进程调用 alarm() 时)

快速查找 / 删除到期定时器(时钟中断时)

高效遍历所有定时器,检查是否到期

我们可以把描述的这个结构,理解为最小堆 注意:这里是为例便于理解,操作系统实际并没有用堆结构管理定时器

堆顶元素永远是最早到期的定时器:每次时钟中断只需检查堆顶,若堆顶未到期则所有定时器都未到期,无需遍历全部;如果堆顶超时了,那就查看下一个

4 键盘怎么向目标进程发送信号

我们先构建概念

操作系统怎么知道键盘上有数据了?

硬件中断!!(后面有一个专题讲)

外设向CPU发送中断,CPU暂停手中工作,转而执行操作系统中代码

简单理解硬件中断:假设你现在 在外面,不知道你的舍友在不在宿舍,想知道的话,最直接的方法就是打电话--->这里的打电话就相当于硬件中断、

(1)如何理解键盘输入:键盘就是基于硬件中断进行工作

(2)操作系统如何解释快捷键:把ctrl+c,ctrl+v.....可能直接解释成信号

(3)操作系统怎么知道信号应该发送给哪一个进程

我们先补充一部分知识:软件方面的进程组和作业

会话(Session)

  • 对应图中的session,是用户登录后创建的最高级别的进程集合。
  • 一个会话关联一个控制终端 (图中的终端文件),用户通过这个终端和会话交互。
  • 会话里可以有多个进程组,同一时间只有一个前台进程组可以直接和终端交互(图中写了 "前台进程 (组) 只有一个!!!")。

进程组(Process Group)

  • 对应图里的group是一组关联的进程集合 ,通常由管道命令创建(比如ls | grep txt,两个进程属于同一个进程组)。
  • 每个进程组有一个组长(进程组 ID = 组长 PID),shell 会把前台的进程组设为当前会话的前台进程组。

终端(Terminal)

图中的终端文件,是用户输入输出的接口 (比如你用 xshell 登录后,那个窗口对应的设备文件)。

所有来自键盘的信号(Ctrl+C、Ctrl+Z)和输入数据,都会发送给前台进程组,而不是后台进程

Shell(bash)

  • 图里的bash,是会话的创建者和管理者
  • 用户登录时,shell 会创建会话,成为会话首进程(session leader)。
  • 每次执行命令时,shell 会创建新的进程组,前台运行的进程组会抢占终端,后台运行的进程组无法直接和终端交互。

作业(Job)

  • 作业就是 shell 对进程组的管理抽象:
    • 前台作业:就是当前的前台进程组,独占终端。
    • 后台作业:后台运行的进程组,用&启动,或者用Ctrl+Z挂起后放到后台

图示过程解析:

用户通过 xshell 建立连接,系统为用户创建会话,打开对应的终端设备文件。bash 成为会话首进程,绑定控制终端。用户在 bash 里输入命令(比如sleep、管道命令),bash 会创建新的进程组(作业)
前台命令:进程组成为会话的前台进程组,接收终端的输入和信号。
后台命令:进程组作为后台作业运行,无法直接接收终端信号,也无法读取终端输入。

终端信号路由:Ctrl+C、Ctrl+Z 这些信号,只会发送给前台进程组里的所有进程,后台进程组收不到

ctrl+c只能用来终止前台进程:因为终端的信号只会发送给前台进程组,后台进程组和终端的交互被 shell 屏蔽了

进程组允许是一个进程,作业是由进程组完成的

会话内部至少包含一个进程组,是进程组的继承,通常有属于自己的终端文件

在一个会话中,任何时刻,只允许一个进程(组)在前台--->那么其他命令就无法执行了吗?

前台进程只是暂时占用当前终端的交互权,你可以通过后台运行、挂起、多终端等方式,在不终止前台进程的情况下继续执行其他命令,完全不会 "无法操作"

什么叫做前后台:可以直接获取用户输入的叫前台进程,否则叫后台进程

谁拥有终端文件(主要是键盘),谁就是前台

后台可以向显示器打印,无法从键盘获取数据

ctrl+C是以进程组为点位杀死进程的,所以有时候会杀死多个进程

相关推荐
DBA大董2 小时前
TDengine3.x 数据文件详解
大数据·linux·时序数据库·dba·tdengine
啊哦呃咦唔鱼2 小时前
LeetCode双指针合集
算法·leetcode·职场和发展
生万千欢喜心2 小时前
Linux 安装金蝶天燕中间件 AAS-V9.0.zip
java·linux
lKWO OMET3 小时前
查看 nginx 是否已经启动
运维·数据库·nginx
WolfGang0073213 小时前
代码随想录算法训练营 Day37 | 动态规划 part10
算法·动态规划
baizhigangqw3 小时前
启发式算法WebApp实验室:从搜索策略到群体智能的能力进阶(二)
算法·启发式算法·web app
alphaTao3 小时前
LeetCode 每日一题 2026/4/13-2026/4/19
算法·leetcode·职场和发展
执笔画流年呀3 小时前
多线程及其特性
java·服务器·开发语言
灵智实验室3 小时前
PX4姿态解算技术详解(四):姿态更新/递推与共锥补偿
算法·无人机·px 4