一.预备知识:
首先我们说一下生活中的信号,比如红绿灯,我们对红绿灯的不同颜色时会进行不同的动作,他向我们传递了红灯绿灯的信号,我们会对不同的信号做出不同的反应,即红灯行绿灯停,我们为什么会知道红灯行绿灯停呢?原因是我们从小接受的教育是这样,如果一位一直在大山中生活的人突然来到城市,他是不会知道红灯停绿灯行的概念的,即我们认识信号,并知道应该进行相应的动作,这就是信号的识别。
绿灯亮起,我们要过马路,迈开步子向前走,这就是信号处理,而我们看到绿灯过马路,就是默认动作。那么我们可不可以不往前走,而是看到绿灯后,给朋友打个电话,告诉他,嘿哥们我等到绿灯了,然后被他大骂神经病,这虽然不是正常行为,但我们可不可以这么做呢?肯定是可以的,这是对信号进行自定义处理。我们再假设,你和你朋友一起等红灯,这时朋友说要去上个厕所,在朋友上厕所时绿灯亮了,但是你打算等朋友一起过绿灯,所以你忽略了绿灯亮起,没有对其进行任何处理,而这就是对信号的忽略。所以,信号在处理时有三种动作:默认动作(看到绿灯过马路),自定义动作(看到绿灯给朋友打电话),忽略(等朋友一起)。
那么,我们可不可以在看到绿灯时,不立即进行处理,比如等红灯时你尿急,你旁边就是公共厕所,此时绿灯亮起,你先上个厕所,再过绿灯,这肯定没问题,也就是说,在看到绿灯时,不立即过马路,而是上完厕所再过马路,这是在等合适的时候再进行处理。
在等红灯时,朋友在上厕所,此时绿灯亮了,我们不等他,而是立即冲到他的坑位,告诉他夹断,并擦屁股,再过马路,在你冲到他的坑位时,已经记住了绿灯已亮起,这是对信号的保存,并在合适的时候进行处理
OK,理解了以上后,我们再来看进程信号,进程是如何识别的呢?同样也是认识信号+知道相应的动作,进程是为什么能识别呢,答案是程序员在内部编码完成的,当信号到来时,进程不一定要立即处理,可以先将信号保存,再将当前代码执行完后再处理信号,同样进程在处理信号也有三种动作,即默认动作,自定义动作,忽略。
那么进程如何保存信号呢?我们放到文章末尾解释
二.信号产生:
一.指令产生:
linux中,最常用的杀死进程的指令即kill,我们在shell中输入kill -9 pid,就可以杀死任意进程,杀死进程本质就是向进程发送杀死信号,我们在shell中输入kill -l 可以看到各种信号,如图:
其中1-31为标准信号,31-64为实时信号,我们主要学习标准信号,不同的信号有不同的产生原因,通过kill指令,我们可以指定某一信号发送给指定进程,并将其终止
二:系统调用产生:
首先我们要知道的是,我们是没办法直接向进程发送信号的,必须是通过操作系统向进程发信号,而我们想通过操作系统,就要使用操作系统的函数调用,下面是常用接口:
int kill(pid_t pid, int sig);
第一参数为pid,第二参数为信号,我们用此函数,可实现简易的kill命令,如以下代码
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
using namespace std;
int main(int argc,char* argv[])
{
if(argc < 3)
{
cout << "Usaga: kill pid signal"<<endl;
exit(1);
}
int pid = atoi(argv[1]);
int signal = atoi(argv[2]);
kill(pid,signal);
cout << "killed "<< pid<<endl;
return 0;
}
test:
#include<iostream>
#include<unistd.h>
using namespace std;
int main()
{
while(true)
{
cout << "I'm running my pid = "<< getpid() <<endl;
sleep(1);
}
return 0;
}
使用mykill杀死test,执行结果:
不仅时kill函数,常用的还有raise,向自己发送任意信号,和abort,向自己发送指定信号,这两个感兴趣的可以自行测试一下
int raise(int sig);
void abort(void);
三.软件条件产生:
在管道那篇博客中我们提到,当读端关闭,写端继续写的话操作系统会发送异常信号给写端,并终止他,发送的就是kill -l中的13 SIGPIPE信号。不仅是管道,在进程中,我们可以设定闹钟,当闹钟响时,操作系统会给指定进程发送闹钟响了的信号,并终止该进程(闹钟这一话题,我们在后面可以多加探讨)
unsigned int alarm(unsigned int seconds);
参数为闹钟秒数,一般用来测试硬件性能,比如测试一下一分钟cpu可以执行多少次加法操作等等
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
using namespace std;
int cnt = 0;
void catchsig(int signal)
{
cout << "闹钟响了 " << " cnt = "<<cnt<<endl;
exit(1);
}
int main(int argc,char* argv[])
{
signal(SIGALRM,catchsig);
alarm(1);
while(true)++cnt;
return 0;
}
这里要说一下signal函数,我们前面讲说信号处理可以进行自定义动作,自定义动作需要自己设置,即signal的第二参数是一函数指针,第一参数是将哪个信号,signal就是将指定信号设置为自定动作。代码执行结果:
关于闹钟我们补充一下,闹钟参数设置为0秒是取消前面设置的闹钟,并返回剩余秒数
四:键盘产生:
这个其实没什么好说的,比如在进行运行时我们执行ctrl+c操作,会终止该进程,实际上就是操作系统对进程发送了二号信号,并终止了该进程,我们同样可以通过signal函数进行自定义动作,让ctrl+c杀不死进程,代码如下:
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
using namespace std;
int cnt = 0;
void catchsig(int signal)
{
cout << "捕捉到信号为"<<signal << " cnt = "<<cnt<<endl;
}
int main(int argc,char* argv[])
{
signal(2,catchsig);
while(true)
{
cout << ++cnt << endl;
sleep(1);
}
return 0;
}
执行结果:
此时我们想杀死他就得通过kill 指令杀死,ctrl+c已经无效
五:硬件产生:
当硬件因为进程代码错误所产生的异常被操作系统识别到时,会向进程发送异常信号,我们常见的就是除零错误和野指针,我们在此呢,可以多聊一下这两个错误在硬件上的表现
除零错误:cpu内部只有加法器,在执行乘除及减法,也是通过加法实现的,例如乘法,就是加几次,减法就是加他的相反数,除法就是乘法的逆运算,看减多少次,当然这么说我们也就是大概理解一下,cpu内部不会像我们说的这么简单,在执行除法时,首先会将被除数和除数分别放到两个寄存器中,再将运算结果放在第三个寄存器里,如果除零,任何数除零都是无穷大,因为没办法算减0减几次,所以cpu内部有一个状态寄存器,如果cpu识别到因除零导致的数很大时,会造成溢出,那么状态寄存器的溢出标志位会被设置为1,一旦设置为1,操作系统会将该进程发送信号,并终止该进程,同样,我们可以自定义除零错误收到的信号,让进程可以不被杀死,但是,我们要知道,我们使用的操作系统是分时系统,操作系统会给每个进程分配对应的时间片,cpu只会将进程执行到时间片规定的时间并将其上下文保存后执行下一进程,上下文是包括执行该进程时状态寄存器信息的,所以当我们自定义除零错误动作后,虽然不会杀死该进程,但是cpu每次执行该进程时,状态寄存器的溢出标志位一直为1,所以操作系统也会一直给进程发送除零错误信号,而我们又没有办法修改cpu寄存器中的数值,所以即使不结束该进程,我们也没办法继续让该进程做别的动作,这就造成了资源浪费。
野指针:当我们定义一个int* p = nullptr,然后再*p = 100时,就会发生内存越界,我们在进程地址空间博客中提到,进程访问物理内存是通过地址空间加页表的映射进行访问的,当我们将*p来到页表进行映射时,cpu中有一个单元为内存管理单元(MMU),当我们进行野指针访问,MMU会察觉到异常,并设置相应的异常信号,操作系统会立刻察觉到MMU的异常并给进程发送内存越界的信号。
OK,以上就是信号产生的所有内容,下面我们要再稍微的聊一下部分细节
闹钟:
我们刚在进程中设置了一个闹钟,那么我们可以在一个进程中设置,其他人也可以在其他进程中设置闹钟,那么在同一时间,操作系统中可能存在很多闹钟,那操作系统应该怎么管理这些闹钟呢?其实很简单,管理的本质,就是先描述,再组织,我们可以自己推一下闹钟时怎么实现的,例如,既然是闹钟,我们应该知道超时的时间吧,所以我们需要记录一下多久超时,同样,我们需要知道时哪个进程设置的吧,那么我们需要一个pid,那么,闹钟响完一次还会再响吗,我们要记录一下他是一次性的还是周期性的,那么我们就可以将其写在结构体中:
struct alarm
{
usigned int when; //未来超时时间
pid_t pid;//哪个进程
int type;
}
描述好了,那我们怎么组织起来呢,答案很简单,我们可以通过小堆的方式将其管理起来,堆顶的闹钟一定是最先响的,操作系统只需要轮询时的对比当前时间戳和超时的时间戳即可,只要一相等,就向struct alarm中的pid发送闹钟响了的信号
核心转储:
当我们在man中查看各个信号的含义时,我们会看到下图:
我们看一下action中有不同的行为,ter'm是终止,ign是忽略,stop为停止,这些都好理解,那么core是什么呢?三号信号,SIGQUIT,是在内存越界时会被操作系统识别发出的异常信号,如果收到此信号,会在当前路径下生成core.pid文件,在使用gdb调试时,使用core命令调用该文件可直接定位问题所在,如果不生成core文件的话,可以看一下这篇文章
https://blog.csdn.net/qq_35621436/article/details/120870746
本篇完