【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是以进程组为点位杀死进程的,所以有时候会杀死多个进程

相关推荐
逻辑君9 小时前
Foresight研究报告【20260011】
人工智能·线性代数·算法·矩阵
珊瑚里的鱼9 小时前
【动态规划】不同路径Ⅱ
算法·动态规划
printfLILEI9 小时前
php中的类与对象以及反序列化
linux·开发语言·php
zyl837219 小时前
Docker 使用手册
运维·docker·容器
适应规律9 小时前
【无标题】
人工智能·python·算法
蒟蒻的贤10 小时前
关于文法G2算符优先分析的一个坑
算法
古月方枘Fry10 小时前
MGRE实验
运维·服务器
变量未定义~10 小时前
单调栈、单调队列(模板)、子矩阵、连通块中点的数量、堆箱子(4星)
算法
博客-小覃10 小时前
Zabbix之华为交换机的日志记录信息操作详细教程
服务器·网络·华为·zabbix
叠叠乐10 小时前
redmi k90 pro max 强解BL,刷海外rom, 并刷入sukisu ultra
linux