信号-重谈地址空间3


在这里插入图片描述
信号的产生:外卖小哥给我打电话说你外卖到了

信号的保存:我可能正在推高地,脑子里面记住我外卖到了,一会再去拿

信号的处理:我打完了,下楼把外卖拿了

完成了一次信号的生命周期

1.信号的概念搞定—输出一堆的结论,支撑我们对信号的理解

生活中 你是怎么认识信号的??
1、能识别到信号
2、知道信号的处理方法

计算机中 你指的是谁啊?
进程

进程啦
1.进程 必须识别+能够处理信号—信号没有产生,也要具备处理信号的能力—信号的处理能力,属于进程内置功能的一部分
2.进程即便是没有收到信号,也能知道哪些信号该怎么处理
3.当进程真的收到了一个具体的信号的时候,进程可能并不会立即处理这个信号,合适的时候再处理
4.一个进程必须当信号产生,到信号开始被处理,就一定会有时间窗口,进程具有临时保存哪些信号已经发生了的能力

ctrl + c为什么能杀掉我们前台进程呢?
Linux中,一次登陆中,一个终端,一般会配上一个bash,每一个登陆,只允许一个进程是前台进程,可以允许多个进程是后台进程
当你的进程启动后,bash默认变成后台进程,你的进程就是前台
区分前台和后台:前台能够获取键盘输入,不能就是后台

后台启动 :./a.out &
那后台启动的时候键盘输入 ctrl+c 没法给后台,那发给bash了,那bash怎么没退?
因为Bash对这个做了特殊处理

再回到ctrl + c为什么能杀掉我们前台进程
前提是:键盘输入首先是被前台进程收到的! 你都不是发给我的我怎么给你响应呢?
ctrl+ c本质是被进程解释成为收到了信号,2号信号意味着中断,所以它就退出了
在这里插入图片描述
什么1, 2后面跟着一堆大写SIGINT ,你应该立刻想到宏
所以在内核中就是数字

我们在基于时间片轮转的情况中只常用前31个信号

我们该如何处理信号呢?
信号的处理方式:
1.默认动作(看到红灯就在那等)
2.忽略 (生死看淡,红灯直接闯)
3.自定义动作 (红灯一亮,我就跳舞!)
我们把自定义动作这种情况称之为信号的捕捉

收到一个信号,处理方式就这三种

验证ctrl + c确实是收到2号信号才退出的
进程收到2号信号的默认动作,就是终止自己,我们现在用signal更改2号信号的动作看看ctrl+c到底是不是发的2号

系统调用 signal

用来修改当前调用进程对于特定信号的处理动作
signum 修改几号信号
typedef void(*sighandler_t)(int) 定义了一个函数指针类型sighandler
将某信号的处理动作修改为指针指向的方法

在这里插入图片描述
在这里插入图片描述
现象:按下ctrl +c确实是2号信号,2号信号被我们更改了处理方式
当ctrl+c进程不退出了改为我们自定义动作,这是很正常的,信号的处理方式(默认,忽略,自定义)只能三选一
在这里插入图片描述
问题:
1、signal设置位置问题
在这里插入图片描述
signal 只需要设置一次,往后都有效

2、这个自定义myhandler函数是我们调用signal就调用了?还是我们后面执行时才调用的?
也就是问myhandler函数什么时候被调用?
设置自定义动作以后如果没有对应信号产生,那这个方法便不会被调用,只有产生了 对应的信号,这个方法才会被调用。
这不很正常嘛

3、我把31个信号全设置自定义捕捉,但我就是不退,那OS没有人能杀掉我了
有些信号可以自定义,有些信号就不能

4、myhandler函数中为什么要传入int signo呢?
情景: 当多个信号都用同一个方法处理
在这里插入图片描述

发信号调用处理函数时我怎么知道是收到几号信号调用的呢?
所以需要这个参数


键盘数据是如何输入给内核的,ctrl+c又是如何变成信号的—谈谈硬件了
键盘被摁下,肯定是OS先知道!
OS怎么知道键盘上有数据了??
在这里插入图片描述
OS开机时就已经被加载到内存中,根据一切皆文件,内核中键盘文件strut file 也有对应的缓冲区
读取键盘数据本质就是把键盘上的数据拷贝到文件页缓冲区中,通过文件描述符0读到用户层读上来

OS要拷贝前提是要知道键盘上有数据才能考,那它怎么知道有数据了?
最简单的方式就是定期检测一下键盘,可是外设很多,对OS来讲是非常大的负担
在数据层面CPU不和外设打交道,但是在控制层面CPU是要能读取外设的
CPU上有很多针脚,键盘在物理上间接和CPU直接能连到的。CPU虽然不从键盘中读数据,但是键盘是可以在硬件上给CPU发送硬件中断,OS进行对应的工作时,一但键盘有数据,键盘会通过硬件单元把键盘信息发送给CPU,来让OS完成拷贝
在这里插入图片描述

