信号处理详解

在我们的生活中到处都可以看见信号,比如:烽火戏诸侯、飞鸽传书、古希腊马拉松送捷报,现在的QQ、微信、还有你被crush拉黑的哥谭感叹号等等等。那么,在我们计算机体系中,信号是如何产生、发送和处理的呢?信号的整个生命周期:信号产生、信号保存、信号处理。本文,将会详细介绍信号是什么。

目录

1、信号的产生

(1)kill命令

(2)键盘发送信号

(3)系统接口

(4)软件信号

(5)异常

2、信号保存

3、信号处理

4、信号捕捉

5、内核态 VS 用户态

6、操作系统是如何运行的

7、代码模拟信号处理


在操作系统中,一共有31个普通信号

信号编号信号名称描述
1SIGHUP挂起信号,通常用于指示配置文件重新加载。
2SIGINT中断信号,通常由用户通过 Ctrl+C 产生。
3SIGQUIT退出信号,类似于 SIGINT,但会生成核心转储。
4SIGILL非法指令信号,表示进程执行了无效的指令。
5SIGTRAP跟踪陷阱信号,用于调试。
6SIGABRT异常终止信号,通常由 abort() 函数触发。
7SIGBUS总线错误信号,通常由于硬件故障或对未对齐地址的访问。
8SIGFPE浮点运算异常信号,表示算术运算错误。
9SIGKILL强制终止信号,无法被捕获、阻塞或忽略。
10SIGUSR1用户定义的信号1,通常由用户定义的处理程序处理。
11SIGSEGV段错误信号,表示无效的内存访问。
12SIGUSR2用户定义的信号2,通常由用户定义的处理程序处理。
13SIGPIPE管道破裂信号,表示写入关闭的管道。
14SIGALRM定时器到期信号,通常由 alarm() 函数设置。
15SIGTERM终止信号,通常用于请求进程终止。
16SIGSTKFLT栈溢出错误信号(仅在某些系统中可用)。
17SIGCHLD子进程状态更改信号,通知父进程子进程状态的变化。
18SIGCONT继续执行信号,继续一个被暂停的进程。
19SIGSTOP停止信号,类似于暂停进程,但不能被捕获、阻塞或忽略。
20SIGTSTP终端停止信号,通常由用户通过 Ctrl+Z 产生。
21SIGTTIN后台进程读取终端信号,通常由后台进程尝试读取终端时触发。
22SIGTTOU后台进程写入终端信号,通常由后台进程尝试写入终端时触发。
23SIGURG紧急数据到达信号,用于异步通知网络数据到达。
24SIGXCPUCPU 时间限制超出信号,通常由超出 CPU 时间限制触发。
25SIGXFSZ文件大小限制超出信号,通常由超出文件大小限制触发。
26SIGVTALRM虚拟定时器到期信号,通常用于用户定义的定时器。
27SIGPROF统计定时器到期信号,通常用于分析进程的执行时间。
28SIGWINCH窗口大小变化信号,通常由终端窗口大小变化触发。
29SIGIO异步 I/O 信号,通知进程 I/O 操作准备就绪。
30SIGPWR电源故障信号,通常由电源问题触发。
31SIGSYS错误的系统调用信号,表示非法的系统调用。

信号处理:
默认动作、忽略动作、自定义处理---信号捕捉
进程处理信号,都是默认的---默认动作通常指向终止自己 / 暂停 / 忽略

信号的记录
信号的发送之后,进程不一定即时处理,而是在合适的时候处理
所以进程需要记录收到的信号,在PCB的一个变量专门用来保存收到信号
uint32_t signals;//记录收到信号变量,以位图方式记录(只有1-31种信号)

发送信号:修改指定进程PCB信号位图,0->1,即信号发送


1、信号的产生

(1)kill命令

kill -num 进程pid:给进程发送信号

(2)键盘发送信号

2号信号SIGINT:ctrl + c,终止进程;

3号信号SIGQUIT,:ctrl + \ ,终止进程

(3)系统接口

#include <signal.h>
int kill(pid_t pid, int sig);

pid: 目标进程的进程 ID。
sig: 要发送的信号类型

#include <signal.h>
int raise(int sig);

给自己发送信号,当前进程

#include <stdlib.h>
void abort(void);

abort 函数通过发送 SIGABRT 信号给当前进程来强制终止程序。6号信号
作用:如果进程屏蔽信号,则进程无法被影响,所以保留6号信号,强制终止
同时还有,9号信号不允许自定义捕捉,只能killed 进程

信号的发送,只能由操作系统执行
例如键盘是硬件,硬件由操作系统识别组合产生信号,并发送对应信号,执行信号

(4)软件信号

13号信号:SIGPIPE,读关闭了,但写还在进行,管道会被发送该信号,终止管道进程
14号信号:SIGALRM,警告

闹钟函数

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
alarm 函数用于设置一个定时器,在指定的时间后发送 SIGALRM 信号给当前进程。
闹钟默认只触发一次
alarm(0);取消闹钟,返回值上一个闹钟的剩余时间

