信号我们将从信号产生,信号的保存,信号处理
分别进行讲解~
至少大思路是这样。开始之前还要进行一些基础知识的铺垫。
目录
- 从生活中提炼一些结论:
- 信号概念的一些储备:
- 信号产生:
- 一、kill指令:
- 二、键盘组合键:
- 三、系统调用:
- 四、软件条件:
- 五、异常:
从生活中提炼一些结论:
a. 信号在生活中随处可以产生------比如红绿灯,天空打雷…
b. 我可以识别识别这些信号-----虽然我可能在课堂中,但是并不妨碍我认识
c. 当信号产生了,该如何进行处理------比如我知道红灯亮了不能走,绿灯亮了可以走
d. 我在做别的更重要的事,对到来的信号暂时不做处理
我上段阶段中的“我”换为“进程”,“生活”换为“OS”就是我们的信号了!
但是仅仅有了上边的概念还不够,还要进行一些补充
对于a,信号的产生与进程是异步的
对于b,c说明进程可以识别并处理信号
对于d,其一:进程需要记住这个事。其二:进程需要再合适的时候在去处理。
信号概念的一些储备:
这里会涉及到我们刚刚提到的信号产生,信号保存,信号处理,先分别涉及一点点,在详细的进行解释。
异步:
那么怎样理解异步?异步就像是在课堂上,老师让一个学生去拿快递,如果老师在等待那个学生过来就是同步,不等待直接进行讲课就是异步。
你拿你的快递,我将我的课,互不干扰
信号:
是linux提供的一种,向指定进程发送发送特定实践的方式。
我们基于以上的两点先来看一看!到底信号是个啥?
使用kill -l命令即可看到linux中的全部信号。
1到31号是普通信号,超过31是实时信号,我们不进行考虑1
我们写个如下程序,并对其发送9号信号(相信我们目前都用过的一个信号,相对来说也是最熟悉的)
发现果然被kill了
上图其实就是一个进程对信号进行处理的一个例子,我们就以这个为切入点进行信号处理的渗透。
信号处理常用的有3种方式
a. 默认动作
b. 忽略动作
c. 自定义动作
对于a:进程处理信号都是默认的,通常包括终止,暂停,忽略…
下图中框起来的就是默认动作,Core与Term其实都代表终止,但具体的区别我们等等在谈。
对于b:忽略就是忽略的意思
对于c:那么就不得不提我们的signal函数了
这是一个对信号进行捕捉的系统调用,捕捉后收到该信号就会去执行自定义的操作。
我们来用代码具体实践一下
执行指令:
现象:没有执行默认动作,而是执行我的自定义动作,并且ctrl + c也终止不了!
结论:
我们handler函数中的函数其实就是发送的信号,2号信号就是ctrl + c, strl + c就是向进程发送2号新号!
那么此刻我想问几个问题:
1 如果没有产生信号呢
2 我们捕捉更多的信号呢
对于问题一:没有产生新的信号就代表是正常运行
对于问题二:我们进行捕捉多个信号,进行测试。
对2 3 4进行测试,仍然是和我们预期一致:收到信号会执行我们的自定义函数,但真的是这样吗?对于这点我们待会有验证。
其实此时我们就可以总结出两个信号产生的方式
- kill
- 组合键
我们如何理解信号的发送与保存?
这里只是浅度的认识一下,毕竟我们也说了这只是一些对产生,保存,处理的一些预备知识。
先来看保存:
详细细心的小伙伴以经发现我们的信号是从1开始,31结束,没有0。为什么呢?
这里就到回到进程了,进程是task_struct结构体,结构体中有成员变量,在这些变量中有一个uint32_t 的类型变量(名字不准确却能反映出关键)
这个无符号整形有0000 0000 0000 0000 0000 0000 0000 0000
32个零。
我们使用位图对这32位比特位进行利用!
当发送信号1时就将1号比特位置为1,
0000 0000 0000 0000 0000 0000 0000 0010
发送2时就将2号比特位置为1
0000 0000 0000 0000 0000 0000 0000 0100
…
以此类推。
发送:
我们修改指定的PCB中的信号位图即可!
将0置为1,所以发信号貌似叫做写信号更合适。
但是这里有个要注意的点,OS是软硬件资源的管理者,PCB是一个内核数据结构,理所应当的只有OS有权利进行写入,所以OS才有资格进行修改。
信号产生:
这里在强调一下,我们现在刚刚结束预备知识的部分。
我们已经得出了两点结论。
一、kill指令:
二、键盘组合键:
ctrl+c。
ctrl+c是我们最常用的,但不是唯一一个组合键,还有一个ctrl+\,也是让进程终止的组合键。
三、系统调用:
说到系统调用那必然要提到一个接口
其实看到这个命令我们大概也能想到kill指令其实也就是由这个调用来的。
那我们来模拟一下kill指令。
进行测试,果然如此。
再来看另外一个系统调用raise。
本质上是对kill的封装,对调用此函数的进程发送你指定的信号。
现象:被捕捉后就去执行我们的自定义代码随后死循环->和我们预期一样。
再来看一个系统调用:
abort:
与raise是一样的,不过这个是指定发送6号信号。
但是要注意一点:虽然允许捕捉,但仍然会终止,是我们常用的3种处理方式例外。
现在我们有两个问题:
一:把信号全部捕捉会怎样?
二:如何理解发送?
对于1:我打个for循环全部捕捉即可~
当我们尝试9时就会发现虽然在代码中捕捉了,但是实际上是不允许的,不然如果你真的全捕捉那岂不是反了天了??哈哈。
但实际上除了9还有别的指令也和9一样,不允许捕捉。
对于二:只是为了再次强调,是OS进行发送信号,修改PCB中的位图。
四、软件条件:
一个很抽象的名词。
我们来举一个例子。
在管道阶段我们知道当读端关闭,写端继续写入就会被OS发送SIGPIPE
信号。
那么和软件条件有什么关系的?
管道的产生条件是与struct file内核缓冲区等具有非常紧密的关系的,他们都是软件,当当读端关闭,写端继续写入就会不满足软件条件,最终导致13号信号的发送。
这时候我们就有需要认识一个新的函数了
闹钟函数。
我们先来看一看这函数
现象:
这实际上是14号新号。
现象:果然收到了14号新号
现在有了以上的基础我们要谈论3个子问题
a. 理解一下IO成本
b. 理解alarm
c. alarm的返回值
对于a,我们其实很好验证,
稍微修改一下代码即可观察到现象
11w到9亿,足以看到IO的速度是非常慢的。
对于b,
在理解之前我们要先说另一个话题,我们肯定经历过手机断电还几天或者电脑,但是开机之后仍然可以保持标准的时间,这是由于我们的电子设备内置了一个纽扣电池,帮助我们计时。
而闹钟可以准时提醒我们是根据一个时间戳的东西。
我们理解一个事物往往需要一个切入角度:而这个角度往往都是先描述在组织,我们有那么多进程,每个进程都可能有一个闹钟,那么就需要先描述则个闹钟。
这个结构体内有各种各样的描述闹钟的变量。
那我们选择什么进行组织呢?
当我们要寻找一个没有超时的闹钟时,只要找到最后一个超时的后一个就可以了
我们可以采用有序链表,但这样太慢了,所以我们最终选择堆!每次pop时就是当前的最小堆,观察是否超时即可!
对于3:我们在设一个闹钟进行观察。
当把sleep改为4时
由此我们可以得出结论:返回值是闹钟剩余的时间。
而我们alarm(0)本质上就是取消闹钟,因为它实际上没有什么意义。
那我们设置一个alarm(2),也是取消上一个闹钟,在重新设置一个。
注意:闹钟默认是触发一次的,
虽然设置了多个但是只会触发一个,因为每次设置都是对上一次的闹钟的取消。
那我们如何设置一个闹钟一直触发?
答:在捕捉函数里在设置一个即可,也叫做常设闹钟。
五、异常:
关于异常我们一定遇到过,真的是太经典了…