Linux:信号的发送、保存和处理

一、信号的发送

什么是信号的发送??

与其说是给进程发送信号,倒不如说是给进程的PCB结构体发信号

1、比特位为0或者为1,表明是否收到。

2、比特位的位置是第几个,表明的是信号的编号。

3、所谓的"发信号",本质就是OS去修改task_struct的信号位图对应的比特位。所以其实是"写信号"!!

------>这也就是为什么信号必须由OS发送,因为OS是进程的管理者!!只有它才有资格修改task_struct内的属性!!

问题:PCB内部采用位图来接受普通信号,可是如果我发送了很多次相同的信号呢??你的位图是能保存一次怎么办??

------> 本来这种设计方案就只能保存1次,假设你一直没能处理该信号,而又接受了很多次该信号,那么其实也只能算一次,其他的就会丢失掉。这就好比你妈喊你吃饭,不管他喊了你多少次你没有下去,但是最终你其实都是会下去吃饭的, 而如果是实时信号,就必须得立即处理了,发了几次就得执行几次,不能丢失!

二、信号的保存

为什么需要有信号保存??

------>因为进程收到信号后,可能不会立即处理这个信号,所以就需要有一个时间窗口

2.1 信号的一些相关概念

1、实际执行信号的处理动作称为 信号递达 (handler表)

2、信号从产生到递达之间的状态,叫做 信号未决 (pending表)

