学习系统编程No.27【深入信号处理】

news2024/7/2 3:52:13

引言:

北京时间:2023/6/27/21:43,刚刚更新完这个星期的第一篇博客,现在刚好趁热打铁,看看写到11点左右,该篇博客能完成多少,并且今天和我预想的一样,通过早睡,成功在7点起床,但是由于一些列原因,导致我们起床洗漱,吃完饭之后,又睡过去了,并且一睡就睡到了10点,所以在学习时长方面,还没有昨天时间长,不过好在今天晚上进行了一定的弥补,尽量早日可以调整好作息,达到最好的更文效率吧!过几天线上课就要开始了,到时候肯定是更花时间,单单是上课我就有点受不了,更不要说,还要开始做题,头大,哎,感慨!废话不多说,为了早睡,正式进入该篇博客主题,还是有关Linux系统中信号相关的知识,信号这块的知识,涉及面比较广,所以需要我们花费比较大的力气去搞定它。

在这里插入图片描述

深入信号处理

明白信号的处理,一般不是立即处理,而是在"合适"的时候进行处理,并且这个合适的时候指的就是当某一个进程从内核态切换为用户态,那么此时就会有两个问题,首先是为什么进程从内核态切换为用户态时,信号就会被处理呢?其次是内核态和用户态具体表示什么意思呢?如下所述,此时我们对在"合适"的时候进行信号处理进行深度解刨!

如何理解用户态和内核态

首先明白,用户态和内核态是操作系统中两种不同的执行模式,最大的区别就在于资源访问权限,其中用户态执行模式也被称为受限模式,内核态也被称为特权模式,单从名称上我们就可以理解,用户态进程访问资源的能力有限,而内核态进程的访问资源能力则不受限,能够对所有的系统资源以及硬件资源进行访问,本质理解也就是涉及到操作系统内部的代码(系统调用接口、硬件资源)只有内核态进程能够访问,而用户态进程不行,最简单理解如下:

用户态:表示的是某个进程在执行用户,当然也就是我们自己,编写的代码时,进程所处的状态。
内核态:同理,表示的是进程在执行操作系统(OS)代码时,进程所处的状态。

深入理解用户态和内核态

搞定了上述知识,我们对什么是用户态,什么是内核态有了一定的理解,但这些理解也只是基于概念上的理解,所以想要深入理解用户天和内核态相关知识,此时就需要通过图示或者是对比,结合以前学习过的相关知识来理解它,具体如下重谈地址空间内容中详解,但是在重谈地址空间之前,我们需要明白一个点,就是为什么用户态进程会去执行内核级代码(OS代码),具体和两个原因有关,其一与时间片有关,其二更好理解,与系统调用接口有关,具体什么是时间片,下述简单介绍,这里不多做讲解,而系统调用接口就非常好理解,因为无论是我们在使用各种系统调用接口,还是各种动态库、静态库,编写代码时,都离不开操作系统的内核代码,所以明白用户态进程和内核态进程是不可分割,相互配合使用的。

时间片理解:操作系统的多任务处理环境下,为了提高代码执行效率,实现公平的调度和资源共享,操作系统将CPU执行时间划分成一段一段的时间片,分配给每个进程,以轮换的方式让每一个进程都有机会执行,每当一个进程的时间片用完了,操作系统就会进行上下文切换,将CPU控制权转移到下一个等待执行的进程。

