目录
1.预备知识
信号是给进程发送的。如果一个信号到来,进程会做什么?首先进程必须识别信号,识别信号有两个关键点:认识信号;收到此信号后要做的动作。通常,信号的识别由程序员通过代码处理。
在Linux下如何查看信号?使用如下命令:
kill -l ===>查看信号
其中,1~31称为普通信号,34~64称为实时信号。本片文章只谈论普通信号。
当某个进程收到一个信号后,它不一定马上处理信号,进程很可能在做其他任务。进程的任务执行和信号的产生是异步的。那么收到信号到处理这个信号之间是存在一个时间窗口的,也就是说进程必须记住这个信号,也就是说进程必须有对信号的保存能力(接收信号但不处理)。进程处理信号有三种动作:采用信号的默认动作;自定义信号的动作;忽略信号的动作。
信号保存在进程的进程控制块(PCB)中,其中有一个unsigned int signal字段,用位图结构描述信号。假设进程收到3号信号,那么就把第三个比特位由0置1。发送信号的本质就是修改pcb中的信号位图。因为pcb属于操作系统维护的数据结构对象,其管理者是操作系统,也就是说,只有操作系统才有权力修改位图结构。
信号的发送方式有多种,但无论是哪种,其本质都是通过操作系统向目标进程发送信号,也就是说,操作系统必须提供相关的系统调用。kill命令的底层也一定调用了系统调用。
2.信号的产生方式
2.1通过键盘
我们可以使用Ctrl+C热键组合来终止前台进程。其本质就是被操作系统识别,然后解释为2号信号。我们可以通过以下命令来查看详细的信号手册:
man 7 signal ===>查看信号手册
如果想让Ctrl+C热键组合不做信号的默认动作,我们可以使用signal系统调用来自定义信号的行为。
代码如下:
#include <iostream>
using namespace std;
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
void sigcb(int signo)
{
cout << "捕捉到信号:" << signo << endl;
}
int main()
{
signal(SIGINT,sigcb);
while(true)
{
cout << "我是一个进程 pid:" << getpid() << endl;
sleep(1);
}
return 0;
}
可以看到,Ctrl+C并没有终止掉前台进程,而是调用了我们自定义的函数。需要注意的是,signal这个接口仅仅是设置将要到来的信号的行为,也就是说,调用了signal之后,会将2号信号的行为更改未为我们自定义的行为,而不是直接执行我们自定义的行为。
2.2通过系统调用
操作系统提供了关于信号的接口,也就说明了操作系统发送信号的能力是为用户准备的,用户发起一个请求,操作系统再去执行。
最常用的系统调用便是kill命令(前面说过kill命令底层一定调用了系统调用),它可以向任意进程发送任意信号。
raise系统调用能够给自己发送任意信号,我们以一段代码演示:
#include <iostream>
using namespace std;
#include <signal.h>
#include <unistd.h>
int main()
{
while(true)
{
cout << "运行中..." << endl;
sleep(1);
raise(9);//给自己发送9号信号
}
return 0;
}
abort系统调用能够给自己发送指定信号(6号信号),我们以一段代码演示:
#include <iostream>
using namespace std;
#include <signal.h>
#include <unistd.h>
int main()
{
while(true)
{
cout << "运行中..." << endl;
sleep(1);
abort();
}
return 0;
}
大部分情况下,进程收到的大部分信号的默认动作都是终止进程。即使它们的动都相同,但是信号不以动作的同与不同划分,而是各种信号代表不同的时间,也就是说,不同的信号代表不同的终止原因。例如前面文章提到的管道,当管道的读端关闭时,写端就没有存在的意义了(操作系统不允许存在任何铺张浪费行为),此时操作系统向写端进程发送一个信号,进而终止进程。
2.3通过硬件异常产生信号
信号的产生不一定非得用户显式发送。当我们写一段程序,有除0操作。我们知道计算的过程是交给cpu的,那么操作系统为什么能够知道我们除0?还能给进程发送一个终止信号?
int main()
{
int a = 3;
a /= 0;
return 0;
}
其原因在于:cpu内部有一个状态寄存器,用来衡量某次运算的结果。这个寄存器有一个溢出标记位,当标记位溢出,则表示当前计算结果们没有意义,不需要被采纳。也就是说,除0会产生一个无穷大的值,从而导致状态寄存器的标记位溢出,然后发生cpu运算异常。而操作系统是要对所有硬件管理的,所以操作系统自然而然地知道了cpu发生了异常,然后操作系统又能对软件做管理,很轻松地找到了是谁导致地异常,然后再给导致异常的进程发送一个8号信号(我们看到的错误信息就是8号信号导致的)。
我们对上面的代码进行改造以下,将会看到一个暂时无法理解的现象:
#include <iostream>
using namespace std;
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
void mySig(int signo)
{
cout << "收到信号:" << signo << endl;
sleep(1);
}
int main()
{
signal(8,mySig);
while(true)
{
cout