可是外设一多,都给CPU发中断,我CPU怎么知道是哪一种设备呢?
所以每一种中断都有中断号,假设键盘的中断号是1,未来对应键盘有数据就绪了,它会直接和CPU链接的针脚把自己的中断号传给CPU,给CPU发送高低电平,CPU就知道了
当CPU识别到了键盘上有数据,目前仅仅是硬件CPU知道键盘上有数据,并没有在数据上进行从键盘拷贝给内存。

CPU中有寄存器,可是寄存器凭什么能保存数据呢??
对CPU充放电的过程,通过键盘给CPU发送高电平,CPU把一号针脚解释成0001,32个bit位对应32个硬件单元,把一个硬件单元充成高电位,所以此时CPU内就有数据了
在配上类型在软件上就是计算机里面的数据了

在这里插入图片描述

1、今天假设键盘中断号是10,所以键盘通过中断单元对CPU针脚充放电让CPU寄存器有中断号10,完成了键盘向CPU通知自己数据已经就绪这件事
2、软件层面上,在OS系统内比较靠前的位置,开机启动时给我们形成一张中单向量表,向量是啥?不就是vector,数组吗
所以中断向量表就是个数组,由OS维护的。这个表里面都是方法的地址,主要包括直接访问外设的方法(主要是磁盘,显示器,键盘等),这批方法是在OS内自主实现,开机就有的。
所以也可以理解为函数指针,它会指向OS中某个位置对应某个方法

键盘一但有数据就绪按回车了,键盘立马通过CPU针脚给CPU发送硬件中断,硬件中断通过充放电的形式被CPU记录下来,中断号10到CPU中,你都把CPU中断了CPU都帮你做这个事情了,CPU也顺便让OS来读取这个数据了,所以OS就以中断号索引中断表中找对应的方法,中断号就是数组的下标,所以找到之后紧接着执行这个读取键盘的方法,这个方法才是真正意义上把数据从外设拷贝到内存中的方法

从此往后OS再也不用关心任何外设了,因为每个外设都会给他不重复的中断号,每个外设都在中断向量表中注册了对应的读取方法
所以外设数据就绪了,OS直接执行中断方法把外设数据拷贝到内存里

我们学习的信号,就是用软件方式,对进程模拟的硬件中断
信号也会有这样的表,后面说
OS要拷贝前提是要知道键盘上有数据才能考,那它怎么知道有数据了?
所以OS知道了键盘上有数据啦
知道之后就把对应的数据如果是abc,213直接把这样的数据从硬件拷贝到键盘文件缓冲区上层就读到了
如果键盘上是ctrl + c这样的组合键呢?
所以当OS在进行拷贝之前会判断输入的是数据还是控制(ctrl +c转换成2号信号发送给进程)它并不把ctrl + c放到缓冲区里
所以你的进程就收到了2号信号

总结:
键盘是基于硬件中断进行工作的

以前学文件时,我打开一个文件开始读取数据了,把磁盘数据往内存里读
进程如果读磁盘,那磁盘就要去扇区寻址,进程就要等待,当磁盘找到数据时,那OS怎么知道磁盘已经找到了呢?磁盘数据就绪也会向CPU发送中断,所以OS把进程从等待变为运行状态,然后进程再执行read就可以读数据了
当我们在进行读取时,OS怎么知道读完了?磁盘在拷贝完成时也会给CPU发送中断
所以整个OS就是不断接受外部中断来处理外设的过程,就不用自己每次检查所有设备了


现象:为什么当有进程死循环向显示器打印时输入命令是乱的但是ls和pwd命令还能正常显示?
在这里插入图片描述

一切皆文件,显示器,键盘都有struct file 和对应自己的缓冲区
所以当键盘输入时,输入到键盘缓冲区
如果要把输入的数据显示出来,就把缓冲区中的数据给显示器缓冲区拷贝一份,这就叫回显
如果今天其他进程向显示器写入,它是向显示器缓冲区写入,最后打印到显示器上
键盘和显示器是不同的文件,用不同的缓冲区
当有进程死循环向显示器打印时输入命令是乱的但是ls和pwd命令还能正常显示,因为你在输入时,你输入的ls在键盘缓冲区再会回显到显示器中,打印时打乱了但是并不影响键盘缓冲区,键盘缓冲区是有序的ls,所以OS拿到了这个ls,所以执行命令并不影响

异步:信号的产生和我们自己的代码是异步的,你发你的,我发我的
也就是我不用等待信号到来一起如何如何,我就继续执行我的代码

2.信号的产生—重点

一.键盘组合键

ctrl + c : 2 (中断)
ctrl + \  : 3 (退出)
不是所有的信号都是可以被signal捕捉的,比如:9(终止),19(暂停)

二.kill命令

kill -signo pid

三.系统调用
在这里插入图片描述
1、系统调用 kill
pid 进程pid
sig 信号几
向一个进程发送信号

我们可以用这个系统调用写一个自己的kill命令了