重谈地址空间
通过对有关虚拟地址空间的知识理解,我们就可以很好的深入理解用户态和内核态,当然重谈地址空间最主要的目的就是搞懂为什么要区分用户态进程和内核态进程,简单理解很容易明白,就是不想让内核代码被随意访问,所以此时问题就可以转变成,为什么内核代码会被随意访问呢?想要解决该问题,并且搞懂上述问题,此时我们就需要重谈地址空间,如下图所示:
在这里插入图片描述
结合之前学过的知识,我们知道,操作系统为了提高代码执行效率和资源管理方式,对于进程的执行方式,实现的是多任务同时进行,并且为了保证每一个进程之间互不干扰,每一个进程都有属于自己的虚拟地址空间,这样不仅可以很好的保证系统的安全性和稳定性,还可以高效的进行内存管理和资源利用。此时如上图所示,我们明白,在虚拟地址空间中,内核空间占1GB,用户空间占3GB,用户空间中的代码和数据通过用户级页表映射到物理内存,内核空间中的代码和数据通过内核级页表映射到物理内存。并且我们知道,由于用户编写的代码不同,所以每一个进程在用户空间中的代码和数据都是不同的,同理可知,由于地址空间中内核区的代码和数据是操作系统本身就存在的,用户不能就行修改,所以内核空间中的代码和数据是不变的,永远相同的,从而我们就知道,对于所有进程来说,每一个进程不仅拥有唯一的虚拟地址空间,也拥有与之对应的用户级页表,但对于内核级页表来说,所有进程共享同一份,当然原因就是每一个进程虚拟地址空间的内核区代码是相同的。最终明白,这样设计的好处就在于,可以让进程切换和内核代码无关,任何进程都能访问到同一份内核代码。

搞懂了上述知识,此时我们就明白,为什么内核级代码可以被随意访问,因为内核级代码本身就存在于进程的虚拟地址空间上,如果该进程想要直接访问,就可以直接实现函数指针跳转到内核区, 然后通过内核级页表访问到物理内存中与之对应的内核代码和数据,与进程代码直接访问共享区上的动静态库,没有任何区别,都可以直接实现跳转访问,所以此时为了避免这个问题,用户态和内核态的概念就被提出,同理,这两种不同的执行模式,本质就是为了避免内核代码被随意访问,增加系统的安全性和稳定性。

操作系统如何区分用户态和内核态
搞定了上述知识,此时我们就明白了用户态和内核态的起源,本质还是为了配合体系结构,保证系统的安全稳定,不得不佩服前人的智慧,这个体系结构真的很牛,哎,感叹!此时我们再来看看,操作系统具体是如何区分用户态进程和内核态进程,不过在此之前,我们需要看看,操作系统对于一个用户态进程访问内核代码会如何处理,如下:如果一个用户态进程想要直接去执行同一虚拟地址空间上的内核级代码,首先面临的就是操作系统对该进程身份、执行级别的检测,发现该进程是用户态,那么操作系统就会拦截该进程,然后CPU就会拒绝访问对应的代码,并且导致对应寄存器的溢出标志位置1,引发硬件异常,最终操作系统再根据对应寄存器上存储的进程pid,找到该进程,向该进程发送相应的终止信号,进程被终止。所以本质操作系统还是通过软硬件结合的方式来区分用户态进程和内核态进程,在CPU中存在一个CR3寄存器,如果此时该寄存器为3,就表示该进程的执行级别为用户态,如果为0,则表示该进程的执行级别为内核态。如果再深入一点了解的话,此时面临的问题就是:谁在什么时候更改这个寄存器呢?面对这个问题,这个"谁"肯定没有疑问,一定是操作系统,那么操作系统到底是在什么时候去更改这个寄存器呢?这个问题本质也很好理解,同理上述所说,一个用户进程只有在两种情况下会去执行内核代码,一是时间片到了,二是调用系统调用接口,而时间片到了的本质,也就是在执行系统调用,因为保存当前进程上下文,切换下一等待进程,这个代码肯定不是用户来完成的,一定是操作系统内部本来就存在的内核代码,所以明白,操作系统提供的所有系统调用接口,内部在正式执行调用逻辑的时候,肯定存在一段修改该寄存器(CR3)的代码,这就可以很好的解释,为什么执行内核代码(系统调用),CR3寄存器一定为0,而执行用户级代码,CR3寄存器一定为3。

如何理解进程被调度