为什么关机之后,电脑时间还是正确的
关机后电脑时间仍然正确,是因为大多数电脑使用了一个名为实时时钟(RTC)的硬件组件。RTC 是一个内置于计算机主板上的小型电池供电时钟,它能够在电脑关机或断电时继续运行。这种电池,通常是纽扣电池(如 CR2032),能为 RTC 提供持续的电力,确保时钟持续准确。
当电脑重新开机时,操作系统会从 RTC 中读取当前时间并进行同步,因此你看到的时间通常是准确的。这种机制确保了电脑在关机期间不会丢失时间信息。

(5)异常

程序为什么会崩溃,崩溃了为什么退出?
因为非法操作,导致操作系统给进程发送信号
 除零(浮点数错误):8号信号---SIGFPE
野指针:11号信号,INGSEGV

操作系统怎么知道进程异常?
CPU怎么知道我的运算是否是错误的?
eflag寄存器记录溢出标记位,通过标记位识别异常

异常为什么退出?可不可以不退出?(可以不退出,但是不推荐)
如果不退出,当进程切换时,错误信息依旧会被记录保存,保存上下文数据到寄存器
当进程恢复运行时,寄存器又加载上下文数据,此时进程依旧是错误数据
所以进程退出是释放上下文数据,恢复溢出标志位等异常信息
野指针问题:野指针访问错误信息放在CR2寄存器
CR2 寄存器是 x86 架构中的一个控制寄存器,用于存储当前发生的页错误(Page Fault)异常时的虚拟地址。当处理器遇到内存访问错误(如访问无效或保护的内存区域)时,会将相关的虚拟地址保存到 CR2 寄存器,以帮助操作系统处理该错误。这个寄存器在调试和错误处理时特别重要。

异常终止,会出现两种处理情况:

一个是term模式:异常终止
一个是core模式:异常终止,同时形成一个debug文件,把进程异常的镜像数据保存到磁盘中,此机制叫核心转储,云服务器默认关闭这个功能,可以使用命令:

unlimit -c 10240:打开权限

云服务器为什么关闭核心存储?
因为如果进程一直错误,就会一直生成debug文件,占据太多内存,服务可能就会崩掉

为什么要有核心转储?
core文件,用来协助debug调试错误
直接在debug下查看core文件的错误信息
(AI搜)

标志进程退出信息的32位图,第8位标识进程退出是否以core方式终止

子进程诗不会走到后面的
二进制获取:和11111,即0xFF按位与

2、信号保存

实际执行信号的处理动作成为信号递达
默认、忽略、自定义捕捉
信号从产生到抵达之间的状态,称为信号未决(pending)
进程可以选择阻塞(Block)某个信号
信号阻塞意味着一直未决,永不递达,知道阻塞接触
阻塞信号就不会递达
阻塞信号集合,也叫做当前进程的信号屏蔽字(signal mask)

在进程结构体内部,有三个变量数组用来记录标识:block、pending、handler
这些数组都有32位

pending是一个int类型,使用位图的方式
比特位的位置:代表信号编号
比特位的内容:代表信号是否收到

handler是一个函数指针数组:信号的编号,就是数组的下标,可以使用信号编号索引信号处理方法
所以自定义信号处理函数singal,本质上是将函数指针替换掉对应信号的处理方法数组
让原本应该默认处理的函数,变成我们自定义的函数处理

block是一个int类型,也采用位图模式 0000 0000 0000 0000 0000 0000 0000 0000
比特位的位置:代表信号编号
比特位的内容:代表信号是否阻塞

三张表横着看
因此,用上述两张位图+一张函数表,即可让进程识别信号
上述操作都是操作系统在执行
所以信号的发送和接受,都是进程修改对应变量的值


信号集操纵函数:sigset等函数
sigsize_t:用户级类型,类似于int,是一个位图模式

获取bolck和handler接口:
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数
how: 这是一个整数,指示如何更改信号掩码。它可以是以下三个值之一:
SIG_BLOCK:将 set 中指定的信号添加到当前的信号掩码中。
SIG_UNBLOCK:从当前的信号掩码中移除 set 中指定的信号。
SIG_SETMASK:将信号掩码设置为 set 中指定的信号集,替换当前的信号掩码。
set: 指向一个 sigset_t 类型的信号集,这个信号集包含了要添加、移除或设置的信号。这个参数可以是 NULL,如果 how 参数是 SIG_SETMASK,则必需提供此参数。
oldset: 指向一个 sigset_t 类型的信号集,用于保存调用之前的信号掩码。如果这个参数是 NULL,则不保存旧的信号掩码。
返回值
成功时返回 0。
失败时返回 -1,并设置 errno 以指示错误原因。