3、进程可以选择阻塞 某个信号 (block表

4、被阻塞的信号产生时将保存在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作

5、阻塞和忽略是不同的(未读和已读不回),只要信号被阻塞就不会递达,而忽略是递达之后可以选择的一种处理动作!!

2.2 内核中的表示

进程内部有关信号部分维护了3张表,block表(被阻塞的信号)和pending表(接受但未处理的信号)是位图结构,而handler表是函数指针数组表示处理动作。

SIG_DFL : 默认动作

SIG_IGN:忽略

如果我们用户捕获信号设置了自定义方法,就可以将该方法的函数指针填到handler表中

2.3 sigset_t

三张表都是OS内部的内核数据结构,所以你用户想读或者是想改就必须由系统调用接口!! 关键是获取这些表也存在位图,所以这也就意味着我们需要在用户层和内核层之间进行数据拷贝(参数设计上需要有输入型参数和输出型参数)。然后他就设计出了一个信号集数据类型sigset_t ,本质上就是被封装起来的位图结构

问题:为什么要这样设计呢??sigset_t本质上部就是一个unsigned int类型吗??

------>(1)这样设计后期更具有拓展性 (2)防止你随意进行位操作 (3)跨平台保证可移植性

2.4 信号集操作函数

2.5 sigprocmask和sigpending

调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。

sigpending:读取当前进程的未决信号集(pending),通过set参数传出。

尝试屏蔽2号信号 打印pending表

尝试先屏蔽2号信号 然后再解除

问题:那我们如果将所有的信号都进行屏蔽,信号不就不会被处理了么??

------>OS在忽略的时候对9和19号信号防了一手,那么自然就也会在屏蔽信号这里防止9和19号被屏蔽!

三、信号处理

信号是什么时候被处理的??

------>当我们的进程从内核态返回到用户态的时候,进行信号的检测和处理!

3.1 重谈进程地址空间

解析:

(1)我们之前所谈到的进程地址空间大多数谈的都是用户区,但是从3GB------4GB的位置是内核区,内核区也有一张自己的内核级页表。映射到OS的代码和数据(打开OS的时间就预先加载起来了)

(2)调用系统调用的本质就是OS自动做"身份切换",从用户态进入内核态,转化为汇编叫做int 80 (陷入内核)

问题1:我怎么知道当前访问的是用户层还是内核层??

------>CR3寄存器存储的是页表的地址,而ecs寄存器中有两个bit位表示当前处于内核态还是出于用户态(00表示内核态 11表示用户态) 用来帮助OS判断当前访问了什么态,如果当前是用户态但是却访问了操作系统的代码和数据,就会拦截你!

问题2:用户页表有几份?内核页表有几份??

------>用户页表,有几个进程就有几份,因为进程具有独立性,而内核级页表只有一份!!

------>说明每一个进程看到的3-4GB的东西都是一样的!!整个系统中进程再怎么切换,3-4GB映射的内容是不变的!

问题3:进程和OS的视角是怎样的?

------>(1)进程视角:我们调用系统重的方法,就是在我自己的地址空间中进行的!

(2)OS视角:任何时刻都有进程执行,我们想执行OS代码就可以随时调度去执行!

问题4:OS调度进程的执行,可OS也是一个进程啊,那谁来调度他呢??他的本质是什么??

------>操作系统的本质是基于时间中断的一个死循环!

------>在计算机硬件中,有一个时钟芯片,每隔很短的时候,向计算机发送时钟中断。由该硬件来督促OS的执行

问题5:时间芯片是如何督促OS的??

------>首先OS在被启动的时候,必然会先初始化一些必须的资源。在那以后他就会在那不断调用pause函数在那等芯片每隔一段时间来驱动自己!

------>每当时钟响了以后(芯片发送时间中断信号),OS就会执行一些被规定好的检查工作,比如说看看当前正在被调度的进程的时间片是否到了,如果到了就把他从cpu上剥离下来!

------>所以外部的设备的中断不一定会出现(跟外设做数据的交互),但是时钟中断必然每隔一段时间就会来一次(进行进程调度或者是其他检查工作)

3.2 信号的处理全过程示意图

存在2种情况

情况1(没有设置自定义方法):用户态(执行常规指令)------>内核态(遇到系统调用或者中断、异常之后陷入)------>用户态(返回用户态之前,处理一下当前的信号,因为是内置的方法 所以顺便处理完了正好出来 从中断的地方继续执行 )

情况2(没有设置自定义方法):用户态------>内核态------>用户态(和之前不同的是,这次自定义的方法在用户态,所以必须要先出去)------>内核态(自定义方法调用完后会自动通过sigreturn返回内核 响应信号)------>用户态(返回上次中断的地方继续执行)

问题: 通过系统调用、中断、异常进入内核态我可以理解,可如果我就是一个while循环里面也没有任何系统调用,那我是不是就不会进入内核态了??

------> 不是!! 千万要记住不光光只有系统调用、中断、异常会进入内核态,还有时钟芯片定期驱动OS!!进程是会被调度的!! 当你时间片到了被检测到的时候,你这个进程就会从cpu上被剥离下来,然后该进程的PCB会被暂时链入到等待队列中,上下文信息也会暂时被保存起来,而当你二次调度的时候必然需要将一些上下文信息恢复到cpu上,所以这个过程必然会进入到内核态中!!

------>所以我们会有无数次机会从用户态到内核态!!不用担心处理不了信号!!

3.3 信号的处理方法

sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回- 1。

sa_handler是方法

问题1:pending位图是什么时候从1->0的??

------>先清0,再调用

验证方法: 捕捉信号后,然后在自定义的方法里打印pending表

问题2:信号被处理时,对应的信号也会被添加到block表中,防止信号捕捉被嵌套使用

------>正在处理2信号的时候,会将2信号的block表屏蔽掉,这样保证在处理2信号的时候,新的2信号不可被递达, 意思就是必须要等到这个2号处理完,才能处理新的2号!

------>这是为了防止OS一直忙于某种信号的处理,从而引发的嵌套调用(因为自定义函数里可能会再次发送该信号)

------>比方说我们再准备捕捉2号信号之前,我们是先把pending由1->0,然后当我们进入自定义函数的时候还是有可能接受到新的2信号,pending由0->1,而此时如果再进入内核态,那么从内核态出去用户态的时候,当前的2都还没处理完呢,又检测到pending表的1然后接着处理新的2号,此时就会陷入处理2号信号的死循环!!

验证方法:故意在handler方法里写个死循环(意思就是捕获之后就不返回了),这样当我们第二次发送2号信号的时候,那么该信号就会被阻塞到pending表中,我们再打印出来看即可!

问题3:as_mask是什么??

------>as_mask是存放需要手动屏蔽的信号!

------>比如当前我们处理2号信号的时候,他会顺便把所有sa_mask里面bit位为1的信号也顺带屏蔽了!

四、信号所引发的其他子问题

信号具有从当前执行流直接跳转到别的执行流的能力,因此可能会引发以下的一些子问题

4.1 可重入函数

下图出问题的原因就是:本来insert应该是main函数的执行流里调用的,但**(1)insert还没调用完呢就跳转过去执行信号捕捉sighandle了,此时sighandle这个执行流又再次调用了insert** ,此时insert这个函数被重复进入了!! 加上**(2)访问的链表是全局的链表(如果是局部的不会错乱)** 所以就会因为重入引发错乱!!造成了节点丢失,内存泄漏

问题1: 如果一个函数符合以下条件之一则是不可重入的

------>(1)调用了malloc或free,因为malloc也是用全局链表来管理堆的。

(2)调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

问题2: 什么情况下一个函数会被多次进入??

------>(1)以前学cpp的时候都是单执行流,如果是多执行流的话就有可能一个函数被多个执行流进入(多进程)

(2)如上图一样,虽然只有一个进程,但是main函数和sighanle函数其实并没有调用和被调用的关系,他们是并列的关系,属于不同的执行流,前者是必然执行的,而后者是否执行取决于是否收到信号,如果收到了就会暂时停止main函数,等sighanle函数执行完了才会回来执行main ------>所以重入发生在当main恰好在调用一个函数还没返回的时候,突然停下来去捕获信号,而该信号恰好也调用了这个函数。

问题3:为什么目前大部分的函数都是不可重入函数??

------>因为我们大部分使用的函数都可能会涉及到一些容器, 比如扩容,就会相互影响!

4.2 解决过度优化的volatile

我们来看看下面的代码

这个代码按道理是个死循环不会退出,但如果flag是全局变量,我们就可以在中途通过捕捉信号来把他的值修改一下,那么就会退出了!!

但是这种情况是有可能会被做优化的:因为main和handler是两个执行流,所以当cpu发现main函数内部不对flag做修改而只是单纯读取并做逻辑运算的时候,综合以上两个条件他就会直接把flag变量优化到CPU内寄存器中方便后续的操作(通过减少数据的拷贝来提高效率),这样的话如果你再去修改flag,那你修改的就是内存里面的flag而不是进程里面的flag!

Linux的优化方案设计:

我们会发现优化方案为O1的时候,此时flag就被优化了!!

因为优化导致我们的内存不可见了!

解决方案就是用:

volatile关键字: 保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量 的任何操作,都必须在真实的内存中进行操作!

与他相反的关键字:

register建议型关键字:当前修饰的变量能放在寄存器中就放在寄存器中!

4.3 基于信号的异步等待方案

子进程在退出的时候,他并不会悄悄退出,而是在退出的时候会主动向父进程发送SIGCHLD(17)信号

验证:

既然子进程退出的时候会收到信号,那我不如等收到信号再回收,这样不就避免傻傻等待了吗?

所以我们可以把等待子进程的函数写到信号捕捉的函数里!!

问题1:可是假如有10个进程同时退出呢??你在处理1个进程的时候信号可是会被阻塞的,那就会导致8个进程的信号丢失了!!这要怎么办??

------>所以我们可以尝试在收到1个信号的时候,就开始一直尝试回收。

问题2:可是如果我们当前10个进程,只是退出一半,而另一半还得继续运行呢??那么父进程在收到信号时发现进程没有全部退出,他就会卡在信号捕捉函数里阻塞起来了,该怎么办??

------>解决方法就是采用非阻塞轮询!

因此,将回收子进程的过程放在信号捕捉函数里,并采用非阻塞轮询,可以大大提高等待的效率!

问题3: 那么以前我们并不知道有这种方案的时候,子进程向父进程发送信号,那父进程的默认动作究竟是什么??

------>SIGCHLD信号的默认动作是忽略,所以就是相当于什么都没做!

问题4:子进程会变成僵尸进程因为父进程想关心父进程的退出状态,所以他才会在那等待父进程回收,可是如果我压根就不想关心子进程的退出状态呢??我可不可以让OS直接帮我回收呢??

------>事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction将SIGCHLD的处理动作 置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证 在其它UNIX系统上都可 用。请编写程序验证这样做不会产生僵尸进程。父进程就不需要wait了!

问题5: 以前的默认动作就是忽略,那为什么我们把他捕获后再设成忽略就没有僵尸了??这到底是怎么区分的??

------>其实看起来都是忽略,但根本不一样!!其实原本是SIG_DFL,只不过他的方法恰好就是忽略而已,而我们捕获后把他改成SIG_IGN就可以区分开了!! 这样就是一种特殊的方式告诉OS你直接把子进程给回收吧,我不打算关心子进程的状态!

相关推荐
m0_541034846 分钟前
Linux shell脚本练习(五)
linux
江梦寻9 分钟前
搭建RustDesk中继服务器_
运维·服务器·网络·网络协议·cisco·思科·思科模拟器
li三河27 分钟前
ubuntu下anconda装pytorch
linux·pytorch·ubuntu
2401_854391081 小时前
SSM 寝室管理系统:住宿管理的科技之光
java·运维·科技·架构
工业3D_大熊1 小时前
3D可视化引擎HOOPS Visualize与HOOPS Luminate Bridge的功能与应用
linux·前端·c++·windows·macos·3d·c#
工业3D_大熊1 小时前
3D开发工具HOOPS对B-Rep的支持:提升3D建模与可视化的精度与效率
linux·c++·windows·macos·3d·c#·制造
Stark、1 小时前
【Linux】自定义项目-进度条
linux·运维·服务器
fegxg1 小时前
ROS Action通信解读
linux·ubuntu·机器人
我是一只代码狗2 小时前
nginx中的root和alias的区别
运维·nginx
人类群星闪耀时2 小时前
机器学习在运维中的实时分析应用:新时代的智能运维
运维·人工智能·机器学习