谈到进程调度问题,此时就不得不结合操作系统,首先我们一定要明白,电脑在开机过程中,本质就是在把操作系统的代码和数据加载到内存中,那么操作系统的代码和数据被加载到内存之后,它是如何启动的呢?此时就涉及到1号进程(systemd),想要让系统启动就一定需要一个进程,该进程就是systemd,因为所有进程之间是一个多叉树的关系,所以1号进程(systemd)就是所有进程的祖先,管理着所有的进程,如下图所示:
在这里插入图片描述
通过上述所说,此时我们就可以明白,操作系统的启动过程是由内核代码完成的,其中1号进程执行的就是与操作系统对应的代码。都知道,操作系统是一个管理软硬件资源的软件,那么具体是如何管理的呢?首先明白,对于操作系统来说,它肯定是一直处于工作状态,那么如何让操作系统一直处于工作状态呢?在系统内部是这样设计的,如下:在硬件中存在一个OS时钟,每隔一段时间,该OS时钟就会向操作系统发送时钟中断信号,操作系统接收到该信号之后,操作系统就会去执行对应的中断处理动作,操作系统在该OS时钟硬件的控制下,就可以定期的执行操作系统对应自己的任务,从而到达一直处于工作状态。

进程调度
搞定了上述知识,再来理解进程调度,那么就是顺理成章,进程调度的本质,就是某一个进程的时间片到了,然后该进程的上下文被保存,保存之后操作系统替换另一个拥有时间片的等待进程而已。再深入理解,就是操作系统去调用进程调度接口(schedule),执行该接口对应的代码,进而完成进程调度。那么操作系统是如何判断一个进程的时间片是否到了呢?同理上述所说,因为操作系统一直处于工作状态,接收到OS时钟的中断信号后,操作系统就会去执行中断处理动作,当然该中断处理动作,也就是操作系统需要完成的各种任务,其中就包括了检测当前进程时间片的动作,具体检测原理:由于进程被调度时间会被保存,此时只需要用当前检测该进程的时间减去保存时间,再对结果进行判断,就可以知道对应的进程时间片是否到了。

总:系统调用的本质就是在编写代码时,将对应系统调用接口的地址编写到我们自己的代码中,然后当程序运行起来之后,CPU处理该代码时,根据对应的地址,找到对应的系统调用代码,但在执行系统调用代码之前,会先执行对应修改寄存器(CR3)的代码,从而让操作系统检测时,检测出该进程是一个内核态进程,程序正常运行。并且同理,在内核代码执行完成之后,依然有相关修改寄存器(CR3)的代码,目的就是将执行模式从内核态变回用户态。

详解信号处理过程

理解内核态切换为用户态时,信号就会被处理

搞定了上述有关用户态和内核态相关的知识,此时我们再来谈谈信号被递达时的相关知识,也就是当一个阻塞信号被解除阻塞时,对应的信号为什么会被立即递达!首先明白,信号的产生是异步的,进程在接收信号的同时,可能也处于运行状态,只有当进程处于一个合适的状态时,该进程才会执行对应的信号。好比在日常生活中,如果你手头上正在做一件事,突然又收到消息,另一件事等待你去完成,那么这个时候,你肯定不是立即就去执行,而是等待一个合适的时候,才会去完成,同理,在操作系统内部,进程运行也是一样。并且通过上述讲解,我们对什么是内核态和用户态可以说是了如指掌,对于什么时候内核态会切换为用户态也有了明确的理解(时间片、调用系统调用),如下图所示:
在这里插入图片描述

如上图所示,此时发现,当进程因为调用系统调用接口,从用户态切换为内核态时,执行完对应系统调用代码时,操作系统就会对该进程pcb中的信号列表进行检测,看是否有信号需要被执行。但,此时会面临一个问题,也就是在进行信号检测时,如果该信号的执行动作是自定义动作,那么此时执行相应自定义代码时,应该使用用户态进程,还是内核态进程呢?具体分析如下:

