程序的“烽火台”:信号的产生与传递
一、认识信号
1. 什么是信号?
在了解进程信号之前,我们先用生活中的例子来理解一下。
在日常生活中,有许多信号,比如:红绿灯,鸣笛声,上下课铃声…,这些信号在没有产生之前,我们就已经知道了这些信号应该怎么处理,这是因为我们已经形成了一种共识。在现实世界中,为什么要有这些信号呢?我们用最简单的信号通知机制,告诉人,应该要做什么,维持秩序,是用来做信息事件通知的
。
大家都点过外卖吧,当外卖员给你打电话下楼取餐时,你不一定会立即下楼,所以,在进程中也是一样的,当信号来临时,进程也不一定立即处理信号
。
2. 进程是如何处理信号的?
那么,进程是如何处理信号的呢?
1.默认动作
2.忽略信号
3.自定义捕捉
那么,进程是如何识别这些信号以及处理的呢?
这是因为,进程内部已经内置了对于信号的识别和处理机制
。
man 7 signal //查看进程信号
1-31 是普通信号,34-64 是实时信号,这些信号都是宏。我们只研究普通信号。
进程未来要对信号做出处理,那么OS就要提供对应的系统调用,修改进程对于信号的处理动作
。
typedef void (*sighandler_t)(int);
//signum信号编号
//handler信号处理方法
//SIG_IGN 忽略信号
//SIG_DFL 默认动作
sighandler_t signal(int signum, sighandler_t handler)
以前我们使用 Ctrl + c 终止过前台进程
,我们就以它为例,来验证一下。
可以看到,Ctrl + c 不再有效,这是因为我们将进程对于2号信号进行了忽略处理,如果我们使用默认处理,那么就会终止进程
。
自定义捕捉
handler函数会将发送的信号当做参数传入,执行自定义的方法
。
Ctrl + \ 向目标进程发送3号信号
。
还记得我们之前说过的前台进程和后台进程吗?那么,什么是前台进程和后台进程呢?
命令行启动的进程默认叫做前台进程 status+
。
./cmd &,让该进程进入后台运行 status
。
Linux一次登录状态的时候,OS会启动 bash 进程,由bash进程启动其他进程,但是系统任何时刻只允许一个进程处在前台,其它进程处在后台
。
计算机的键盘只有一个,允许获取键盘输入数据的进程,叫做前台进程。
后台进程无法从键盘获取数据。
这也是为什么我们无法使用 Ctrl + c 杀掉后台进程的原因,因为后台进程无法获取键盘数据
。
孤儿进程会自动把自己变成后台进程
。
二、信号产生
1. 产生信号的方式
1.键盘可以向目标进程发送信号
。
2.kill 命令向目标进程发送信号
。
3.系统调用
。
4.软件条件
。
5.异常
。
上面我们说过,进程处理信号可能不会立即处理,那么,进程在合适的时间处理信号时,就要求进程必须把信号进行保存
。
这就像你在宿舍打游戏时,外卖员给你打电话让你下楼取餐,你说等一会下去取餐,这就要求你的大脑必须将这个信息记住。
那么,进程将信号记录在哪里的呢?又是怎么记录的?
还记得PCB吗,PCB是描述进程的属性的,信号就被保存在进程的PCB里
。
那它又是怎么记录的呢?使用位图就可以了(unsigned int bitmaps)一共31个信号,使用位图刚好合适
。
bit 的位置:表示信号编号
。
bit 的内容:表示是否收到
。
问题1:如何理解给进程发送信号?
只需要修改进程的 task_struct,信号位图的特定位置由 0 置 1即可,本质就是向目标进程写信号
。
问题2:进程如何部分识别信号?
通过位图对应的位置,是0还是1
。
可是PCB是内核的数据结构对象,修改位图本质是修改内核数据,那么,只能由OS来修改了。
所以,无论信号发送的方式有多少种,最终全部都是由OS向目标进程发送信号的
。
我们说,进程对于信号的处理方式有3种,默认行为,忽略和自定义捕捉。
那么,如果我们将所有的信号都进行忽略和自定义捕捉呢?是不是所有的信号都失效了呢?
接下来就验证一下。
自定义捕捉
忽略信号
可以看到,进程信号大部分都被捕捉和忽略,少部分信号是无法被捕捉和忽略的
。
2. 详解信号产生的方式
2.1 系统调用
信号的产生有多种方式,前面两种我们已经了解,现在就来看系统调用。
//成功返回0,失败返回-1
//pid目标进程
//sig发送的信号
int kill(pid_t pid, int sig);
//成功返回0,失败返回非0
int raise(int sig);
void abort(void); //通常用来结束任务
我们稍微修改一下代码,看看运行结果。
可以看到,abort 函数向进程发送了6号信号,本应该执行自定义捕捉方法,但是执行完自定义捕捉方法之后依然执行了 abort 函数的功能
。
2.2 软件条件
软件条件
//返回先前设置的闹钟剩余的秒数,如果之前没有设置过闹钟,返回0
unsigned int alarm(unsigned int seconds);
需要注意的是,这个闹钟只会起一次效果
。
想让闹钟多次起效果就要求设置多个闹钟。
alarm调用,一次只运行一个进程,设置一个闹钟,以最新的为准。第二次设置闹钟的新时间,会取消上一次的闹钟,返回上一次闹钟的剩余时间
。
alarm 有 IO 的时候,效率比较低
。
printf 不断与外设进行 IO,导致效率低下
。
通过对比,就可以验证上述结论了。
2.3 异常
比如说,野指针,除0错误会导致进程崩溃,那么,为什么这些错误会导致进程崩溃呢?
可以看到,除0和野指针的确导致进程崩溃了,但是,信号自定义捕捉以后,进程在显示器上进行了刷屏,这是为什么呢?
回答这个问题之前,我们先回答刚才的问题。
所以除0,野指针…导致进程崩溃的原因是:因为软件问题被OS识别,给目标进程发送了信号,然后进程处理信号,默认终止了进程
。
现在,我们需要了解异常的底层再对刚才的问题进行回答。
除0:
在进行除0时,这个工作是由CPU
来完成的,CPU内会有许多寄存器,在运算时从内存中将这两个数据拿到两个寄存器中,比如eax中存放数据10,ebx中存放0进行除法运算,将运算结果写入到ecx中。在数学上,一个有限的数除以一个很小的数,会得到一个无穷大的数,而CPU内寄存器的位数是有限的
,为了避免这种情况,在CPU内会存在一种控制和状态寄存器(EFLAGS),这个寄存器中会记录CPU单次运算所对应的状态,寄存器中有许多 bit 位,每一个 bit 位都表明一种标志,其中一个标志叫做溢出标志位
,在进行常规运算时,这个 bit 位的内容为0,表明结果可信,如果除0操作,溢出标志位的内容设置为1,结果不可信,表明本次运算在硬件上就报错了,那么,OS要不要知道呢?答案是要的,OS是软硬件资源的管理者
。
除0操作,是CPU在执行代码的时候,这表明CPU一定在调度某个进程。当调度执行一个进程的时候,CPU内部寄存器的本质是:当前进程的硬件上下文
。
OS已经知道了硬件报错了,那么OS知不知道是谁导致当前CPU报错的?答案是知道的。在OS内有一个 struct task_struct* current 指针指向当前正在调度的进程
。
硬件报错,OS就会根据 current 指针找到目标进程,向目标进程发送信号,进程一旦默认被杀掉,当前进程的硬件上下文也就不存在了,CPU的报错也就没有了
。所以,OS杀掉进程是为了恢复CPU的正常工作。
所以,刚才我们对信号进行了捕捉,为什么会在显示器上进行刷屏呢?
自定义捕捉之后,我们没有将该进程进行退出,那么,该进程的PCB就会一直保留,硬件上下文也会一直存在,所以,OS会再一次调度该进程,恢复上下文,就会继续报错。只要该进程没有退出,就会被OS一直调度
。
野指针:
CPU调度进程时,会从寄存器中获取指令,EIP指向当前指令的下一条指令的虚拟地址,IR寄存器根据EIP寄存器里的指令地址获取对应的指令内容,CR3寄存器保存的是页表的基地址,CPU根据EIP获取下一条指令的地址,MMU会拿着IR寄存器里的地址经过页表的转化得到物理地址,如果成功了,就继续向下执行,如果失败了,CPU里有一个CR2寄存器,存储触发页错误的线性地址(虚拟地址),触发页错误(硬件中断)
。
野指针问题如果触发了报错,本质也是触发了硬件CPU报错。
野指针不一定会报错,比如数组越界
。
如何理解键盘产生信号呢?
键盘也是硬件,按下键盘的时候,OS也要知道,比如按下了 Ctrl + c,OS就会根据按键进行识别,向目标进程发送对应的信号。
那么,OS是怎么知道键盘被按下了?
通过硬件中断做到的
。
键盘与CPU之间间接的通过针脚达到高低电平,从而触发硬件中断,向OS发送数据
。
那如果是磁盘,网卡呢?
所以,几乎每一种外设,都要在内核中内置一些处理方法,来进行处理中断的请求。这些方法构成一张中断向量表,也是OS的一部分
。
3. 核心转储
现在,来探讨一下Core
和Term
的区别。
core dump标志位,叫做核心转储,该标志位表示退出信号的详细退出类型。0表示Term,1表示Core(进一步追踪进程异常原因)
。
进程异常了,我们肯定想知道为什么会异常,哪一行导致异常的。
什么叫做核心转储呢?
OS在进程结束的时候,把进程当前运行的上下文数据,dump转而存储在当前目录下,形成一个core文件,可以用它来进行调试
。
core文件在linux中默认是被禁掉的。
ulimit -a //显示所有资源限制
(gdb) core-file core //加载core文件,定位出错行
今天的文章分享到此结束,觉得不错的小伙伴给个一键三连吧。