#include <signal.h>
int sigpending(sigset_t *set);//获取当前进程的pending位图,输出型参数
参数
set: 指向一个 sigset_t 类型的信号集,用于存储当前阻塞的信号。
返回值
成功时返回 0。
失败时返回 -1,并设置 errno 以指示错误原因。
信号抵达之前,位图就已经被修改了
发送信号的本质就是修改pending的位图位置

3、信号处理

自定义方式
忽略方式:SIG_IGN
默认动作:SIG_DFl default

信号的捕捉流程:
信号处理可能不会即时,而是在合适的时候处理
什么叫合适的时候?
进程从内核态返回到用户态时处理
在返回时,检查进程的三张表:block、pending、handler
从上到下,所以1-31号信号
如果对应信号block不是阻塞状态,0
pending处于1,已经接受信号,需要处理
再检查handler对应信号的执行方法,如果是SIG_IGN,则忽略;同时未决信号重置为0
如果是SIG_DFl default,则默认;同时未决信号重置为0
否则就执行用户自定义的函数方法,即所谓信号捕捉
此时进程要从内核态切换到用户态,去执行用户的自定义函数
但是,不安全
因为如果直接执行用户的代码
也许用户在这部分代码中,做一些rm等高级权限的操作命令
此时,用户就相当于绕过了操作系统的权限限制
这是不安全的
所以,要返回到用户态,让用户用自己的权限去执行代码
有问题,用户自己负责

4、信号捕捉

sigaction函数,这是一个标准的信号处理函数,用于设置信号处理程序。
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
第三个参数:当你修改了一个进程的信号捕捉方法,万一你想恢复原来的捕捉方法呢?所以目的为保存
写代码建议带上struct,方便阅读

捕捉信号时:当正在处理2号信号时,就会屏蔽2号信号
当2号信号处理完成,自动接触屏蔽
即,禁止对同一个信号做递归式的信号处理
const struct sigaction *act结构体中的成员:sa_mask
作用是,当执行2号信号时,对2号信号屏蔽,但是其他信号并不屏蔽
如果你还想同时屏蔽洽其他信号,例如3
就可以指向要屏蔽的信号:sigaddset(sa_mask,3)
但是有一些信号是不可以被屏蔽的
例如9号信号

信号的产生和发送、保存、处理
事实上说发送信号不够准确,而是向指向进程发送标记位
异常先是硬件出错,操作系统再处理
信号抵达、未决、阻塞

可重入函数和不可重入函数
不要当作优缺点来看待,而是当作特点来看待
全局数据一般是不可重入的

colatile关键字
改内存的值,和cpu寄存器的值没有关系
编译器过度优化导致程序出问题
寄存器隐藏了内存的真实值
编译器优化有等级
-o0,必须保持

SIGGHLD信号
子进程退出时,会给父进程发送SIGGHLD信号

5、内核态 VS 用户态

用户级页表有很多,但内核级页表只有一份
因此,无论进程如何切换,始终能找到操作系统
访问操作系统也是在进程的地址空间中进行的,和访问库函数没有区别
仅仅只是地址的不同
可是,操作系统不相信任何人,所以访问操作系统地址时,有约束
这个约束是什么?就是只能通过系统调用访问数据
那么,到底是怎么跳转到操作系统的地址空间呢?
需要硬件配合---CPU,寄存器cs,code semgment,通俗理解为代码区范围
在cs寄存器内有一个比特位,为0时只能执行内核代码,为3时只能执行用户代码
在代码执行时,首先检查这个比特位的值,然后跳转到对应地址空间区执行
通过这个标志位,也解决了对用户权限的限制和系统调用权限不一的冲突
所以,本质上执行状态的转换,就是改变对应标志位,然后识别不同的执行状态

6、操作系统是如何运行的

从开机到关机,操作系统一直再跑,说明操作系统是死循环
我们的所有的进程由操作系统执行调度,
但是又是谁来执行操作系统呢?
由硬件支持
所以操作系统是一个以硬件为依托执行的软件
其他进程可以理解为以为操作系统软件为依托的软件

在操作系统加载到内存后
会有一个函数指针方法表
这个表对应不同的硬件,例如键盘、鼠标、屏幕等
例如,CPU如何知道键盘什么时候输入?
难道要CPU一直等待吗?
不可能,因为还要执行其他进程
所以,当键盘要输入的时候
会向操作系统发送信号
提示操作系统:“我键盘正在输入,需要处理”
此时,CPU每隔一段时间就会检查一遍这种信号
当检测到信号时,CPU就会暂停其他工作
而转到处理当前硬件IO请求
对于进程的调度、切换也是基于这种原理,但是其中还涉及时间片、时钟周期等概念的综合

那么如何理解系统调用:
在操作系统内部,死循环执行的过程中,会有一个调度机制
在操作系统代码中,有一个系统调用指针表
是一个数组
数组的下标对应不同的系统调用
下标号叫做系统调用号
当我们调用系统调用时
实际上是把系统调用号放置到CPU的执行寄存器
然后执行

7、代码模拟信号处理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

二十5画生

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值