首先明白,操作系统有没有权利执行我们的代码,也就是内核态进程可不可以执行用户代码,答案当然是可以,因为内核态的权限非常高,所以操作系统并不会像处理用户态一样去检测内核态(所以CR3寄存器表示为0还是为3此时并不重要)。但是,从理论上来讲确实是可以,但是从实际出发,是不允许内核态进程执行我们的代码,原因非常简单:因为如果用户代码中存在非法请求,本来很正常,该非法请求操作系统肯定是不允许的,因为你的权限不够,但是如果此时变成是内核态执行该用户代码,那么瞬间就会导致权限满足,导致非法请求被执行,用屁股想都知道不合理,所以在实际中,并不允许内核态进程执行用户代码,本质就是为了防止用户代码中存在非法行为,不会因为内核态的高权限导致某些非法操作被允许执行。所以显然,上述在执行自定义动作时,使用的就一定是用户态进程,而不是内核态进程。

当然同理,搞定了上述问题,也就是当内核态去执行自定义动作时,需要切换成用户态执行,那么执行完之后呢?面对这个问题,从内核态到用户态的切换我们就可以得出答案,第一次内核态切换为用户态时,是为了执行对应信号的自定义动作,并不是为了回到原用户态进程执行剩余代码,所以如果直接让执行自定义动作的用户态进程直接再去执行剩余代码,肯定就会因为找不到对应的代码而出错,所以正确的执行方法,是先让执行自定义动作的用户态进程切换为原内核态进程,具体过程本质还是在调用系统调用接口(sigreturn),然后再让内核态进程切换为原用户态进程,同理具体该切换过程也是在调用系统调用接口(sys_sigreturn),并且该接口的具体工作原理也就是恢复(找到)对应用户态变成内核态时被保存的上下文,从而实现继续向后执行代码。具体过程如下图所示:
在这里插入图片描述

sigaction接口

搞定了上述有关信号处理过程,此时我们再通过系统调用接口来实地操作一下(信号捕捉),当然此时用到的就是sigaction()接口,头文件:signal.h,基本使用方式:int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);值得注意的是,该接口在使用上功能比较丰富,所以使用起来比较复杂,但本质不变,同理signal接口,只是为了操作handler表而已,其中第一个参数signum同理表示的就是信号编号,第二个参数act,从类型来看就是一个结构体指针,第三个参数同理第二个参数,只不过此时第二个参数表示输入型参数,第三个参数表示输出型参数,同理结合之前学习过的知识,都知道,作为输出型参数本质就是为了获取修改之前对应信号编号的处理动作而已,具体先不谈,重点在于搞懂这个结构体类型具体如何使用,如下图所示:
在这里插入图片描述
如上图所示,此时我们明白该结构体中有5个成员变量,也就是说如果想要使用对应该结构体类型的参数,就需要将这5个结构体成员变量都给初始化,并且对应不同的初始化内容,此时sigaction()接口就会起不同的作用,例如此时sigaction结构体中的第一个成员变量sa_handler,可以看出该变量是一个函数指针类型,该变量在sigaction接口中起的作用和signal接口是相同的,如下代码所示:
在这里插入图片描述
可以发现,我们除了将sa_handler函数指针变量给了对应的handler函数之外,其它结构体成员变量都置为0,并且我们明白,如果此时将sa_handler赋值为常数SIG_IGN,那么此时就表示忽略该信号,如果将sa_handler赋值为SIG_DFL,那么表示执行默认动作,所以从本质上来看,sigaction()接口单独对于sa_handler变量来看和signal接口没有任何区别。