2、raise 函数封装kill
给调用者发送指定信号 相当于kill ( getpid() , signum )
在这里插入图片描述
3、abort 函数封装 kill
在这里插入图片描述
调用abort函数会确保进程一定被终止,这和直接发6号SIGABRT信号不一样,发6号如果被捕捉会执行自定义动作

上面捕捉2号信号改为输出一句话的代码,main函数里是死循环打印
有个细节,就是当我们ctrl+c后 自定义动作输出 后续就继续运行
abort函数里面确实发了6号信号,捕捉后,后续它也多做了些工作 确保进程退出

这就告诉了我们
信号部分有自己特定化的定义 man 7 signal 有说明

总结:
信号产生的方式很多, 但是无论信号如何产生,最终一定是谁发送给进程的?
OS
为什么?
OS是进程的管理者!!

四.异常

一个进程异常了,本质上是该进程收到了信号

1、除0错误

在这里插入图片描述
它收到了哪一个信号?
查kill -l 可以看到有个SIGFPE 很符合,那怎么验证一下就是8号信号呢?
你可以自定义捕捉8号信号,当收到8号输出一句话则证明了确实除0会收到8号
自定义捕捉更像是一种注册
在这里插入图片描述

细节:
1.为什么进程不退出了?
因为捕捉到了8号让它输出一句话,不再执行默认处理方法,所以进程没退出(看到pid还在,状态不是Z)
在这里插入图片描述
2.但是这个捕捉写的可不是死循环,但他没有捕捉一次,而是在这里死循环打印捕捉方法
信号为什么会一直被触发?

2.野指针

同样是收到信号 异常了,11号 SIGSEGV,同样的捕捉证明确实是收到了11号信号
在这里插入图片描述
同样的死循环打印捕捉方法,收到异常信号进程不退出了
今天进程出异常了,我们自定义捕捉可以让它不退出,但是不退出也没啥意义最终还是要退出,你想修复一下那想得美。

问题总结:

为什么/0,野指针会给进程发送信号让进程崩溃呢?
/0野指针引起系统问题,OS识别到了这两个问题,OS会给进程发送信号,进程对于信号的默认动作就是终止自己,所以他就崩了

为什么OS能检测到/0 or 野指针?我的代码里除0了野指针了你OS怎么知道的?
1、\0错误
在这里插入图片描述

CPU怎么知道当前执行到了进程代码的哪一行呢?
eip/pc(point code) 会从上往下扫描代码
cpu有很多寄存器,数据计算寄存器:有的寄存器是用来计算的:eax ebx…
状态寄存器status(硬件):是描述CPU当前计算状态的
状态寄存器按照比特位设置标记位,有一个标志位是溢出标志位
a/0 肯定溢出了,溢出标志位由0为1 设置为有效,这是状态寄存器硬件上直接表现出来的。

整个CPU内部很多数据属于当前进程的上下文
CPU寄存器硬件只有一个,但对应CPU寄存器里面的数据可以有无数份,硬件只有一套,多个进程被调度期间,cpu寄存器里面放着当前进程的上下文
在这里插入图片描述
当前进程状态寄存器出异常了属于当前进程上下文,不影响其他进程,其他进程会把它自己的上下文放到cpu。
虽然我们修改的是CPU内部的状态寄存器,但是只影响当前CPU上运行的进程!
从调度和硬件层面上保证了进程之间的独立性
为什么要说这个?
不要认为硬件出错了,整个计算机就出问题了,所有用户的任务被进程包裹的,任何异常只会影响进程本身并不会波及至操作系统。
在这里插入图片描述
CPU里面的状态寄存器属于计算机组成,进程上下文和进程调度属于OS,我们把不同学科之间的信息打通

所以OS当前是如何知道除0了呢?
cpu内状态寄存器本次进程运行时,状态寄存器溢出了,OS要不要知道CPU出现溢出了,计算出错了呢?cpu调度本进程时直接出错了,OS要不要知道你果然出异常了?
OS必须得知道,因为OS是硬件的管理者!
CPU也是硬件,CPU能收外部的中断也能收自己的中断,一旦出异常代码就不要往后走了,OS你连最重要的硬件状态你都不知道那你玩什么呢?OS得保证硬件不出问题
OS发现CPU溢出了,怎么办?OS直接向进程发送信号,然后进程收到信号进程你自己崩溃吧

这就是OS为什么知道我们除0错误的原因。

那野指针呢?
在这里插入图片描述

页表是一个软件结构,页表当中虚拟到物理地址映射,查表的过程并不是OS直接来查,有人说KV还废时间?是的,LInux所有对应的寻址都要做转化,这个工作必须非常快
所有页表查表的过程是由MMU:内存管理单元 来完成的 (MMU集成到CPU中)
cpu里读到的都是虚拟地址,出CPU时,1.拿到页表 2.读到虚拟地址 3.结合MMU硬件
三位一体出CPU时直接是物理地址
你想访问指针的内容你得先找到物理地址,得虚拟转化物理成功后才能读和写,当我们出现野指针时,页表转化时因为权限或根本没有映射时,野指针时本质是虚拟到物理的转化肯定失败,一旦转化失败MMU会报错和CPU内部还有一个寄存器会把虚拟到物理地址转化失败所对应的地址放到寄存器里面,相当于CPU也有硬件报错被OS识别到,然后OS就一直给进程发信号了

