提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
在生活中的信号,有早上的起床闹钟,过马路的红绿灯,快递的运输提示等等这些都可以称为信号,而对信号的处理也大都不相同。
一、信号的准备
在技术应用上的信号,是怎么来的呢?
可以肯定的是,肯定是程序员编写来的,那程序员是怎么编写的,在我们生活中,我们为什么知道过马路,红灯亮时,要停下,绿灯亮时可以过呢,这是因为我们已经提前,给这个信号,做了准备,做了默认处理,在技术应用上也是这样的,提前给这些信号,做了预处理,并提示这是第几号信号。
在Linux上,我们可以输入kill -l 来获取这些信号提示,一共是62个信号,没有32,33,没有0号信号
[1:31]号新号被称为普通信号
[34:64]信号中带有RT的称为实时信号
分时操作系统和实时操作系统
实时操作系统有严格的时序,需要立马严格地处理完成。
这里我们介绍一下,几个常见的信号。
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
while(1)
{
cout << "我正在循环打印这段话...." << endl;
sleep(1);
}
return 0;
}
我们在键盘上按control+c的快捷键发送的信号对应是2号信号,SIGINT,进程在收到该信号时,会进行默认处理,退出进程。
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
int a = 10;
while(1)
{
cout<<"我正在除零操作"<< endl;
a/=0;
}
return 0;
}
CPU有一个状态寄存器,CPU在做计算时,如果溢出了,状态寄存器就会被置为1,并立刻向进程发送8号信号,Floating point exception 浮点数异常 ,SIGFPE,执行收到该信号的默认处理,退出进程。
我们也可以不进行默认操作,通过信号捕捉,进行自定义处理。
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void Custom_processing(int signal)
{
while(true)
{
cout << "我捕捉到"<<signal<<"号信号,我的自定义处理方案是一直打印这句话"<<endl;
sleep(1);
}
}
int main()
{
//通过传递函数指针,我们捕捉到相对应的信号后
//进行我们自定义的处理方式
signal(8,Custom_processing);
int a = 10;
while(1)
{
cout<<"我正在除零操作"<< endl;
a/=0;
}
return 0;
}
这里为什么一直在打印呢,这是因为,CPU的状态寄存器,还是为1的,这个进程没有被退出,操作系统没有修复这个异常,只能通过其他信号终止进程。
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void Custom_processing(int signal)
{
while(true)
{
cout << "我捕捉到"<<signal<<"号信号,我的自定义处理方案是一直打印这句话"<<endl;
sleep(1);
}
}
int main()
{
signal(11,Custom_processing);
int* a = nullptr;
*a = 10;
return 0;
}
每一个进程都有一个独立的虚拟空间,通过页表映射到物理内存,而页表里面的MMU就是用来检查是否越界的,如果我们越界了,他会向进程的PCB发送信号11,如果我们的进程没有被退出,则信号一直存在。
操作系统还有一个更为方便我们检查出错原因的方法,这里要另外介绍一下,信号的另外一重含义,输入man 7 signal,对于一些信号,操作系统默认的处理是退出进程,而对于有一些信号,会在退出进程的同时,记录出错的上下文,并保存在当前路径下,文件名为core.pid,这个文件是可以用gdb用来调试。
如果你用的是云服务器,一般是不会开启这个功能的,需要输入 ulimit -a 指令
核心转储默认为零的,可以输入ulimit -c
加大小,就可以开启核心转储,默认编译是release模式,如果要进行debug模式编译的话,需要在makefile编译后面加-g,这时编译出来的文件,才支持gdb调试。
我们在进入gdb调试后,再次输入 core-file +core文件,这里再进行运行调试gdb会自动定位到产生异常的位置。
核心转储文件往往比较大,一般服务器是关闭了这个功能的。
二、信号的保存
信号的概念
- 实际执行信号的处理动作称为信号递达(Delivery)。
- 信号从产生到递达之间的状态,称为信号未决(Pending)。
- 进程可以选择阻塞 (Block )某个信号(即屏蔽某个信号)。
- 被阻塞(屏蔽)的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞(屏蔽),才执行递达的动作。
- 注意阻塞(屏蔽)和默认执行的是不同的,只要信号被阻塞(屏蔽)就不会递达,而默认执行是递达之后可选的一种处理方法。
操作系统在收到信号时的默认处理方法,保存在handler表里
三、信号的处理
操作系统提供了sigset_t 类型来帮助我们更好的,控制pending和block两个位图表。
这些是操作系统提供给我们的函数接口
#include <signal.h>//头文件
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
- 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,初始化该位图。
- sigfillset用来将参数set信号集初始化,然后把所有的信号加入到此信号集里即将所有的信号标志位置为1,屏蔽所有的信号。
- sigismember是用来判断,某个信号是否被置1,如果为1则返回1,为零则返回零
- sigaddset是用来增加某个信号,可以用来增加屏蔽信号
- sigdelset是用来删除某个信号,可以用来删除屏蔽信号
sigprocmask
通过我们设置好的block位图表去更改操作系统里面默认的block位图表,也可以用来读取。
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
//若操作成功返回0 ,否则返回-1
//how和set都是输入型参数,oset为输出型参数
-
如果oset是非空指针,则读取进程的当前信号屏蔽位图通过oset参数传出。
-
如果set是非空指针,则更改进程的信号屏蔽位图,参数how指示如何更改。
-
如果oset和set都是非空指针,则先将原来的信号屏蔽位图备份到oset里,然后根据set和how参数更改信号屏蔽位图。
假设当前的信号屏蔽字为mask,下表说明了how参数的可选值
SIG_BLOCK set包含了我们希望添加到当前屏蔽位图的信号,相当于mask=mask|set
SIG_UNBLOCK set包含了我们希望从当前屏蔽位图解除的信号,相当于mask=mask&(~set)
SIG_SETMASK 设置当前信号屏蔽字为set的值,即mask=set
sigpending
读取当前进程的信号集。
下面我们编写一段代码模拟一下
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
void show_block(sigset_t* set)
{
cout << "我是屏蔽位图表:";
for(int signal = 1;signal<32;++signal)
{
//判断0-31号信号是否有被屏蔽的情况
if(sigismember(set,signal))
{
cout <<"1"<<" ";
}
else
{
cout << "0"<<" ";
}
}
cout << endl;
}
void show_pending(sigset_t* set)
{
cout << "我是信号位图表:";
for(int signal = 1;signal<32;++signal)
{
//判断是否有收到0-31号信号
if(sigismember(set,signal))
{
cout <<"1"<<" ";
}
else
{
cout << "0"<<" ";
}
}
cout << endl;
}
int main()
{
sigset_t set;
sigset_t oset;
sigset_t pending;
//先初始化两个表
sigemptyset(&set);
sigemptyset(&oset);
//把2号信号添加到屏蔽位图里
sigaddset(&set,2);
//把我们的信号屏蔽位图表,添加到进程中的屏蔽位图表
sigprocmask(SIG_SETMASK,&set,&oset);
show_block(&set);
while(1)
{
//获取当前进程的信号集,即当前收到的信号
sigpending(&pending);
show_pending(&pending);
sleep(1);
}
return 0;
}
pending位图表示是否收到信号
运行可执行程序后,刚开始因为没有信号,所以pending表都是0,在使用2号信号想要干掉进程时,由于2号信号被阻塞, 无法终止进程 并且pending表中对应的2号信号的比特位出现1
只能通过其他信号终止该进程了,需要注意的是,即使我们把32个信号都屏蔽了,也还是有一个信号我们是无法屏蔽的,就是9号信号。