信号处理过程细节知识
明白,当某个信号的处理函数被调用时(信号被递达),内核会自动将当前信号加入进程的信号屏蔽字(也就是将该信号设置为阻塞信号),当信号处理函数返回时自动恢复原来的信号屏蔽字(解除阻塞),这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。明白了这点之后,此时就可以来谈谈sigaction结构中的另外一个成员变量sa_mask啦!也就是在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,此时我们就可以使用sa_mask成员变量来完成,说明这些需要额外屏蔽的信号,并且同理,当信号处理函数返回后,这些信号会自动解除阻塞,也就是恢复原来的信号屏蔽字。具体如下图所示:
在这里插入图片描述
如上图所示,首先明白的就是sa_mask变量本质就是一个sigset_t位图结构,本质就是在设置对应的位图结构,从而让sigaction()接口在使用sa_mask变量时,将sa_mask位图中对应比特位的信号编码阻塞,也就是添加到信号屏蔽字中。该sigaction结构体中剩余成员变量于实时信号有关,这里不多做讲解,其中就是sa_flags变量中还包含一些选项,今天我们先全设置为0就行,所以sigaction()接口相关知识,我们就了解到这,并且信号处理相关知识我们就搞定啦!

总结:有关信号产生、信号保存和信号处理相关的知识,在前几篇博客和该篇博客我们就学习完啦!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/699995.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

容器访问外部网络之ip_forward数据包转发

文章目录 1 问题解决1.1 问题1.2 原因1.3 解决临时打开:永久打开: 2 net.ipv4.ip_forward内核参数通俗解释3 在Linux中验证并实践net.ipv4.ip_forward设置 IP 地址设置路由信息打开 IP Forward 功能关闭主机 R 的防火墙和 SeLinux验证容器技术其它 4 常用…

< vue + ElementUi 组件封装:实现弹窗展示富文本数据,允许全文搜索高亮显示搜索内容 >

实现弹窗展示富文本数据,允许全文搜索高亮显示搜索内容 👉 前言👉 一、效果演示👉 二、实现思路👉 三、实现案例👍 卷王必胜!往期内容 💨 👉 前言 在 Vue elementUi 开…

私域流量怎么运营?

最近几年,随着微信社群营销的兴起,互联网上出现了一种火爆的变现模式,即将流量引入微信个人号或社群,并通过活动、促销和私聊等方式进行转化,从而为企业或个人带来变现,这就是私域流量变现。 实际上&#x…

ARMv8 - 安全机制 - 异常等级

简介 ARMv8架构处理器有一套异常等级(Exception level)机制,分成4个等级(EL0 ~ EL3)。处理器运行时会处于其中的某个等级并且可以进行等级切换。 资源差异 每个异常等级都拥有一些自己版本的特殊寄存器,…

基于大数据技术对基金分析-python

提示:本文为个人原创,仅供技术探讨与交流,对实际投资并不造成建议。 基于大数据技术对基金分析-python 前言一、数据获取:python爬虫1).从天天基金数据接口获取数据2).爬虫前期准备3).爬虫具体实现 二、数据清洗及计算指标1.过滤数…

高性能计算工程师工资一般多少?

由于近两年深度学习的迅速崛起,超算互联网的普及以及AIGC的大规模应用,各行各业对高性能计算工程师的需求大涨,因此高性能计算工程师的工资也在逐年上涨中,并频频爆发抢人大战。 甚至年薪百万依然难以招到合适的人才。 有很多大厂…

Android JetPack 深入分析ViewModel源码

文章目录 前言源码分析ViewModel是如何创建的?ViewModelProvider(this)做了什么?小结 get(MyViewModel::class.java)做了什么?小结 ViewModel是如何实现配置更改后数据恢复的?整体时序图 结语 前言 本文主要分析ViewModel相关源码…

如何提高项目估算精准度 关键看3方面

项目估算非常重要,这直接关系着项目的成本和收入,如果估算不准确,将为项目带来较大风险。一般软件规模可以用多种方式进行估算,但是用功能点估算方式更准确,而自动估算让估算更快速,我们以CoCode需求分析工…

ChatGpt能够用来做什么

作为计算机从业人员,chatgpt能够从多方面提高大家的工作效率,主要包括以下几点: 技术问题解答:当遇到技术问题时,可以向ChatGPT提问并获取解答。ChatGPT可以提供相关的知识、文档和示例代码,帮助程序员快速…