OS它怎么知道 你是溢出还是野指针呢?
因为对应的是不同CPU寄存器里面的硬件报错,CPU就能检测出来,不同的硬件一个状态寄存器,一个虚拟地址寄存器

同样的野指针虚拟地址转化失败硬件报错也只在当前调用进程上下文中有效,进程会带走自己出错的上下文,不会影响其他人

所以刚刚为什么会死循环打印捕捉方法呢?
今天我们捕捉了对应信号,我不崩溃了,你不让退意味着当前进程异常时要一直被调度运行,进程不退,异常以后也无法继续往后跑代码,只能是CPU中进程上下文还有硬件错误,把进程直接切走了,下回你在放回cpu上硬件错误一直存在OS继续报错发信号,如此循环
进程出异常,他就应该退出
捕捉信号不是为了让你去解决问题,尤其是异常,而是提供一种渠道让用户知道你为什么死掉的。
OS系统采用了这种发信号温和的方式,让你有机会做一些保存数据的收尾工作,而不是遇到异常直接把进程干掉,防止丢失数据OS背锅

五、软件条件

异常,只会由硬件产生吗??
比如之前学习的管道,写端正常写,读端直接关闭,OS有办法检测到这种情况直接给进程发送SIGPIPE信号杀死进程,因为这种情况下写入管道已经没有人来读了就没有意义OS系统不做无意义的事情
进程文件描述符和管道文件都是OS提供的,所以这是软件引发的异常
在这里插入图片描述

闹钟

系统调用 alarm 他不是异常就是设了个闹钟时间到了发个14信号而已
在这里插入图片描述
参数 设置几秒以后闹钟
返回值 上一次闹钟响了还剩多少秒
在这里插入图片描述

