Linux系统编程:信号的预备和产生
引入: 比如当前快递小哥需要通知你下来取快递(产生信号),然后通过电话或短信告知了你(发送信号),但是当前你正在打游戏,所以你并不会马上去处理,但是你会记得这件事(信号的保存),当你忙完之后,你接着就去处理快递了(信号的处理),而一般会有3种处理方法:1、幸福地打开快递(默认动作) 2、选择将快递送给好朋友(自定义动作)3、忽略他,然后再打一把游戏(忽略)。
引入: 比如当前快递小哥需要通知你下来取快递(产生信号),然后通过电话或短信告知了你(发送信号),但是当前你正在打游戏,所以你并不会马上去处理,但是你会记得这件事(信号的保存),当你忙完之后,你接着就去处理快递了(信号的处理),而一般会有3种处理方法:1、幸福地打开快递(默认动作) 2、选择将快递送给好朋友(自定义动作)3、忽略他,然后再打一把游戏(忽略)。
一、信号的预备知识
1.1 理解信号
问题1:生活中常见的信号有哪些??
——>信号弹、下课铃、表达爱意、红绿灯、快递发短信取件码、旗语、狼烟、发令枪、冲锋号、肚子饿……
问题2:我们是怎么认识这些信号的??(想象我们从小到大然后认识新事物的过程)
——> 小时候如果你的父母想让你认识一个具体的东西比如苹果,那么他就必须通过一些简单的词语向你描述苹果的特征,而如果认识具体的事比如说吃饭,那么除了描述吃饭这件事的特征,还得让你明白如何吃饭。
——>所以我们能够记住这些常见的信号,本质有两个步骤:(1)识别信号 (2)知道信号的处理方法!!
结论1:进程必须具备 识别信号+能够处理信号的能力
问题3:即使我们当前并没有信号产生,我们是否也应该知道信号产生后应该干什么??
——>就好比我们现在并没有在吃饭,但是这不代表我们不知道肚子饿的时候应该做什么!!所以我们可以这么理解:当我们通过学习和认知明白了识别信号以及信号的处理方式之后,这些知识已经深深嵌入我们的大脑。
结论2:即使信号没有产生,我们的进程也要具备处理信号的能力!! 信号的处理能力是属于进程内置功能的一部分!!
问题4:信号产生了,为什么有时候并不会被立即处理??
——> 因为在某些时候我们可能正在做一些我们主观认为更重要的事,比方说你正在打游戏,已经推到对方高地了,这是你的妈妈突然喊你吃饭,这时候你并不想马上下去,而是希望等游戏打完了再下去,但是无论如何你都得下去吃饭,所以你会记得这件事(信号保存),等你游戏打完的时候再去处理。
结论3:当进程真的收到一个具体信号的时候,进程可能正在执行更重要的任务而不能立即处理,这个过程会有一定的时间窗口,所以进程必须具备保存信号的能力!
问题5: 收到信号后我们可能会有哪些处理方式呢??
——>当你妈喊你吃饭的时候,此时常规的理解应该下去吃饭(默认动作),但是你也有可能正在跟妈妈吵架正在赌气,所以你假装没听到(忽略),也有可能你会一边看电视一边吃饭(自定义动作)
结论4:进程处理信号的方式有三种(1)默认动作 (2)忽略 (3)自定义动作(需要捕获信号)
1.2 前台进程和后台进程
Linux中,一次登录中,一个终端一般会配上一个bash,每一次登录,只允许一个进程是前台进程,可以允许多个进程是后台进程! !
问题1:为什么要区分前台进程和后台进程呢??为什么前台进程只能有一个呢??
——>为了区别又谁来获取键盘输入的资格!!
问题2:ctrl+c为什么能杀死前台进程呢??
——>ctrl+c本质上被进程解释为2号信号,被前台进程获取了信号!!
问题3:前后台进程的理解
——>当你在输入命令行的时候,其实bash进程就是前台进程,所以他得到了获取键盘输入的资格,而当我们执行一个可执行程序后,此时该可执行程序就变成了前台进程,而bash则变成了后台进程.
——>可如果在可执行程序后面加上& 意思就是该可执行程序变成后台进程,然后此时bash还是前台进程,因此ctrl+c就会失效(因为他只能发给前台进程!)
问题4:为什么bash进程不会被ctrl+c杀死?
——>因为bash进程非常重要,不能随意被杀死,所以他的内部对ctrl+c这样的信号做了特殊处理
问题5: 当一个死循环的可执行程序被放在后台执行的之后,我们在给前台的bash进程输入命令的时候显示屏指令会很混乱,但是为什么并不受影响??
——>因为信息不仅仅被键盘读取了,也会回显到了显示器上(输入和回显是写在不同的缓冲区),所以我们看到的混乱影响的是输出但是不应该输入,因此并不会影响命令的执行!!
——>同理,这也是为什么你输密码的时候,即使显示器为了保护密码所以不帮你回显,你也能够把密码输进去的原因!!
——>而混乱的原因是因为显示器是一个共享资源且没有加以保护,所以当多个进程同时访问的时候就会出现打印信息混乱的问题!!
1.3 认识信号
1-31是普通信号 (不一定需要立即处理)
34-64是实时信号(必须马上处理)
没有32和33,所以一共是62个信号
1.4 认识signal
这是一个简单的信号捕获函数,可以帮助我们给一个信号设置一个自定义方法!!
signal函数只要设置一次,该进程的生命周期内就一直有效,而myhandler函数只有在后面产生了这个信号之后才会被调用,否则永远不会被调用!!
问题1:我们signal函数在传参的时候不是已经绑定了信号了吗??为什么handler函数里还需要signo??
——>因为未来这种方法可能会被多个信号当成他的自定义方法,所以如果handler方法没有这个参数的话,我怎么知道是因为收到哪个信号才进入handler函数的呢??所以我们必须得有这个参数!!
问题2:所有的信号都可以被捕捉和自定义吗??
——> 大部分信号可以被自定义,但是有些信号不行!!就好比9号信号就不行!因为如果我们对他自定义了,那么这个进程就永远都不会被杀死!!那万一他比如中病毒了,那就会出大问题,所以OS提前考虑到了这点,在给予你自由度的同时也限制了你的行为!!(就跟我们ctrl+c杀不死bash进程一样,自由给予我们更多的便捷,但也同样需要约束其行为!!)
1.5 硬件中断
引入:(1)键盘是如何输入给内核的(2)ctrl+c又是如何变成信号的??——>谈谈硬件中断!!
问题1:键盘被按下,肯定OS先知道!!可以我OS怎么知道键盘上有数据呢??难道我要一直去定期轮询外设么??
——> 因为外设有很多,所以一直轮询是负担很大的事情!!所以在现代的计算机中,虽然cpu不和外设直接做数据的交互,但是外设可以通过想cpu发送硬件中断的信号,然后让OS来帮助我们将数据拷贝到内存中!!
问题2:可是这么多外设,我cpu怎么知道这个信号来源于哪个外设呢??
——> 所以必然存在不同硬件中断号码对应不同外设发出的信号,而cpu不同的针脚就对应着不同外设发出的信号,比如如果外设向cpu1号针脚发送信号(通过高低电频),那么cpu就知道这是什么设备了,然后cpu的寄存器就会暂时将这个数据保存起来(通过充放电)
问题3:cpu怎么处理这个信号呢??
——> cpu收到外设的信号后,他必然是通过指挥OS来帮助我们完成将外设数据拷贝到内存的工作,所以OS必须得具备访问不同外设的方法!!其实我们的OS在刚刚启动的时候,就默认在一个位置维护了一张中断向量表,其中存储的就是OS访问不同外设的方法!!
问题4:键盘上输入的不仅仅只有数据啊,那ctrl+c是怎么变成信号的??
——>所以OS在使用中断方法的时候会先进行判断,如果是控制信号(ctrl+c)的话,就会将他转化成2号信号发送给进程,如果仅仅是单纯的数据,那么就会把数据放到内存中的缓冲区里
问题5:OS调用方法后,他怎么知道拷贝完成了没有??因为可能有其他进程也在等呢
——> 外设在拷贝完成时也会给cpu发送中断信号!!所以当OS识别到时就会知道这个动作已经执行完毕了,然后转而就会去做其他工作了!!
结论1 :所以我们会发现OS其实一点都不关心外设,OS是通过不断接受硬件中断来处理外设的!(当他在cpu的寄存器中识别到相关外设发出的硬件中断信号后,然后通过中断向量表找到从该外设拷贝数据到内存中的方法并执行!! 而什么时候拷贝结束也是由外设向cpu发送硬件中断信号OS才能得知的!!)
结论2: 外设拿到数据后向cpu发送硬件中断,然后OS识别到后再执行中断方法,这个采用的是软硬件结合的方式,而我们之后学习的进程中的信号,就是纯软件方式对进程模拟的硬件中断!!
二、信号的产生
2.1 同步和异步
1、信号的产生和我们自己的代码是异步的!
如何理解同步和异步??
(1)比如你是一个老师,你口渴了让张三去帮你买水,这个时候你告诉同学们让大家先自习,等张三回来了再继续上课
——>此时张三什么时候回来会影响到你什么时候上课,因为大家都得等他,所以这就是同步
(2)还是刚刚的场景,张三去买水,而你还是照常上课,所以无论张三什么时候回来都不会影响你上课的进度,只有当张三喊报告的时候你才会响应一下让他进教室,甚至如果张三旷课了你也不管。
——>此时张三买水和我上课就是异步的,你做你的我做我的互相不影响,只有你买水回来了我可能会稍微暂停一下课程然后响应一下。 你回来之前我是不会等你的。
2、无论信号如何产生,最终一定是由OS发送给进程的,因为OS是进程的管理者!!
2.2 键盘组合键和kill命令
ctrl+c:2号信号
ctrl+\: 3号信号
kill -signo pid:向某个pid的进程传递signo信号
2.3 系统调用
2.3.1 给任意进程发任意信号-kill
2.3.2 给自己发送任意信号-raise
2.3.3 给自己发送特定信号-abort
abort是中止的意思
abort函数是属于3号手册,所以他并不仅仅只是简单地发送6号进程!! 当我们尝试去捕获6号信号的时候,我们发现最后进程还是会终止! 但是单纯给这个进程发送6号信号却不会!!
所以我们可以得到的结论是:
(1)abort执行完指定的自定义函数后会自动恢复成默认,然后重新发送6号信号(abort内部多做了一点工作),所以使用abort的时候无论6号信号是否被捕捉进程都会被强制终止!!
(2)kill是系统调用,而raise和abort只不过是在kill的基础上进一步封装的C库函数,所以他可能会有一些差异化的处理方式,一些细节需要我们自己去理解!!
2.3.4 模拟实现kill命令
2.4 异常(硬件条件)
2.4.1 除0异常和野指针的解析
除0错误收到了8号信号
野指针收到了11号信号(段错误)
问题1:OS是怎么知道进程的内部出现除0错误的??
——>cpu在处理该进程的时候,他的一些信息都是隶属于该进程的上下文,而除0会让cpu上的状态寄存器中的一个溢出标记bit位变为1 ,因此OS就可以检测到异常!!然后发送信号让进程崩溃。
问题2:为什么进程出现异常了OS不崩溃??
——>虽然我们修改的是cpu的状态寄存器,但是当前的信息只属于该进程的上下文,所以只会影响你这个进程(具有独立性),跟我OS没有任何关系,我依然可以去跑其他进程!!
问题3:OS怎么知道进程内部出现段错误??
——> 段错误其实就是内存错误,而我们虚拟地址空间和物理空间是通过页表去映射的,页表就相当于是一个kv模型,而为了提高转化的效率,有一个叫做MMU的内存管理单元帮助我们做虚拟地址到物理地址的转化!!如果转化成功的话,会将转化成功的物理地址放在一个寄存器里,而如果转化失败的话,也会将转化失败的虚拟地址放在一个寄存器里,而当OS检测到的时候就会发送段错误的信号!
问题4:那为什么OS在发现进程异常后为什么必须发送信号来让该进程终止呢??
——> CPU也是硬件,而OS作为硬件的管理者必须对CPU的状态非常了解,所以当OS发现cpu状态寄存器出现异常的时候,那该进程的后续的执行肯定就没有任何意义了!!所以必须向该进程发送强制终止的信号 ——>OS是通过不同的寄存器或者寄存器里不同的标记位来判断是哪种异常的,所以异常是首先表现在硬件上,然后由OS识别过后再发送相关的信号给当前正在cpu上运行的进程!
问题5:我们通过捕获信号不让进程崩溃,但是为什么信号会被一直触发呢?
——>我们通过捕获信号进程如果不崩溃的话,那么该进程就要一直被调度运行!!可是虽然你保护了这个进程,但是由于你始终没有修正硬件错误,所以OS一调度该进程就会发现有错误,然后就会一直给你发信号!!
问题6:那我们可以去修正这个硬件错误么??
——>首先不谈你是否有这个权限,问题是由你进程引发的,即使你修正了硬件错误往下执行了,但是因为你中间出错过,无论如何我都不会相信你的运行结果了,所以你执行下去是没有任何意义的!!你出错的就应该自觉退出!!
问题7:既然出现异常的时候进程必须得退出,那我们为什么还要去捕获呢??
——>我们捕获异常并不是为了去解决这个异常(也没有这种能力),而是为了能够通过这个渠道来让进程退出之前,用户能够更加清晰地知道出现了什么问题,或者是有那种重要数据需要临时保存下来,或者是需要关闭某些资源。 这样可以尽可能减少损失,同时方便上层用户了解情况!(土一点说,异常捕获不是为了让你不死,而是为了让你死个明白,或者死之前托付一些重要的事情。)
问题8:OS明明可以直接干掉进程,他狠起来连自己都可以干,可他为什么不这样做呢??而是要用发信号这种温和的方式??
——>一方面是因为OS作为底层并不清楚上层在干什么,他担心你万一做的是一件特别重要的事,那我们是不是应该在进程退出之前做一些准备(比如临时存储重要数据,打印一些错误信息的日志),这样未来可以方便用户去修正这个问题(让进程死个明白,交代后事)
——>从OS的视角来看,我可以杀死这个进程,但是我担心这个进程很重要,如果我直接把你杀了,到时候你用户出问题了找不到原因还回来找我撒气,那干脆这样,我检测到了异常我就给你发信号,当你检测到信号的时候你可以通过一些捕获然后设置一些自定义动作来让进程在终止前做一些必要的收尾工作,然后再exit, 我已经给你机会了,你如果还处理不好那就是你自己的问题了,跟我OS没有任何关系!!
——>所以我们会发现OS虽然像个皇帝一样高高在上,但他的行为其实也是要受到约束的,不能无脑地去行事!!
2.5 软件条件
异常并不是只会由硬件产生!
2.5.1 一个进程发送信号杀死另一个进程
我可以获取其他进程的pid,然后用kill命令把他杀了
2.5.2 系统调用出错
OS对于文件会有不同的态度,取决于具体问题的严重性(一些情况下会通过系统调用接口的返回值告诉你)
2.5.3 alarm
参数:闹钟响的秒数
返回值:闹钟提前响的时间(比方说我们对闹钟设置的都是5s,另一个闹钟已经响了,但是你从设定到现在只过去了2s,这时说明你提前了3s,所以要返回3)
闹钟不是异常,所以只会响一次!
让闹钟一直响的方法:捕获之后再设闹钟!(可以通过这种方式在进程中设置一些定时任务!)
OS中会存在大量的闹钟设定(同一个进程可以设置多个,不同进程也可以设置),因此OS必须要将他们管理起来 !!
问题1:闹钟结构体里有什么呢??
——>(1)进程pid :知道该闹钟隶属于哪个进程 (2)当前时间戳+参数=未来时间戳(只要当前时间戳大于未来时间戳,就表明超时了,就会给对应的进程发14号信号)
问题2:如果我用链表管理起来,我怎么知道哪一个要超时了呢??难道一直遍历链表么??
——>我们可以用一个优先级队列(小堆)管理起来,这样只要当前堆顶元素没有超时,那么整个优先级队列就都没有超时!!
2.5.4 core dume
core dump究竟是什么呢??我们会发现一些信号会带有core
我们通过一段代码看看这个core dump标记位是否发生变化
然而因为云服务器默认没有开启core功能,所以我们观察不到现象
接下来我们要尝试打开core功能!!
1、ulimit -a查看当前系统的资源配置
2、ulimit -c 10240 修改block大小(开启core功能)
所以core dump是核心转储的意思:打开系统的core dump功能,一旦进程出现core类型(运行时错误)的异常,OS就会将进程在内存中的运行信息给我dump(转储)到进程的当前目录形成core.pid文件
他的核心思想是:先运行,再通过core文件进行事后调试
问题1:你都可以通过发信号、然后捕获来告知错误原因,那为什么还要多次一举形成一个core文件呢??
——> 因为我想知道更详细的信息(比如知道是在哪行出错的),形成了core文件后,我们可以用-g进行编译,然后打开gdb,输入core-file core.pid ,可以看到非常详细的信息!!
问题2:core功能看起来还不错,那为什么云服务器默认不打开core功能呢??
——>因为大多数情况下,如果一个服务挂掉了,应该由运维程序员去重启,但是大公司后端服务器集群很多,所以如果手动去重启就很麻烦,所以他会做很多自动化运维的手段,比如说当服务挂掉的时候,会立马检测错误然后想办法先恢复出来(比如重启),然后事后再通过一些日志信息来排查。所以大多数情况下如果服务挂掉后都会重启的,可如果存在那种很差的代码跑起来就挂,那么每次一重启就会挂,那么就会出现很多core文件(core文件会占据一定的空间),时间长了时候,本来可能只是一个简单的问题,但是后期就变成了磁盘打满core文件的内存问题,甚至极端情况下OS也有可能会挂掉,所以core一般来说要在线上服务关掉的!!这样可以保证重启的功能可以维持!!
更多推荐
所有评论(0)