💖作者:小树苗渴望变成参天大树🎈
🎉作者宣言:认真写好每一篇博客💤
🎊作者gitee:gitee✨
💞作者专栏:C语言,数据结构初阶,Linux,C++ 动态规划算法🎄
如 果 你 喜 欢 作 者 的 文 章 ,就 给 作 者 点 点 关 注 吧!
文章目录
前言
今天博主将给大家讲解的信号这个知识,信号和信号量是两个哪有任何关系的概念,所以大家不要以为信号量没有学,就不敢来学信号,信号是我们日常生活中必备的,有了信号才会让我们社会变得稳定,计算机里面的信号也是起了这样的作用,没有信号,那么程序就会乱,所以接下来博主就通过生活信号的例子来映射到计算机里面的信号,在带大家认识计算机信号是怎么样,会有一条讲解逻辑,大家跟着博主的思路走下去吧。
讲解逻辑
- 预备工作(讲解信号的认识)
- 信号的产生
- 信号的保存
- 信号的处理
一、信号的预备工作
1.1生活->计算机
先给大家讲解一个小故事,跟着博主总能时不时听到一个小故事
有一天小明在家非常的饿,但是他又不想出门,此时他就点了一份外卖,但是他不能干等着,觉得太无聊了,就开了一把王者,当他准备推别人高低了,外卖到了打电话来说,你外卖到了来去一下,但是你此时有更重要的事情要做,你就说,等会去拿,但你打完这把,此时你大概率会取外卖,而不是再开一把,上面的小故事,就包含了讲解逻辑的四点,我们知道打电话来了就是信号的认识,真的打电话来了,就是信号的产生,等一会再拿,就需要把信号保存起来,打完了去拿就是信号的处理。 显然大家对上面会产生疑惑,但是又觉得这是必然的,谁不知道打电话来了就是外卖到了呢,好比红灯停绿灯行的道理呢,但是博主还是需要一点点的给大家灌输信号的概念,越是简单的东西,就要细细的讲。
听完上面的故事,接下来通过上面的故事引发了下面这三点结论:
在生活中,我们常见的信号有信号弹,上下课铃声,红绿灯,闹钟以及外卖的电话等等
- 你是怎么认识这些信号??
a.肯定是有人教,我自己记住了这些常见的信号
b.教会了识别信号,还要教处理信号的方法
- 即使是我们现在没有信号的产生,我也知道信号产生之后,我该干什么
大家现在在家里坐着,也知道红灯绿灯这个信号如果发生我们该干什么
- 信号产生了,我们有可能不会立即处理这个信号,在合适的时候(后面会讲)。
因为我们此时在做更重要的事情,好比在推高地,所以在信号产生后->时间窗口->信号处理,在这个时间窗口内,你必须记住信号已经到来了。
上面用加粗标记的你在计算机里面指的是进程,也对应三点结论:
- 进程必须要有识别信号和信号产生后的处理能力,信号没有产生,也要具备处理信号的你能力,所以这些信号在一开始就内置在进程的描述对象里面,创建时候自动初始化
- 进程即便是没有收到信号,也能知道哪些信号该怎么处理,所以在进程中可以在任意位置使用kill -l查看有哪些信号
- 当一个进程针对收到一个具体的信号的时候,进程可能并不会立即处理这个信号,会在合适的时候,所以一个进程当信号产生到信号开始被处理,就一定会有一个时间窗口,就要保证在时间窗口内进程要具有临时保存这些信号的能力(这也叫信号相对于进程控制来说好似异步的。)
我们还没有开始讲解进程的信号,通过生活的例子,我们就可以得出进程应该也符合生活中的结论
1.2ctrl+c终止进程
我们常用操作哪些是具备了发送信号的功能:
我们来写一个简单的程序:
#include<iostream>
#include <unistd.h>
using namespace std;
int main()
{
while(1)
{
cout<<"i am aprocess"<<endl;
sleep(1);
}
return 0;
}
大家应该知道CTRL+C会终止进程,但是为什么可以终止进程呢,这个肯定和信号有关,但是先放放,先带大家认识一下前台进程和后台进程
1)前台进程 ./test
2)后台进程 ./test &
在形成后台进程后会有一个任务号,通过jobs来查看后台进程,
我们在使用fg +任务号
,就可以使此后台进程变成前台,如果此时又想变成后台就使用CTRL+Z
,相当于发19号信号,他会暂停程序变成后台进程,想要在运行起来使用bg+任务号
画图理解Linux中进程间的关系:
在一个session中,只能有一个前台任务和多个后台任务,都可以往显示器进行打印,所以显示器不是区分前后台的,键盘只有一个,所以谁拥有键盘谁就是前台进程,将键盘数据传给具体的一个进程,防止乱。当会话关闭,则bash也关闭了,后台进程没有直接退出,而是被继承到os上,PPID就会发生变化变成1,所以后台进程会受到用户的登录和退出的影响
如果后台进程不想受到会话的登录和退出影响的话,就采用守护进程化
前台进程和后台进程的区别是,我们在前台进程运行test.cc,其余的指令就跑不了(
问题1
),而且命令行也显示不出来,但是CTRL+c(问题2
)可以终止进程,而后台进程,我们发现指令程序还可以继续跑(问题1
),而且分开输入指令依旧可以跑(问题3
),最重要的是CTRL+C终止不了进程了(问题2
),但是还是打印在显示器上(问题4)
问题2: 我们的每一次登录,就是一个终端窗口,只允许一个进程是前台进程,bash就是一开始的前台进程,和运行多个后台进程,当有其他前台进程,bsah就自动变回后台进程,当没有前台进程,bash就变成前台进程,前台进程能够收到键盘输入,所以当我们的test变成后台进程,这个进程就收不到键盘输入的CTRL+c,就退出不了,但我们的bash是前台进程的时候,我们使用CTRL+c,bash怎么不退出,原因是bash进程受保护了,收到ctrl+c不做处理
问题1: 我们的CTRL+c只能终止前台进程,后台进程只能通过kill -9 +pid杀死,原因是只有前台进程才可以收到键盘输入,test是前台的时候可以被ctrl+c终止,是后台的时候就不行
问题3: 答案看1.3章节的第四点
问题4: 后台进程为什么还打印在显示器上,不能以为打印在屏幕上就是前台进程,这是一个显示器文件,和进程是没有关系的,该往显示器打就往显示器上打印
讲解完毕前后台,我们提出疑惑:为什么我们的ctrl+c能够终止前台进程
原因是收到了2号信号,是终止程序,我们来看看进程内置了多少个信号:kill -l
我们一共有62个信号,没有32 33号信号,这些信号其实都宏定义,左右两边都是一个意思,0-31是我们这篇要讨论的信号,34-64是实时信号,收到信号立马要处理,我们不讨论。为什么没有32 33号信号,这就与历史有关,大家下来可以去搜搜
信号的处理方式
- 大家默认的处理动作
- 无视这个信号,不做处理
- 自己设计一个处理动作(自定义捕捉)后面会经常用这个验证结论
上面的三点在我们日常生活中其实这样,比如红绿灯,默认是红灯等绿灯行,不管红绿灯直接无视,红绿灯亮起你有自己的一套动作,就这三种
突然说起这个是为了验证我们程序在输入CTRL+C确实是收到2号信号:
我们来介绍一个函数:
这个函数的第一个参数是传入要修改信号处理方式的信号,第二个参数对应自定义动作,是一个函数指针,这个函数有一个参数,是我们第一个参数,让我们一起来验证一下吧:
#include<iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
using namespace std;
void myhandle(int signal)//这个函数后面会经常用到
{
cout<<"i get a signal:"<<signal<<endl;
}
int main()
{
while(1)
{
//signal(SIGINT,myhandle);
signal(2,myhandle);//修改2号信号的默认处理方式
cout<<"i am aprocess:"<<getpid()<<endl;
sleep(1);
}
return 0;
}
确实我们验证我们的进程是收到了2号信号,因为修改了处理方式,所以就没有终止进程,因为处理方式只能选择一种
解决疑惑:
我们的signal函数为什么第一个参数已经传信号了,为什么myhandle还要传参数??
- 系统接口都是c语言里面写的,函数直接本质是独立的,不是c++的类,声明一个成员变量,内部的函数都可以使用这个成员变量,signal内部会调用myhandle这个函数,因为在一个进程里面可能不止一个信号要修改处理方式,都是调用myhandle这个处理方式,为了标识是哪个信号调用了这个处理方式的函数,所以需要传参。就好比下面:
signal(2,myhandle); signal(3,myhandle);
这样就可以知道哪个信号使用这个处理方式我们的signal为什么放在第一行,不放在其他地方,我们的目的是让这个进程以后收到这个2号信号换成自定义处理方式,不需要放在循环里面,放在最后也不行,放在最前面就恁恶搞保证这个歌程序在任意时刻收到这个信号都可以执行我的自定义方式
看看哪些信号可以被修改处理方式
既然可以修改信号的处理方式,我们如果把9号也修改,进程在死循环,那么这个进程是不是就无敌了??我们来看验证结果:
通过结果我们发现除了9号和19号不能被修改其余都可以被修改,这样说明系统不会让一个无敌的进程存在,因为根据常理,这两个信号允许被修改,可能会造成一系列严重的后果,大家下来自己去思考一下,也很容易理解。
1.3通过硬件来谈谈ctrl+c
以键盘代替硬件
这一幅图,让大家了解硬件的数据是怎么被os读到内存里面的。博主讲解这个一个是为了给大家涨知识,另一个是让大家再次认识信号,我们的硬件中断其实就是一个信号,但是和我们今天讲的信号没有关系,但是我们今天讲的信号就是用软件的方式对进程模拟硬件中断
- 如果是那种abcd的输入按键,os就直接识别到这样的数据,但是收到像ctrl+c这样的组合键的时候会做判断,将ctrl+c转换成2号信号给进程
- 我们的键盘有数据,os就会将键盘的数据读取到内存,那么我们的os怎么知道数据读取结束呢?os不必知道,键盘数据读完,也会给cpu发送硬件中断,还是一样的套路,其他硬件也是这样的操作
- 为什么我们cin,scanf在输入的时候会进行阻塞等待键盘输入呢?原因是我们os是进程的管理者,你键盘不给我数据,我就让这个进程等待,你啥时候给我,我就让进程继续运行下去,是os控制着一切的。
- cpu的引脚连接多个硬件,但不是每个硬件都对应一个寄存器,这样就会导致多对一的关系,万一多个硬件发生中断怎么办,CPU先处理那个硬件中断呢??所以我们设计者为了避免这个问题,在体格时刻值允许一个硬件中断发过来,即使你同时使用鼠标和键盘,在屏幕上看着是一起的,但是对于CPU他是不同时刻获取中断的,我们感知太慢了,给我们他们是同时发送中断的错觉。
- 来回答1.2中问题3的问题:
相信大家的问题应该得到了解答,我们有的时候要回车才可以,有的时候按下来就有,通过上面的图,大家应该就可以明白,设置了缓冲区A的缓冲策略,什么场景用什么刷新策略。
二、信号的产生
上面是我们讲解信号的预备工作,接下来就开始讲解信号的产生。
通过1.3节我们通过ctrl+c这样的组合键给进程发送信号,也就是信号产生的过程,也知道进程是怎么收到的组合键的,通过os. 所以接下来就来看看我们键盘上面还有哪些组合键可以给进程发送信号,博主带大家来测试一下:
2.1os自己给进程发送信号
这个信号的产生的不一定是进程出现了问题,而是os自己去发送的。有三种方式:
第一种方式:
ctrl+c:2
ctrl+\:3
ctrl+z:19
大家看到结果了吧,还有其他的大家可以自己去摸索。
第二种方式:
kill -signal pid
第三种方式:
这个是通过系统调用接口来产生信号,一会介绍完系统调用接口,博主带大家模拟实现一个符合第二种方式的mykill,也会比较有意思:
- kill
这是一个可以在命令行输入也可以当成函数,知识名字一样,一个是指令,一个是函数,不要以为是一样的,这个函数的主要功能就是给任意进程发送信号,好比父子进程
第一个参数:进程的pid,第二个参数就是信号
来看案例:
#include<iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
using namespace std;
void myhandle(int signal)
{
cout<<"i get a signal:"<<signal<<endl;
}
int main()
{
cout<<getpid()<<endl;
signal(7,myhandle);
int cnt=5;
while(1)
{
cout<<"i am a proc"<<endl;
if(cnt==0)
{
kill(getpid(),7);//我这里就一个进程,所以进程pid就是自己
cout<<"我收到7号信号了,退出循环"<<endl;
break;
}
cnt--;
sleep(1);
}
return 0;
}
我们使用kill函数实现了给进程发送信号,大家下来自己测试一下给不同进程发信号,可以使用管道让我们进程间进行通信,把你要发信号的进程号传过去。接下来模拟的mykill中也有体现
- raise
这是一个kill子集的函数,他的作用是给调用他的进程发送信号,所以少了一个参数。
raise(7);//把刚才kill的位置换成这个就可以了
这个函数还是比较简单的,
- abort