我们应该怎么去理解这个闹钟呢?
好,同学们,这个理解闹钟,这个闹钟本身不是我们的重点,但是我把原理给大家说,大家听一下啊,同学们。你的进程可以通过系统调用设定闹钟。另一个进程是不是也可以通过进程设置闹钟,十个进程,那么20个进程,30个进程,每一个进程是不是都可以使用我们的alarm系统调用来让操作系统给他设个闹钟,所以操作系统当中是不是一定会存在大量的闹钟设定?所以操作系统当中是不是一定要设定大量的我们对应的闹钟,对不对?所以OS现在要不要管理闹钟呢,来,所以同学们那么他要怎么管理呢?所以每一个闹钟是不是都在内核当中?都是一个struct啊,比如说叫做alarm这样的结构体,然后呢在操作系统层面上,他只需要将所有的struck alarm这样的结构的对象啊用链表了,用各种结构管理起来,所以所谓的闹钟管理就变成了对链表的增删查改,所以你下来如果想了解的话,你要做的工作只是在内核当中去找啊,我们对应的alarm结构体在哪里?结构是什么?匹配的算法是什么就完了,很简单吧,对不对?下面呢我要再问大家啊,同学们来。可是我闹钟如果,但这个闹钟结构体应该有什么呢?好,同学们,你觉得这个闹钟结构体它的属性里应该有什么啊?第一,闹钟结构体里至少应该要有对应的进程控制块的指针,那么来表明该闹钟是哪个进程设计的,所以闹钟里面呢它可以包含我们对应进程的pcd的pid或者是PCB的指针,是不是一旦闹钟响了,进程就快速的找到了哪个进程好没问题吧?啊,第二个时间,同学们如果让你自己设置闹钟,你对应的我设了个五秒的闹钟,请问设闹钟的时候,这个alarm它的底层实现当中一定在我们对应的闹钟对应的结构体里,是不是一定要带上闹钟超时的未来时间,所以我们在构造这个闹钟结构体的时候,这个闹钟结构体里一定有它自己未来的time out时间,同学们,你觉得这个time的时间用什么时间会比较好?我看到同学们还能不能想起来?我们以前讲过的一些概念啊,同学们好啊,同学们,有没有同学告诉我这是你觉得在操作系统层面上,我们想要知道一个闹钟,他在未来什么时间点要触发,我们用什么时间比较简单呢诶,非常ok时间戳好,同学们理解了啊,懂了啊,所以我们其实在我们的操作系统内部呢,实际上对每一个我们对应的闹钟结构里带一个叫做时间戳啊,,你可以按秒或者是按微秒纳秒,你想要多大精度的闹钟都可以,那么我们可以定一个什么ut64这样的变量,然后把所有对应的时间我们全部记录下来,你也可以采用结构体记录它的秒和纳秒或者微秒都可以,反正呢对于我们来讲呢我们是,可以带上时间戳的,一旦带上时间戳了,所以我们要设定闹钟,要填充闹钟结构体属性很简单,获取当前进程时间,我们讲过啊,获取当前这个时间讲过,我们说过了啊,啊,系统的时间,我们当时在讲日志的时候说过怎么获取了啊,那么说明系统它会维持时间,我这时间了,我们把时间考一份,然后再加上你传的参数,这不就是未来时间吗?那么所以我们系统里充满了大量的闹钟,那么所有的闹钟如果用链表管理起来,你操作系统他怎么知道超时了呢?很简单嘛,因为我们当前啊系统当中有自己维护的当前时间,你自己的闹钟的结构体里包含了你未来的超时时间,所以只要当前时间大于等于你曾经设定的时间,说明你超时了,好不难理解吧,所以从目前你给我交代的,那其实我们系统呢它会定期的去检测遍历我们对应的链表,然后呢拿当前时间和每个时间上的对比,一旦对比了发现我们当前的时间,一旦当前时间大于你对大于等于你自己设定的未来时间,好吧?那我就给你通过PCB找到进程,给进程发发信号就完了,好,然后把这个闹钟结构给放完,因为闹钟是一次性的,方案就把它从对应的链表当中把它移走就,完了,好,同学们,所以你看我们虽然并没有去看过他,但是我们照样能理解他啊,好,但是我今天要多一次啊,好。闹钟可能会非常多,对于操作系统来讲呢,它本身啊其实你会发现我们每次遍历我们对应的这个链表呢,它一定会有一个概念,就是我们遍历的这些链表结构体对象里面有很多的时间并没有超时,同学们,那么我这个遍历不就白遍历了,同学们,你们以前学过的数据结构里面有没有一种结构它能够啊就是时间嘛大家定好的你有长有短而且是线性递增的所以你们以前学的数据结构里面有没有一些结构,其实是我肯定是想优先把已经超时的先给他处理处理走,凡是我没处理的,没处理走的,那我就不想把它再处理了,因为他还没超时,你们以前学的数据结构里面有哪些东西是可以有哪些数据结构是非常便于我们进行像这种级别的数据处理的好,那么就是天然它就是比如说有序的,而且呢我们还可以进行很好的去处理好,有没有超时,我们很好对比好,同学们啊,非常好,同学们想到了你们学过的数据结构,优先级队列或者我们可以搞一个最小堆,然后把所有的数据和对象用最小堆管理起来,只要整个堆顶的那个数据它没有超时,整个堆都没有超时,所以操作系统就不需要遍历所有节点了,只要堆顶结构超时了,把堆顶的这个节点一做该做的工作啊时间对比了,发送我们对闹钟信号了啊,完了都没问题,然后再把堆顶对应的数据结构直接移除掉,移除掉之后堆自我调整,所以他就会下一次再把最近一次超时的时间再给你自我枚举出来,然后我们再跑啊,好,直到我们发现没有超时的,我们就完成所有的已经超时的信号处理了,那么我们呢这就是数据结构,操作系统先描述再组织给我们带来的认知方面的变化。好,你操作系统也是软件,你也要维护对应的数据结构,维护数据结构都是用对象管理的,这不就是先描述再组织吗?然后呢再组织把所有的alarm对象用最小堆管理起来,那么我们系统就可以实现对闹钟管理了,好同学们,好同学们,那么如上就是我们关于闹钟当中啊,闹钟不是异常,他指的是系统层面上的一种条件,也叫做软件条件好,同学们那么如上就是我们信号产生的五大方式


core dump标志
在学习进程等待时,我们解构status关注低7位和次低8位,但是还有一个第八位没说
在这里插入图片描述

第八个比特位表示信号动作终止时,终止方式是Term 还是 Core
在这里插入图片描述
利用kill给子进程发送不同终止方式的信号,和解构status进行位操作把core dump标志位打印出来看看
在这里插入图片描述
在这里插入图片描述
打开系统的core dump功能 ulimit -c 大小
—旦进程出异常,OS会将进程在内存中的运行信息,给我dump(转储)到进程的当前目录(磁盘)形成core.pid文件:核心转储(core dump)
在这里插入图片描述
你的可执行程序编译通过了,程序跑起来了后给你进程发信号然后告诉你终止,这是运行时错误
你光知道信号,信号告诉你什么原因出错了,这玩意能告诉我是哪一行代码出错的,编译时还得加上-g选项,还得如下操作
在这里插入图片描述
gdb就告诉你14行出错了
这种属于进程先崩了在调试

3.信号的保存

在这里插入图片描述
kill -l 查查信号,对于我们关心的1-31号信号,这个编号好像很特别嘛
问题

什么是信号的发送?

对于普通信号而言,对于进程而言,进程要知道自己有还是没有,收到哪一个信号
进程那么多相关内核结构你到底是给谁发?
是给进程的PCB发。
task_struct
{
int signal; / / 0000 0000 0000 0000 0000 0000 0000 0110//普通信号,位图管理信号
//最右边0号比特位我们不用
}
1.比特位的内容是O还是1,表明是否收到
⒉.比特位的位置(第几个),表示信号的编号
3.所谓的“发信号”,本质就是OS去修改task_struct的信号位图对应的比特位。
你叫发信号不如 叫 写信号!!
为什么必须是OS向进程PCB写信号?
意味OS是进程的管理者,只有它有资格才能修改task_struct内部的属性!!!

