信号 和 信号量 有啥关系吗?
没有任何关系, 就像老婆 和 老婆饼一样。
信号的概念
我们生活中的信号
信号弹, 上下课铃声, 红绿灯, 发令枪,闹钟...
a. 你怎么认识这些信号的? 通过后天的学习
什么叫做认识信号
?
1 识别信号 2 知道信号的处理方法
b. 即便现在没有信号产生, 我也知道信号产生之后我们该干什么。
c. 信号产生了, 我们可能并不会立即处理这个信号, 而是在合适的时候去处理, 因为我们可能正在做更重要的事 -- 所以信号产生后到信号处理时这之间一定会有一个时间窗口
, 在这个时间窗口内, 你必须记住信号的到来。
所以
1 进程 必须 识别 +能处理信号 -- 即使信号还没有产生, 也要有处理信号的能力 -- 信号的处理能力应该是进程内置功能的一部分。
2 进程即使没收到信号, 也能知道哪些信号该怎么处理
3 当进程真的收到了一个具体的信号的时候, 进程可能并不会立即处理这个信号,而是在合适的时候处理。
4 一个进程必须当信号产生, 到开始处理信号, 一定会有时间窗口, 进程具有临时哪些信号已经发生的能力。
信号的处理方式:
1 默认动作
2 忽略
3 自定义动作
ctrl + c
为什么能直接杀死我们的前台进程呢?
Linux中, 一次登录中, 一般会配上一个bash, 每一个登录, 只允许一个进程
是前台进程, 可以运行多个进程
是后台进程。
( 一般前台和后台的区别就是谁来获取键盘输入
。)
1 键盘输入是有前台进程收到的
2 ctrl + c
本质是被进程解释为收到了 2
号信号。
(1 - 31 号信号 我们称为普通信号)
(34 - 64号新号 我们称为 实时信号 该信号产生后必须尽快处理)
验证: 进程收到2号信号的默认动作就是终止自己 :
这是一个系统调用,用于修改进程对于信号的处理动作
, 其中 第一个参数表示信号编号 例如 本次我们会使用2
, 第二个参数是一个函数指针, 表示我们针对这个信号所使用的自定义操作
这样就可以将signum
所对应的操作设置为我们自定义的操作。
可以发现此时我们输入ctrl + c
不会终止进程, 而是打印了一句话。
键盘数据是怎么输入给内核的? ctrl + c
又是如何变成信号的?
1 键盘被按下, 一定是操作系统先知道的!
操作系统是怎么知道键盘上有数据的呢?
我们知道Linux下一切皆文件, 键盘同样也是一个文件, 读取键盘上的数据, 本质上是把键盘输出的字符拷贝到键盘文件的缓冲区里
但是, 操作系统
是怎么知道键盘上是否有数据的呢?
在数据层面, CPU是不和外设打交道的,但是在控制层面, CPU是可以读取外设的
在硬件层面, CPU有很多的引脚, 这些引脚通过主板与外设相连, 外设可以通过引脚给CPU发送硬件中断
的信号, 不同的外设发送的硬件中断有不同的中断号
我们假设一号引脚是CPU与键盘相连的引脚, 当这个引脚接受到信号了后操作系统就知道键盘上有数据了, 然后将键盘上的数据拷贝到缓冲区里。
操作系统在启动的时候会有一个中断向量表
这里面存储的主要是直接访问外设的方法的地址
, 他们所指向的方法也是操作系统自己就有的。
当CPU收到中断号后, 就会以该中断号为索引去这个向量表中找对应的方法, 然后执行这个方法, 在上述问题中, 这个方法就是将键盘数据拷贝到键盘文件缓冲区的方法。
补充一下 我们学习的信号就是用软件的方式, 对进程模拟的硬件中断
键盘输入的数据有两类, 一种是1 2 3等等
的数据, 还有一种是ctrl + c 等等
的控制, 我们操作系统在拷贝数据之前会对数据进行判断我们输入的是数据还是控制, 例如在本次问题中, 操作系统识别到了ctrl + c
控制, 就不会将数据拷贝到缓冲区内, 而是将其转化为2号信号发送给前台进程, 所以我们的进程就收到了2号信号。
同样, 当拷贝操作完成后键盘同样会给CPU发生信号, 告诉CPU可以去执行其他的操作了。
信号的产生
无论信号是怎么产生的, 最终一定是操作系统将信号发送给进程的!
因为操作系统才是进程的管理者
键盘组合键
ctrl + c
产生2号信号
ctrl + \
产生3号信号
ctrl + z
产生19号信号(该信号不支持设置自定义行为)
普通信号中9, 19
(杀死进程, 暂停进程) 无法被捕捉。
kill命令
kill -signo pid
系统调用
kill
我们来就用这个系统调用来编写一下自己的kill指令
成功的杀死了这个进程
raise
作用是给调用自己的进程
发生对应的信号。
abort
作用是给自己发送6
号信号
异常
我们知道这段代码, 肯定会出现除0错误
通过 kill -l 我们可以看到该进程是收到了8号信号
如果我们将8号信号自定义一下
进程不再退出, 而是不停的执行我们自定义的动作。
操作系统是怎么知道我的代码进行了除0操作的呢?
CPU会有一个状态寄存器, 他的最高位是溢出标志位
, 当发生除0操作
的时候, 这个标志位就会由0置1;操作系统就会识别到这个错误, 然后给进程发生对应的信号。
为什么会一直执行我们自定义的动作 而不是只执行一次?
由于我们自定义了收到信号的行为, 使得进程不会退出, 所以在CPU不断的调度该进程的时候,都会先将进程的上下文拷贝到CPU上, 于是就被操作系统发现状态寄存器的溢出标志位为1然后重复上述的动作。
对于野指针错误 与 除0 错误一样, 我们该怎么解释 操作系统是如何发现野指针的呢?
当发生野指针的时候, 一定是虚拟地址到物理地址的转化失败了(没权限, 没映射关系等等)
CPU内有一个寄存器, 当通过页表发生虚拟到物理的地址转化失败的情况发生, 他会将转发失败的虚拟地址放在这个寄存器里, 然后CPU会发生硬件报错, 接着被操作系统识别到。
异常也会由软件产生
当管道的读段关闭了后, 写端会被操作系统发送一个SIGPIPE
的信号, 然后被干掉
软件条件
-- 闹钟
他唯一的一个参数表示要在多久后发送14号信号(传递的是秒数)
他的返回值表示:设置本次闹钟时, 上次闹钟的剩余时间。
我们可以看到, 进程只接受到了一次信号, 说明信号只会发生一次。
如果想每隔5秒响一次, 可以在自定义动作里继续设置alarm(5)
每个进程都可以向操作系统申请闹钟, 系统中一定会存在大量的闹钟, 操作系统要不要管理这些闹钟呢? 当然要, 所以一定有描述闹钟的结构体, 然后操作系统用各种数据结构将这个结构体管理起来,所谓的闹钟管理就变成了对数据结构的管理。
操作系统中是用堆(优先队列)
来管理的这个结构体, 建立一个最小堆, 只要堆顶的元素没有超时那么就不用管别的结构体, 如果堆顶超时了, 就取出堆顶做相应的操作, 然后继续上述动作, 管理时间用的是时间戳
我们可以看到虽然上面的动作都是终止进程, 但是有的是term
有的是core
他们之间有什么区别呢?
我们之前将进程等待的时候讲过, 我们接受到的值的次第8位
表示进程的退出码低7位
表示进程的退出信号, 而第8位
表示core dump标志位, 他就是用于区分, term 和 core 的。
我们的云服务器默认是关闭了core功能的 我们可以用ulimit -a
来查看是否被关闭, 如果被关闭了打开即可。
该图中第一个就是。
我们可以使用ulimit -c 10240
将其最大文件大小设置一下, 这样就开启了。
我们使用 2 号信号终止进程
发现core位是0
然后使用8号信号终止进程
发现core位是1
而且系统自动生成了一个core.32356
的文件, 其中32356表示的是刚刚进程的pid。
打开系统的core dump功能, 操作系统会在进程出异常的时候将进程在内存中的信息, 转储到进程当前的目录形成core.pid文件
这个功能我们称为核心转储(core dump)
为什么要进行核心转储呢?
我们可以通过这个文件得知我们的程序在运行过程中的哪行代码出错了
记得编译时带上 -g
选项
回车后就发现 是第14行出错了
所以我们其实可以理解为core就是先term 再 core
。
我们都说信号是会发给进程的, 这样说太笼统了, 准确的说信号是发给进程的PCB的
所以进程的PCB里一定有有一个结构来存储进程收到的信号
对于普通信号而言, 系统使用位图来管理的
- 比特位是0还是1表示进程是否收到对应信号
- 比特位的位置表示第几号信号
- 所谓的"发信号",本质是OS去修改task_struct的信号位图对应的比特位。
为什么是操作系统来发送信号?
因为操作系统是进程的管理者, 只有他有资格修改test_struct内部的属性!
信号的保存
为什么要对信号做保存?
信号的的发送和进程的运行是异步的, 进程无法预料到信号什么时候回到来, 同样操作系统也不知道进程是否在做重要的事情, 所以信号到来后进程可能不会立即处理而是到合适的时机再处理
, 所以我们需要保存起来。
了解一下实时信号 (32号及以后)
- 实时信号一旦到来, 必须立即处理。
- 实时信号不能丢失(发送了10次就必须执行10次)
一些概念
在操作系统中除了pending表还有信号对应的执行方法, 这些方法存储在hander数组中(hander数组是一个函数指针)
一开始这里面都是系统默认的方法, 当用户自定义其方法后会将用户自定义的方法的地址填入该信号对应的位置。
所以总结一下就是
操作系统发送信号就是修改PCB中的panding表,用signal 捕捉信号自定义行为, 就是修改hander表
在默认情况下所有信号都是没有被阻塞
的, 我们可以对特定的信号做阻塞, 一旦我们对某个信号做了阻塞处理, 以后就不会执行这个信号, 除非后来我们解除了对他的阻塞。
具体是则怎么阻塞的呢?
我们要引入一个新的位图
-- block表
也就是这个图中的蓝色箭头部分, 他与panding表结构一样, 比特位的内容 0 -> 不屏蔽 ; 1-> 屏蔽
为了代码的可移植性性, 操作系统加入了sigset_t
类型。 作用是描述 pending 和 block之类的位图, 以及对应的操作接口。
将该位图全部置 0。
将改为图全部置 1
向指定信号集中添加一个特定的信号
在特定的信号集中, 去掉某个信号
判断某个信号是否在信号集中
总结 :
信号集 sigset_t 是系统给我们提供的一种类型, 方便我们对block 和 pending表做操作的
所以我们是不能自己对位图进行位操作, 而是必须通过这些接口。
sigprocmask
其中第一个参数有三总选项
第一个选项, 表示将原始的block图 按位或
上我们参进去的位图(相当于做填增)
第二个选项, 表示将参入的信号集 按位取反
然后与原始的block图按位与
(在block中去掉 set里包含了的信号)
第三个选项, 表示将当前set直接设置进进程的block。(覆盖式的重新设置)
第二个参数是位图作用入上述
, 我们通过
这些方法来得到我们想要的位图。
第三个参数是输出形参数
是用于保存上次一该进程的block表(在我们此次修改前)
sigpending
他唯一的参数是输出形参数, 可以帮我们得到该进程对应的pending表,
我们来小小的使用一下上面的接口。
大概意思就是对进程的2号设置阻塞, 然后观察进程的pending位图
可以看到进程正常跑起来后没有pending里没有任何信号, 此时我们按下ctrl + c
试试
可以看到, 按下ctrl + c
的瞬间, 2号信号就处于pending位图中了, 且进程没有递答他, 这是因为我们之前设置了阻塞。
我们加上这句话, 是的进程在执行20秒后又将2号信号解除block, 看看进程是否会递答2号信号。
可以看到当解除2号信息的block后 进程递答了2号信号, 进而使得进程退出。
既然被屏蔽了的消息就不会被操作系统递答, 那么是不是只要我们把所有的信号的屏蔽了, 那我这个进程就是不死不灭
的呢?
你太天真了, 当然不会。
我们用这段代码, 尝试将所有的信号都屏蔽经过我们的测试呢 依旧是9号和19号
无法被屏蔽, 正如之前9号跟19号无法被捕捉一样。