vue2封装单张图片上传(常用于身份证正反面)

一.实现效果 二.入参 props: {defaultImg: {//默认位置的照片type: String,default: "",},uploadWidth: {//照片框的宽度type: String,default: "148px",},}, 另外如果修改了宽度的话,在外部组件需要用scss重写一下样式 /deep/ .el-upload-lis…

芯片中的上百亿个晶体管是如何设计的?

2021年4月21日,在芯片界的顶级会议Hot Chips大会上,Cerebras Systems公司发布了一款晶圆级引擎芯片——Wafer Scale Engine 2。 这款芯片采用台积电7纳米工艺制程,拥有85万个AI核心,包含2.6万亿个晶体管,面积为46225平…

Linux8.进程(中)(状态)

1.grep -v 关键字a :不显示关键字a匹配的信息。 2.进程状态 :新建,就绪,阻塞,挂起,执行,终止。 运行 : task_struct结构体在运行队列中排队,这就叫做运行态。 阻塞 :等待非CPU资源(磁盘,网卡…

科技云报道:公有云内卷时代,青云的新想象力在哪?

科技云报道原创。 2023年接踵而至的价格战,将国内公有云的竞争力度再次拉满。阿里云、腾讯云、京东云、移动云带头降价,也将寒意传导给了更多腰部云服务商。毫无疑问,这是一场对云服务商的残酷考验。 在公有云高度内卷的阶段,以…

kaggle新赛:肾脏血管分割大赛赛题解析(CV)

用AI为医疗贡献一份力量,从加入本次竞赛开始! 赛题名称:HuBMAP - Hacking the Human Vasculature 从人肾组织切片中分割微血管结构 赛题链接:https://www.kaggle.com/competitions/hubmap-hacking-the-human-vasculature 赛题背…

java 正则表达式总结

目录 一、简介 二、源码分析 1.简单实例 : 2.底层实现 : 1 fund()方法 2 group(0/1)方法 (1)group(0): (2)group(1): 三、 基本语法 1.元字符介绍 : 2.元字符—字符匹配符 : Δ代码演示 3.关于字母大小写问题 : Δ代码演示 4.元字符—定位符 : 1 定义 2 常用定位符 3…

图简介-数据结构和算法教程

介绍 图是由顶点和边组成的非线性数据结构。顶点有时也被称为节点,并且边是连接图中的任何两个节点的线或弧。更正式地说,一个图是由一组顶点(V)和一组边(E)组成的。该图表示为G(V,…

Win11的两个实用技巧系列之亮度条消的解决办法

Win11更新后无法调节亮度怎么办 Win11亮度条消的解决办法 Win11更新后无法调节亮度怎么办?win11系统升级以后,发现屏幕亮度不能调节,没有亮度调节按钮了,下面我们就来看看Win11亮度条消的解决办法 电脑更新后,亮度条消…

如何设计可以动态扩容缩容的分库分表方案?

对于分库分表来说,主要是面对以下问题: 选择一个数据库中间件,调研、学习、测试;设计你的分库分表的一个方案,你要分成多少个库,每个库分成多少个表,比如 3 个库, 每个库 4 个表&am…

SpringSecurity(五):前后端分离认证总结案例。

前后端分离认证总结案例 前言难点分析Controller层eneity层RoleUser dao层service层config层LoginFilterSecurityConfig resourcesmapper propertiespom.xml结尾 前言 和上一篇一样,从上倒下复制粘贴,所有代码贴完再运行,代码没有问题&#…

初步学习使用SpringBoot框架(手动插入数据模拟访问数据库)

对于SpringBoot框架介绍大家可以看看这个这篇文章,SpringBoot优缺点以及如何安装使用 以下我是按照老师给的安装方法进行安装使用SpringBoot框架: 大家安装SpringBoot框架时候,最好安装3.0以下的,不然需要对应较高版本的JDK版本&…