文章目录
- 前言
- 一、信号是什么?
- 二、学习步骤
- 使用kill -l命令查看信号列表
- 可以看到有那么多信号,那么进程是如何识别这么多信号的呢?
- 使用kill命令终止进程
- 信号的捕捉
- kill函数
- raise函数
- abort函数
- Core dump
- 如何查看自己的核心转储功能是否被打开?
- 如何打开核心转储功能?
- 由软件条件产生信号
- 由硬件异常产生信号
前言
本章主要讲信号的产生与处理以及信号的作用,再延展出core dump的相关概念,信号对于进程十分重要。
一、信号是什么?
信号在我们的日常生活中也应用广泛,如信号灯,当绿灯亮起,我们就知道这条马路我们可以通行。我们今天所学习的信号也是同样的作用,是要让进程接受到不同信号所进行不同的处理方式。
二、学习步骤
使用kill -l命令查看信号列表
在之前学习进程状态的时候,我们就曾使用过kill命令来终止进程,而kill命令就是专门用来给进程发送信号的,那么常见的信号有哪些?
可以通过在shell输入
kill -l
可以看到有那么多信号,那么进程是如何识别这么多信号的呢?
在每一个进程的PCB中都存储相对应的信号位图结构,那么我们就可以大概知道,这种位图结构只需要由0置1就可以实现对信号的接受,而实际上也就是如此。
我们再来看上面这幅图,左边是它们的信号编号,右边则是它们的宏名称。
我们今天所讨论的是1-31的信号,其中34以上的信号为实时信号,不在我们本章的学习范畴中。
使用kill命令终止进程
我们可以先写一个死循环程序,使用kill终止进程
右侧在跑死循环程序,左侧使用ps axj命令查看运行进程的pid,再使用kill -2 pid命令来终止该进程。
所以这里我们就是对目标进程使用2号信号来终止该进程。
信号的捕捉
进程对于信号有三种处理方式:
1.默认处理
2.忽略(忽略也是一种处理方式,一般是等到合适的时间再进行信号处理)
3.自定义(捕捉信号)
对于信号的捕捉,系统给了我们这样一个接口函数,使用man 2 signal查看signal函数
这里的sighandler_t是函数指针。
signal函数的主要作用就是用于自定义捕捉信号,将signum的信号处理方式变换成handler的处理方式
代码如下(示例):
#include<iostream>
#include<cstdlib>
#include<unistd.h>
#include<signal.h>
void catchSign(int signum)
{
std::cout << "捕捉到" << signum << "号信号 pid:" << getpid() << std::endl;
}
int main()
{
signal(SIGINT,catchSign);
//signal(2,catchSign);
while(true)
{
std::cout << "hello " << getpid() << std::endl;
sleep(1);
}
return 0;
}
运行这一串代码之后,我们再尝试使用kill -2 pid 命令,就会发现
进程收到2号信号后不是终止进程,而是打印出我们catchSign的内容
然后如果你这个时候先想用ctrl+C来终止进程,你会发现也无法终止进程
这是为什么? 因为ctrl+C这一组合键就是被OS识别为向前台进程发送2号信号!
我们还可以使用ctrl+\来终止进程。而ctrl+\其实也是被OS识别为向前台进程发送3号信号。
kill函数
shell命令有kill命令,C语言也提供了一个kill函数,可以向pid进程发送sig号命令。
raise函数
这个函数也是C语言提供的函数,用于对自己这个进程发送sig信号。
abort函数
abort函数使当前进程接收到(6)SIGABRT信号而异常终止。
Core dump
通过在shell输入 man 7 signal我们可以看到
Term代表终止,那么Core呢? 我们刚刚也尝试使用了3号信号,进程仍然也终止了。这里的Core其实是指 触发该信号可能会发生Core dump(核心转储).
曾经我们在学习进程退出的时候,我们对于进程的退出状态有过探讨,如果进程由于收到信号异常退出,第8个bit位为core dump标志。 所以,核心转储到底是什么? 它其实是为了保护进程在运行过程中所产生的数据,将内存的数据转储到磁盘中, 这就是核心转储!
示例代码:
#include<iostream>
#include<cstdlib>
#include<unistd.h>
#include<signal.h>
#include<wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
sleep(1);
std::cout<<"子进程 " << getpid() << ":即将异常退出" << std::endl;
raise(SIGQUIT);
}
int status = 0;
waitpid(id, &status, 0);
std::cout << "父进程: " << getpid() << " 子进程: " << id << " exit sign: " <<\
(status & 0x7F) << " is core: " << ((status >> 7) & 1 ) << std::endl;
return 0;
}
这段代码主要是让子进程向自己发送3号信号,并且3号信号是Core,父进程用status接收子进程的退出状态,通过按位与和右移bit位的方式,我们分别可以得到子进程退出时收到的信号和Core dump位。
运行如下
奇怪了,为什么这里的core还是0呢? 实际上是因为我这里使用的是远端云服务器,而在云服务器上,核心转储功能是被默认关闭的!如果你使用的是虚拟机,虚拟机是默认打开的!
如何查看自己的核心转储功能是否被打开?
在shell输入ulimit -a命令
如何打开核心转储功能?
在shell输入ulimit -c size
需要注意的是core file的单位是block,因为要存储进程中所产生的数据还是要一定的磁盘空间来存储的,所以size不能太小。
注意:如果这里修改core file size 失败,尝试使用sudo来添加root权限。
这次我们打开了核心转储功能,再来试试上面的代码运行结果是否不同。
这次我们的core dump位成功为1,并且我们当前路径下还多出了一个core文件,该文件为二进制文件。
由软件条件产生信号
顾名思义,就是达到软件某种条件则会产生的信号,我们以alarm和SIGALRM信号为例。
先来看看alarm函数
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。
像alarm给进程设一个闹钟,当闹钟时间到了,达到了设定条件就发送信号,就叫做由软件条件产生信号。
通过alarm我们可以来测一测我们cpu每秒运算能力。
#include<iostream>
#include<cstdlib>
#include<unistd.h>
#include<signal.h>
#include<wait.h>
long long count = 0;
void catchALRM(int signum)
{
std::cout << "count: " << count << std::endl;
alarm(1);
}
int main()
{
signal(SIGALRM,catchALRM);
alarm(1);
while(1)
{
++count;
}
return 0;
}
这就是我的云服务器的cpu运算能力。
由硬件异常产生信号
对于我们而言最常见的就是除0的浮点数异常和野指针所产生的越界访问的异常。
这种其实是硬件上所发现的异常,然后再对进程发送终止信号。
先说SIGFPE浮点数异常信号,大家应该都知道我们程序内部进行的运算都是由cpu进行的,当出现除0时,cpu的状态寄存器会记录浮点数错误,然后OS在计算完毕后进行检查就会发送对进程发送SIGFPE信号。
野指针也是一样的,我们的进程中的虚拟内存的数据要映射到物理内存,会经过MMU(Memory Manager Unit内存管理单元)发现越界访问,也就发送对应的SIGSEGV信号,Segmentation violation段错误。