这里用的是整数,但是为了做后续的扩展性采用了位图

为什么对信号做保存?

进程收到信号之后,可能不会立即处理这个信号。信号不会被处理,就要有一个时间窗口。
那如果使用位图结构,我连续发了10次普通信号,进程一次也没处理,因为这是位图位图只能置一次1,那剩下9次信号不就丢了吗?
是的
实时信号会利用队列管理起来,这样就不会丢,而且要求立刻处理

信号如何被保存?

信号保存的2位图1数组三元组

信号本质是就是【1,31】的数字,每—种信号都要有自己的一种处理方法
在这里插入图片描述
pending 表是位图管理结构 就是上面什么是信号的发送?的逻辑,第几个位置是0 or 1表示第几号信号有无
因为信号可能不会被立即处理,就利用这个位图保存起来
信号从产生到递达(处理)之间的状态,称为信号未决(Pending)。
信号产生时,pending表设置对应信号为1,直到信号递达才清除该标志就是置0

block表 和 pending 表一样都是位图结构 0,1表示该信号是否被阻塞/屏蔽
进程可以选择阻塞 (Block )某个信号。即使信号没有产生
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.(就是说pending表里可以是1但不会被处理,换句话说就是一样可以发信号,但信号不会被递达除非解除屏蔽)
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作

实际执行信号的处理动作称为信号递达(Delivery)
handler表 函数指针数组 表示每种信号的处理方法
你用signal可以自定义捕捉信号实现自定义处理方法

handler里面SIG_DFL和SIG_IGN的类型和我们自定义捕捉时传入自定义函数指针类型一样,所以可以直接signal(2,SIG_IGN)
他就去判断如果handler表里是1就忽略,如果是0就是默认退出,虽然0和1都被强转成void (*) (int)
执行方法是进程执行,也就是说是进程处理信号
在这里插入图片描述
所以用这两张位图和一个函数指针数组就可以实现对普通的信号记录保存 相关的管理工作了
这三张表该怎么看呢?
横着看 先看pending收没收到信号,再看被block阻塞了没有,在看怎么处理handler
在这里插入图片描述
我们对于信号的学习,无论多少接口操作都是围绕着获取和设置这三张表展开的
信号如何被保存,已经搞定了

所以OS想发信号,其实就是修改Pending位图
至于信号有没有block和handler,取决于进程和用户是否设置过,剩下的就是靠进程在合适的时候处理信号

sigset_t 信号集

它是一个位图:结构体里面套数组
OS提供给用户的系统数据类型位图,用来获取和操作pending表 和 block表
sigset_t和系统调用配合起来就能对进程的pending 和 block操作了
封装了这些接口意味着用户不能对位图进行任何位操作

在这里插入图片描述

系统调用sigprocmask 控制block表

在这里插入图片描述

系统调用 sigpending

在这里插入图片描述

形参是输出型参数,把调用进程它所对应的pending表带出来


下面写一个代码实验,先阻塞2号信号,然后一直打印pending表时向进程发送2号信号,观察pending表2号信号变为1,因为2号信号不会被递达

4.信号的处理

重谈地址空间3

在这里插入图片描述

我们把0~3GB叫用户空间,
3~4GB叫做内核空间
我们之前学的除了关于内核部分,大部分都只在用户空间之中
我们其实并没有接触过内核空间1GB,它映射的是OS的代码和数据
在当代计算机中,OS被计算机最先加载的软件,所以一般OS加载到物理内存比较靠底侧的位置

那内核空间要和物理内存中的OS的代码和数据建立映射该如何建立映射?
是不是需要内核级页表啊? 是的他需要内核级页表
实际上OS比较复杂,OS地址空间和物理内存之间可以直接建立映射,他就是固定的偏移量
比如拿对应某个内核空间的地址,地址空间不是多了3GB吗?你拿这个内核空间的地址减去3GB就是它物理内存中的地址,直接映射当代OS可以做到,但今天不想这么说,因为OS还有其他地方确实是需要内核级页表的。

我们就这么认为,当前整个OS的代码和数据都是通过内核级页表映射找到物理内存中的代码和数据
请问:
系统里有很多进程的话,用户页表有几份?
有几个进程,就有几份用户级页表,因为进程具有独立性!把进程通过页表来分开,你是你的,我是我的

系统里有很多进程的话,内核级页表有几份?
每个进程都有自己的地址空间,每个进程都有0-3给自己用,3-4给OS用的
但对不起,内核级页表只有1份。
每一个进程看到的3~4GB的东西都是一样的!
也就意味着, 整个系统中,进程再怎么切换,3,4GB的空间的内容是不变的!!!

