信号的小细节真的很多~
文章目录
- 前言
- 一、信号的保存
- 总结
前言
首先我们先引出一个新的概念,叫核心转储。linux系统提供了一种能力,操作系统可以将一个进程在异常的时候将核心代码部分进行核心转储,将内存中进程的相关数据全部dump到磁盘中,一般这个文件会在当前进程的运行目录下,形成core.pid这样的二进制文件。当然如果我们使用的是云服务器的话,这个核心转储功能是默认关闭的,但是我们可以通过命令将这个功能打开:
使用命令:ulimit -a 查看当前系统中特定资源对应的上限
而我们圈出的core file size就是核心转储的功能,默认为0就是关闭状态,想要打开使用选项:
ulimit -c 10240就是将核心转储文件的大小设置为10240.
下面我们直接用一些信号发送给正常的代码使之异常退出看是否有核心转储文件生成:
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
void handler(int signo)
{
cout<<"我们的进程确实收到了"<<signo<<"号信号"<<endl;
exit(1);
}
int main()
{
while (true)
{
cout<<"我是一个正常的进程,正在模拟某种异常:"<<getpid()<<endl;
sleep(1);
}
return 0;
}
下面我们将程序运行起来然后发信号:
为什么没有生成核心转储文件呢?下面我们试试其他信号:
为什么8号信号就可以呢?还记得我们上一篇讲过信号的方式有term和core,term是终止,那么core是什么呢?其实term就是终止,以core终止会先进行核心转储,然后再终止进程。下面我们看一下刚刚的核心转储文件:
里面全是二进制乱码,也就是说这个文件不是给我们看的是给计算机看的,下面我们说一下核心转储有什么用:其实是为了在异常后方便进行调试。
首先将代码修改一下:
然后我们将makefile中的命令调成可调试状态:
只需要在g++指令后面加上-g选项,下面我们重新运行一下代码:
有了核心转储文件后下面我们用gdb进入调试模式:
然后我们直接输入指令:core-file +核心转储文件
这个时候gdb自动帮我们找到了报错的代码行数以及原因,这就验证了我们刚刚说的核心转储文件可以帮助我们在产生异常后方便进行调试,这种方案叫事后调试。当然为什么核心转储功能这么好用云服务器却要默认关闭呢?因为这个文件所占内存很大,一旦有多个出错每次都生成这样的核心转储文件那么服务器很容易挂掉,所以默认不支持打开核心转储功能,下面我们用指令将核心转储功能关闭:
我们只需要用指令:ulimit -c 0即可关闭:
下面我们讲一下系统如何识别核心转储的打开或关闭:
还记得我们之前讲的位图吗?我们的core标志位就在中间的那个比特位,如果这个位置的二进制为1则说明开启了核心转储功能,否则就是没有开始,要验证也很简单,只需要让子进程出现异常让父进程去接收,下面我们演示一下:
int main()
{
pid_t id = fork();
if (id==0)
{
cout<<"野指针问题 ....here"<<endl;
cout<<"野指针问题 ....here"<<endl;
cout<<"野指针问题 ....here"<<endl;
int* p = nullptr;
*p = 100; //对空指针进行解引用
cout<<"野指针问题........"<<endl;
cout<<"野指针问题 ....here"<<endl;
cout<<"野指针问题 ....here"<<endl;
exit(0);
}
int status = 0;
waitpid(id,&status,0);
cout<<"exit code: "<<((status>>8)&0xFF)<<endl;
cout<<"exit signal: "<<(status&0x7F)<<endl;
cout<<"core dump flag: "<<((status>>7)&0x1)<<endl;
return 0;
}
以上代码能在退出后给我们打印返回值,退出信号以及core标志位:
下面我们将核心转储重新打开我们再运行一下程序:
当我们重新运行程序后发现core dump的标志位变成了1也就是核心转储功能被打开了,并且我们成功拿到了核心转储文件:
以上就是核心转储的所有知识,下面我们进入信号的保存。
一、信号的保存和处理
阻塞信号
这个指针数组里存放的函数指针就是上图这样的,该数组的下标表示信号编号,数组的特定下标的内容表示该信号的递达动作。
下面我们看看信号递达的动作,比如执行和忽略:
int main()
{
signal(2,SIG_DFL);
while (true)
{
sleep(1);
}
return 0;
}
首先DFL的意思是默认,默认就是执行了意思就是说遇到2号信号就执行,下面我们运行一下:
键盘输入ctrl+c后就执行了2号信号,下面我们查看SIG_DFL的宏:
通过函数定义我们看到这个宏就是用函数指针实现的,下面我们试试忽略信号:
int main()
{
signal(2,SIG_IGN);
while (true)
{
sleep(1);
}
return 0;
}
IGN是ignore的缩写,就是忽略的意思,就是说我们遇到2号信号就忽略:
代码运行后确实将2号信号忽略了,然后我们用ctrl + \退出程序。
下面我们认识一下sigset_t:
认识了信号集后我们学习一下如何用信号集操作函数:
#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);
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值:
如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。
下面我们演示一下对2号信号进行屏蔽:
void showBlock(sigset_t *oset)
{
int signo = 1;
for (;signo<=31;signo++)
{
if (sigismember(oset,signo))
{
cout<<"1";
}
else
{
cout<<"0";
}
}
cout<<endl;
}
int main()
{
//只是在用户层面上进行设置
sigset_t set,oset;
sigemptyset(&set);
sigemptyset(&oset);
sigaddset(&set,2);
//设置进入进程,谁调用,设置谁
sigprocmask(SIG_SETMASK,&set,&oset); //1.2号信号没有反应2.我们看到老的信号屏蔽字block位图是全0
while (true)
{
showBlock(&oset);
sleep(1);
}
return 0;
}
下面我们先讲解一下代码,首先进入main函数我们创建两个对象,因为创建的对象或变量是在栈中存放,所以我们只是在用户层面上进行设置。然后我们先将新的信号集和旧的信号集初始化一下,初始化完成后将2号信号添加到新的信号集上去,这样就相当于屏蔽了2号信号。然后我们设置信号屏蔽字为set所指向的值就是谁调用这个进程谁就将二号信号屏蔽了,然后这个函数返回值返回老的信号屏蔽字,但是由于我们已经将信号屏蔽字初始化了所以老的信号屏蔽字block位图全是0.然后我们写一个死循环去显示老的信号屏蔽字的位图有哪些信号被设置了有哪些信号没有被设置,在这个函数中我们知道一共有31个信号,并且需要判断当前这个信号是否在老的信号集里,如果是就打印1如果不是就打印0,sigismember能判断signo这个信号是否在老的信号集里。
下面我们运行起来:
通过结果我们可以知道与我们所想的是一样的,下面我们修改一下代码将信号屏蔽字取消屏蔽:
int main()
{
//只是在用户层面上进行设置
sigset_t set,oset;
sigemptyset(&set);
sigemptyset(&oset);
sigaddset(&set,2);
//设置进入进程,谁调用,设置谁
sigprocmask(SIG_SETMASK,&set,&oset); //1.2号信号没有反应2.我们看到老的信号屏蔽字block位图是全0
int ***t = 1;
while (true)
{
showBlock(&oset);
sleep(1);
***t++;
if (***t==10)
{
sigprocmask(SIG_SETMASK,&oset,&set);
}
}
return 0;
}
我们设置一个计数器让计数器等于10的时候将进程的信号集恢复为oset也就是取消屏蔽2号信号,现象就是一开始我们按ctrl+c是没有反应的,但是到***t==10的时候2号屏蔽字恢复直接就终止程序了,下面我们看看现象:
我们可以看到现象与我们想的完全一样。
下面我们在认识一个新的接口:
sigpending函数是获取set的pending表,也就是说可以知道哪些信号是未决的,下面我们看看返回值:
如果成功则返回0如果失败返回-1,下面我们用函数重新写一下上面的代码并且引入新现象:
#include <iostream>
#include <signal.h>
#include <assert.h>
#include <unistd.h>
using namespace std;
static void PrintPending(const sigset_t &pending)
{
for (int signo=1;signo<=31;signo++)
{
//sigsimember可以判断signo信号是否在pending中存在
if (sigismember(&pending,signo))
{
cout<<"1";
}
else
{
cout<<"0";
}
}
cout<<endl;
}
int main()
{
//1.屏蔽2号信号
sigset_t set,oset;
// 1.1初始化
sigemptyset(&set);
sigemptyset(&oset);
// 1.2将2号信号添加到set中
sigaddset(&set,2);
// 1.3将新的信号屏蔽字设置至进程
sigprocmask(SIG_BLOCK,&set,&oset);
//2. while获取进程的pending信号集合,并以01打印
while (true)
{
// 2.1 先获取pending信号集
sigset_t pending;
//初始化pending
sigemptyset(&pending);
int n = sigpending(&pending);
assert(n==0);
(void)n; //保证在release模式下不会出现编译时的warning
// 2.2 打印,方便我们查看
PrintPending(pending);
//2.3休眠一下
sleep(1);
}
return 0;
}
这段代码的现象是:因为有block和pending位图,当我们将某个信号block后这个信号就不会被递答了,然后我们给进程发送这个信号,一旦发送那么这个信号在pending表中的比特位就会被修改为1,然后我们就可以看到pending表中2号信号位由0变1的过程。
上面的代码与前面那个演示代码非常相似,并且该注释的我们都注释了,下面我们直接运行起来看看现象:
现象和我们说的一样,当然我们输入ctrl+c也是一样的。因为我们将2号信号进行了屏蔽,即使我们发送了2号信号但是2号信号不会被递达,只能留在pending位图里,下面我们让代码在10秒后恢复被屏蔽的信号并且必须看到pending位图从1变成0:
//2. while获取进程的pending信号集合,并以01打印
int ***t = 0;
while (true)
{
// 2.1 先获取pending信号集
sigset_t pending;
//初始化pending
sigemptyset(&pending);
int n = sigpending(&pending);
assert(n==0);
(void)n; //保证在release模式下不会出现编译时的warning
// 2.2 打印,方便我们查看
PrintPending(pending);
//2.3休眠一下
sleep(1);
//2.4 10s之后,恢复对所有信号的block动作
if (++***t==10)
{
cout<<"解除对2号信号的屏蔽"<<endl; //先打印
sigprocmask(SIG_SETMASK,&oset,nullptr);
}
}
由于我们在倒计时结束后已经将之前老的信号屏蔽字设置为进程,所以这次我们不需要老的信号屏蔽字了直接设为nullptr即可。下面我们看一下现象:
为什么与我们预期的不一样呢,我们想要看到的pending表呢?这是因为2号信号由阻塞状态修改为解除屏蔽后2号信号直接终止进程了,所以我们是看不到现象的,要看到现象我们需要对2号信号进行捕捉:
static void handler(int signo)
{
cout<<"拦截到"<<signo<<"信号"<<endl;
}
signal(2,handler);
while (true)
{
// 2.1 先获取pending信号集
sigset_t pending;
//初始化pending
sigemptyset(&pending);
int n = sigpending(&pending);
assert(n==0);
(void)n; //保证在release模式下不会出现编译时的warning
// 2.2 打印,方便我们查看
PrintPending(pending);
//2.3休眠一下
sleep(1);
//2.4 10s之后,恢复对所有信号的block动作
if (++***t==10)
{
cout<<"解除对2号信号的屏蔽"<<endl; //先打印
sigprocmask(SIG_SETMASK,&oset,nullptr);
}
}
下面我们将程序运行起来:
可以看到这次的现象就与我们预期的现象一样了,一开始将2号信号进行了阻塞,然后当我们发送2号信号的时候信号保存在pending表中,等10s后解除2号信号的屏蔽了然后我们立即捕捉这个信号,然后循环继续打印pending表,此时2号信号已经递达所以2号信号的位置由1变成0.
总结
以上就是信号的保存的内容,现在我们已经学会了信号的产生,信号的保存,下一篇文章我们将详细介绍信号的处理。