目录
理解信号
1. 生活角度的信号
你在网上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递。也就是你能"识别快递"
当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需5min之后才能去取快递。那么在在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不 是一定要立即执行,可以理解成"在合适的时候去取"。 在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间,你并没有拿到快递,但是你知道有一个快递已经来了。本质上是你"记住了有一个快递要去取"
当你时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种:
-
执行默认动作(幸福的打开快递,使用商品)
-
执行自定义动作(快递是零食,你要送给你你的女朋友)
-
忽略快递(快递拿上来之后,继续开一把游戏) 快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话
2、技术角度的信号
在我们的云服务器中,我没每登录一次shell就会让见一个bash子进程,由于Linux系统规定,每次只允许有一个前台进程,可有多个后台进程,当我们在运行前台进程的时候,可以在命令行上输入ctrl+c来让我们的程序终止,这也就是在发送终止信号
注意:
1、Ctrl-C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
-
Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl-C 这种控制键产生的信号。
-
前台进程在运行过程中用户随时可能按下 Ctrl-C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步 (Asynchronous)的。
信号的概念十分简单,就是:进程之间事件异步通知的一种方式,属于软中断。
根据上面我们所说的背景知识,将我们的进程当作人,快递当作信号,我们可以得出下面的结论:
1、进程必须能够**识别和处理信号,**需要存在处理信号的能力,属于进程内置功能
2、当进程收到信号时,进程可能不会立即处理,会在合适的时候进行处理
3、一个进程从收到信号,到处理信号之间的时间,叫做时间窗口,此时进程需要能够保存信号
4、信号处理的方式:a、默认动作(大部分为终止),b、忽略,c、自定义动作(动作的捕捉)
进程是如何从键盘得到信号的
当我们的计算机在设计的时候,为了方便检查我们外设上面的资源,采取了一种中断 的方法,中断就是指CPU暂停正在运行 的服务,转向 为中断源服务,结束之后在继续回到原来工作进行处理的一种方式,当我们的外设上面存在数据之后,我们的外设通过硬件(中断单元)的响应,让Cpu知道我们某一外设上存在数据,然后控制OS进行数据读取,然后送到用户缓冲区,当我们的操作系统对输入数据进行解析之后,发现是信号,就会在OS内维护的一张中断向量表内寻找中断方法。再次传递给我们进程。
信号的产生
信号产生有五种方式:
1、键盘组合键
可以通过键盘组合键来产生信号,例如ctrl+c(2号信号),ctrl+'\'(3号信号)
ctrl+c(2号信号),进程终止

ctrl+'\'(3号信号)

2、kill命令
Linux中 kill 命令可以向指定进程发送指定的信号,从1号到31号信号我们叫做普通信号,34号到64号叫做实时信号 ,其中缺少了32和33号。在1到31号信号中,不可以自定义的信号有9好和19号 ,其他的信号可以进行信号捕捉。

3、系统调用
这里介绍三个系统调用接口
signal
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
功能:进行信号捕捉,自定义信号操作(9号19号除外)
参数:signum:信号编号
handler:自定义函数
返回值可以忽略
kill
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
功能·:向指定进程发送指定的命令
参数:pid 进程的pid
sig 信号参数
返回值:成功为0,失败为-1
raise
#include <signal.h>
int raise(int sig);
功能:给自己发送指定信号
参数:sig 信号参数
返回值:成功为0,失败为 -1
abort
#include <stdlib.h>
void abort(void);
功能:给当前进程发送6号信号(注意:调用该函数时,就算捕捉了6号信号还是会退出进程)
4、异常
我们目前最熟悉的异常一个就是野指针和除以0了,在信号中分别对应11号和8号信号,当我们发送这样的异常时,尝试去捕捉8号信号
cpp
void myhandler(int sing)
{
cout << "process get signal : " << sing << endl;
}
int main()
{
signal(8, myhandler);
int a = 10;
int b = 0;
while (true)
{
a /= b;
cout<<"please cout me"<<endl;
sleep(1);
return 0;
}
运行结果如下:我们会发现,他会一直调用我们的handler函数,其他代码不会进行执行。

现象解释:在我们的进程在被调度的过程中,我们的CPU中存在一个状态寄存器,他会保存我们当前进程的状态,在其中有一个bit位代表着溢出,当我们除以零的时候,这个计算值就会趋近于武器大,导致溢出位异常,当我们的CPU每次运行这个进程的时候,都为带着这个进程的上下文来CPU中进行计算,当我们的溢出标志位一旦溢出,就会发送8号信号,但是我们对信号进行了捕捉,导致这个进程并没有终止,所以我们的溢出标志位一直存在,导致无论什么时候我们在运行这个进程的时候,都会检测溢出标志位,发送8号信号,导致我们自定义函数一直被调用.
5、软件条件
我们在管道的时候以及学过了SIGPIPE信号,它就是一种软件条件产生的信号,当我们的管道的读端被关闭的时候,写端会产生SIGPIPE信号来关闭进程
我们再来介绍一个函数alarm
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号,该信号的默认处理动作是终止当前进程。
这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。打个比方,某人要小睡一觉,设定闹钟为30分钟之后响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,"以前设定的闹钟时间还余下的时间"就是10分钟。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数
验证一下:
cpp
int main()
{
alarm(5);
while (true)
{
cout << "I an running" << endl;
sleep(1);
}
}
结果刚好在五秒后关闭