从此往后,你不是有各种系统调用吗,我们每个进程在地址空间中的正文代码块中里面调用系统调用,相当于在自己的地址空间里面调用该方法,调用完成后直接再返回到地址空间中的正文代码块中,就如同在自己的地址空间里直接调用(就是图中地址空间的从正文代码出发的红色轨迹)

站在进程视角:我们调用系统中的方法,就是在我自己的地址空间中进行执行的,调完就返回。
这部分和我们当年学习动态库时有异曲同工之妙,我们都在自己的地址空间里去进行调度(图中的正文代码块中出发的绿色轨迹)

站在操作系统视角:任何一个时刻,都有有进程执行。我们想执行操作系统的代码,就可以随时执行! !因为随便一个进程它的地址空间都有内核空间和内核级页表就能找到OS所有数据

下面来谈谈操作系统的本质:

OS从根上到底是什么呢?你可以理解OS就是系统中一个进程,它也可以是化整为零般的(意思是把一个整体分成许多零散的部分)在系统中每一个进程都可以让我找到操作系统,因为它的地址空间3-4G每一个人都映射到操作系统当中了,所以整个系统里想找到操作系统代码,很容易
操作系统的本质:基于时钟中断的一个死循环!

故事:我的OS起来了,我让他跑一个任务就跑,而且他还会定期的调度进程等,那你有没有想过是谁逼着OS的代码去执行的呢?你自己写的代码都是OS在调度的,是一个软件在调度我这个软件,可是是谁让OS在执行呢?
在我们的计算机硬件中,有一个时钟芯片,每隔很短的时间,向计算机发送时钟中断,而我们一旦接受到了时钟中断,就要执行中断所对应的方法,这个方法就是OS的代码
每隔很短发送一次称之为滴答

时钟会以高频率触发时钟中断发给CPU,CPU就有寄存器就会收到时钟中断,则OS会执行对应的中断向量表(之前将键盘硬件中断的表)里面的方法。
在这里插入图片描述
OS前期启动时把该做的工作都做完,往后他就会执行一个while死循环,它就要检测当前有没有时钟到来,一旦时钟到来了,CPU就会执行时钟中断所对应的方法,这个方法就是OS里面要执行的中断方法(比如说进程的调度)
所以OS正在执行当前进程的代码,后来时钟中断来了 ,我停下来,执行一下调度,执行调度时检测一下所有正在调度的进程,看当前进程时间片到没到,没到直接返回,如果到了,此时就把当前进程从CPU剥离,再选择其他进程继续运行然后继续再执行,时钟中断如果再到了我们再去做这个工作
所以整个OS它的执行本质是被动的要由时钟这个硬件来驱动OS来运行的

OS早期源码main函数
在这里插入图片描述

for( ; ; ) 死循环中的 pause()就是暂停,OS就在那等着呢,往后整个OS它都靠着时钟中断来驱动它运行,也就是说你有外设中断了那么OS就去执行对应的中断,包括读数据写数据,如果没有外设就绪,那么每隔一段时间直接执行对应的pause 暂停,它做什么呢?它就要执行对应的时钟中断,时钟中断来了它就要执行对应的方法比如调度和执行

外部设备的中断不一定到来,但是OS中每隔几纳秒就给你OS发送时钟中断,逼着你OS,你OS不是在那等着吗,所以OS就会一直执行调度等。
正因为硬件推着OS在走,所以OS才会推着你的进程在走,所以你的代码才会得以推进
在这里插入图片描述
再来谈谈CPU
我们以前从代码里调用到动态库里,我依旧是在用户空间里面调的,所以没有权限问题
但是今天要调用OS的代码(系统调用),你想调你就有资格去调吗?
虽然内核空间被所有进程共享,你作为用户你不能直接执行OS的代码!因为OS不相信任何用户
当年在说页表时,每一个当前正在被调度的进程它有自己的用户级页表,CPU里面有一个CR3寄存器直接指向当前进程所对应的用户级页表,当OS选择调度该进程,所以OS会把当前进程页表,PCB都放到CPU里,可以让OS快速找到它。这个页表的地址直接就是物理地址
所以往后进程在访问时一个进程的页表地址我们是直接能找到的。
在这里插入图片描述

在CPU当前还有一个寄存器,这个寄存器叫做ecs寄存器
你怎么知道你当前正在访问的是用户态还是内核态呢?
在ecs寄存器中有最低2个bit位,这两个比特位排列组合 00 01 10 11
linux内核中,在CPU硬件上有两种常见工作模式:内核态,用户态
所以到底是在用户态还是内核态?
cpu里面的标志位ecs寄存器 低2位 我们用的是 0(内核态) 和 3(用户态)
所以如果你想访问内核态的代码,你必须想办法把ecs低2位 由3设置为0,这就叫进入内核态,就允许你访问OS的数据了 ,如果当期寄存器是3,对不起我们访问OS代码时OS就会拦截你不让你访问。
那谁能帮我们更改这个0 和 3 呢?CPU必须提供方法让我们能够更改CPU工作级别
答: int 80陷入内核 汇编 把寄存器由3改为0
实际上权限审核很复杂,除了今天段寄存器低2位表示之外,还有页表也有字段。

所以什么叫做内核态?允许你访问操作系统的代码和数据
什么叫做用户态:只能访问用户自己的代码和数据

所以两个状态进行切换时,一方面跳转到OS内的系统调用函数入口,你需要提前把cs寄存器
低2位由3变0,就这么理解就可以了
总结:
当我在进行系统调用时,首先要把对应的用户身份由用户态变成内核态,怎么变?对应系统调用被OS设计过,当调用执行时,它会自动帮我们把寄存器权限位由 3 变为 0 ,紧接着在地址空间里面找到OS代码去访问对应方法就可以了,系统调用执行完了当我从内核态返回上次用户空间执行的代码处,cpu的执行模式要从内核态 改为 用户态,继续执行自己的代码


那我要是写个代码int 80 是不是就能访问OS的代码?
OS只允许你调用特定的接口,即便你int 80你也无法访问OS数据,因为所有访问必须经过系统调用,系统调用也是有自己独立的编号的

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

OS设定上是给进程发信号,它不参与信号的处理,默认方式设置好,你愿意用就用,不用就用自己捕捉的,进程要自己处理信号(当然是在OS调度之下处理)
那信号是什么时候被处理的呢?
你要处理一个信号,前提是你自己知道收到信号了,进程就必须得在合适是查查自己的三元组,而三元组都属于内核数据结构,其他人无法查阅,把信号发了之后整个信号的处理是由进程自己完成的,这说明进程一定要处在一种内核状态才能够对信号做处理
所以,信号处理的时机:当我们的进程从内核态返回到用户态的时候,进行信号的检测和处理!
因为从内核态返回时进程重要的工作基本上也做完了,它顺便检测一下信号不过分

信号是如何被处理的?

信号的处理过程

在这里插入图片描述

当我们知道从用户态到内核态之后呢,我们自己写的代码main函数正在执行时,信号来了或者信号没来,我们要进行系统调用了,系统调用进入到内核此时把处理工作做完了,我们在返回是做检测:先遍历pending表有没有信号,没有信号直接过,有信号再看有没有被block,直到碰到一个没有被block的信号,直接执行handler表中的方法,此时就把这个信号处理了
如果动作是默认终止,该终止进程就终止,内核态什么都能搞,代码释放,进程变僵尸
如果是忽略,什么都不做,直接返回,把信号从1清成0。
什么时候会清理pending中的1呢?是当我们执行完信号动作时,还是执行动作之前?
答案:执行它的方法之前,把pending表中信号由1改为0
默认和忽略很好搞,我们最怕的是自定义
如果自定义动作了,我们需要跳转到singhandler执行方法,要执行你自己写的方法,此时本来是内核态那要不要转为用户态呢?
答:我们要从内核态变为用户态,因为OS不相信用户,他不想让用户直接访问OS的代码数据,另一方面,OS不想执行用户写的任何代码,所以今天执行到用户的方法时就必须得从内核态变成用户态,要以用户态的身份来执行用户的代码
因为万一你自己在信号处理方法中做一些非法操作呢,以内核态身份盗取root密码不就让你搞成了
到了用户态singhandler方法执行完了,用sigreturn接口再次进入内核,在从内核返回到上次被中断的地方
因为你要返回上一次系统调用的地方,只有OS知道你上次从哪里来的
当年系统调用进入的内核就必须从内核返回,不能从singhandler直接返回main函数和调用处。
那sigreturn方法是怎么被执行的?
当我们OS以用户身份执行方法singhandler时本质是要压栈的,给singhandler这个函数也在后面入栈入一条指令就叫sigreturn指令就可以了,执行完你的代码就会多执行sigreturn重新陷入内核,然后再从内核里返回!

这个过程比较复杂,记不住怎么办?
10s让你记住
基于用户捕捉代码信号处理的流程
在这里插入图片描述
最后问题:我的代码里根本就没有系统调用,比如写个while循环里面全是加减乘除计算,我ctrl+c也能杀掉他,那它也有系统调用吗? 那它是怎么陷入内核的?
永远不要忽略一个事实就是进程是会被调度的!
只要进程在跑,cpu就会调度你的进程,只要要调度时间片必然会消耗完毕,消耗完毕必然要把进程从cpu剥离,必然下次调度你时又要把你的进程从阻塞队列,运行队列等各种队列拿到CPU开始跑,当它把你的进程二次调度时不就是要由OS先把你的PCB地址空间页表包括上下文全部恢复到CPU上,这肯定是在内核态的!然后cpu调度你时开始执行代码时它跑的不就是你自己写的代码吗?所以它注定了一定会从内核态再回到用户态,所以不要觉得只有系统调用才会陷入内核,在你的代码周期里它会有无数次机会从用户到内核,从内核到用户。

  • 5
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值