本章目标
1.信号产生
1.信号产生
1.1由异常而产生的信号
在之前的学习当中,我们的进程一旦出现了除0,空指针解引用就会报错,从而导致进程崩溃?
那为什么会出现这种情况.最本质的原因是os向进程写入了对应的信号,从而触发终止进程的默认行为

在这里面,我们能够找到我们的老朋友, 11号信号段错误,这个一般是由空指针解引用触发的,8号错误 浮点数报错,除零错误经常容易触发这个信号
空口无凭,我们现在写一个测试代码来触发这两个信号
cpp
#include<iostream>
#include<signal.h>
#include<unistd.h>
void hander(int signo)
{
std::cout<<"float point error"<<std::endl;
}
int main()
{
signal(SIGFPE,hander);
sleep(1);
int a = 10;
sleep(2);
a=a/0;
return 0;
}
我们用信号捕捉去捕捉8号信号,如果下面的a/10报错,就会执行我们自定义的hander方法

在这里我们看到了我们的浮点数报错了.同时观察我们的测试代码,我们并没有循环,但出现了死循环的的情况
我们成功的看到了浮点数报错,是因为进程触发了信号,从而终止进程了.
但是我们的os是如何知道什么时候向进程写入信号,知道发生浮点数报错的呢
实际上div 除0错误本质上是硬件错误.
我们的计算逻辑总共分为两种逻辑运算和算术运算.
这两个操作都是由我们的cpu执行的

在我们的cpu内部除了通用寄存器,cr3这类控制寄存器.实际上还会包含一个标志寄存器.这个标志寄存器可以理解成一个位图,它的内部有一位表示这是否出现浮点数报错.当我们进行运算的时候,我们的操作系统要将内存的数据写入到cpu的寄存器当中,也就是我们的上下文数据.
在cpu进行运算之后会把结果保存到另一个寄存器当中.同时如果出现错误会将cpu内部的标志位由0变为1.cpu会暂停当前指令的执行,向操作系统触发对应的中断,操作系统会得知当前出现了硬件报错了,os会检查这个寄存器如果发现为1的位,就会认为当前的计算结果实际上是不可信的,操作系统会根据当前结果向当前正在执行的进程的信号位图标志位写入对应的信号从而触发对应信号的默认行为,终止进程.
为什么操作系统知道是哪个进程的?
因为在同一时刻,操作系统只有一个进程会运行,它被一个叫做current的task_struct 类型的指针指向.操作系统也就能够找到了

为什么我们上面的会出现死循环?
这主要是我们自定义捕捉了8号信号,正常来说8号信号的默认行为是终止进程,但是我们我们自定义捕捉的时候会覆盖当前信号的行为.而cpu会重新执行这行代码.每一次都是除0错误回到上面我们所说的逻辑.于是就看到死循环了.
核心转储(core dump)
我们用man 7 signal 查看一下这两个信号


他们的信号行为一个是core 一种是 term
一般来说由进程自身所产生的信号类型是core 而由外部对进程产生的信号就是 term
而core类型的信号会触发核心转储



我们在前面所说的2号信号就是term类型,而3号信号就是core类型.他们最大的区别就是是否能够触发核心转储.
那么什么是核心转储?
就是将进程出错时产生的异常信息产生一个文件转存到磁盘当中.
那我们为什么没有看到这个所谓的核心转储的文件呢?
在如今的云服务器时代.这个功能已经提前被服务器厂商关闭了

我们可以通过ulimit -a 这个命令去查看队形的信息. 在这个里面我们看到 core file size为0, 我们可以通过它给我们的 -c 选项去调整大小

它最后产生的文件要通过我们的cgdb进行调试所以我们要在makefile当中增加一个 -g 来支持调试

我们可以通过coredumpctl info 的方式去查看最近一个核心转储的信息
这个命令是第三方命令需要下载
bash
sudo apt install systemd-coredump

bash
/var/lib/systemd/coredump/core.test.1001.7fa379cdf1be4af0b0059f8bdb03d2e2.561186.1771152604000000.zst
在Ubuntu 22 04的系统我们可以在这个目录下找到对应的核心转储的位置
bash
coredumpctl gdb
可以用这种方式直接打开对应的核心转储文件

这个命令最好使用完看到结果就给它删掉,否则仍会产生核心转储文件.
bash
apt remove -y systemd-coredump

除了第三方软件还可以通过
bash
ulimit -c unlimited
这种方式打开
在gdb 或者cgdb当中可以通过 core-file 的方式+核心转储文件的方式打开对应的文件定位错误
bash
core-file xxxxx
为什么我们今天的服务器要关闭核心转储这个东西?
看到上面你可能觉得这个东西是相当的方便的,但是我们的服务器一般提供给我们用来是做部署服务的.可以想象一种场景.半夜你的服务器崩掉了,它一秒钟触发一次核心转储.第二天起来你的系统可能已经打不开了.因为全被核心转储占满了.
我们的bash是如何知道我们的进程设置核心转储了呢?
在前面进程章节,我们提到过进程退出码一共被分为了两个部分

退出码和信号.高8位给退出码,低7位给信号.而第8位就是核心转储的标志位.
而我们的所以进程都是bash的子进程.bash自然是能够拿到子进程的退出信息.自然也就知道了coredump是否被标记了
////////////////////////////////////////////////////////////////////////////////////////////////////////
我们在前面提到了的中断,会再信号处理部分介绍,
////////////////////////////////////////////////////////////////////////////////////////////////////////
1.2由函数(系统调用)产生的信号
1.kill函数
kill不仅有对应的命令,同样有对应的函数

kill 函数可以向对应的进程发送对应的信号信息.
我们用一下这个函数,我们实现我们自己的kill命令

cpp
#include<iostream>
#include<sys/types.h>
#include<signal.h>
int main(int argc ,char* argv[])
{
if(argc!=3)
{
std::cerr<<"./test signal pid"<<std::endl;
exit(1);
}
kill(std::stoi(argv[2]),std::stoi(argv[1]));
return 0;
}
2.raise

这个函数自自己给自己发信号类似于
kill(getpid(),signal);
cpp
#include<iostream>
#include<signal.h>
#include<sys/types.h>
void handler(int signumber)
{
// 整个代码就只有这⼀处打印
std::cout << "获取了⼀个信号: " << signumber << std::endl;
}
int main()
{
signal(2,hander);
while(1)
{
sleep(1);
raise(2);
}
return 0;
}
3.abort

这个函数可以向当前进程发送6号信号.默认行为是终止进程,它一般出现在开源项目比较多.
因为它会触发核心转储,而我们常用的exit并不会触发.
同时这个信号即使被捕捉了.也会触发终止进程的行为.
我们管这种操作叫做核心信号行为

cpp
#include<iostream>
#include<signal.h>
#include<sys/types.h>
#include<unistd.h>
void handler(int signo)
{
std::cout<<"aaaaaaaa"<<std::endl;
}
int main()
{
signal(SIGABRT, handler);
while (true)
{
sleep(1);
abort();
}
}
1.3由软件条件产生信号
所谓的由软件条件产生主要指的是不会通过异常,外部设备,主动调用系统调用的函数.而由我们的自己的软件达成了某种条件从而自主的触发了信号.
最典型的例子就是我们之前的管道.当读端关闭的时候,管道会被自己关闭,因为操作系统是不允许一块无用的文件或内存块存在的.这种情况我们叫做管道破裂他会触发13号信号SIGPIPE

在这里我们介绍一个信号

sigalrm 这个信号代表的是一个计时器
我们可以通过

这个alarm 函数去触发这个闹钟.参数的意思是隔多少秒进行触发.
返回值是闹钟剩余多少时间.举个例子我设置了5秒的闹钟,3秒的时候我通过其他的方式,kill命令去发送了14号信号,那么这个函数就会给我们返回2秒
这个slgalrm这个信号默认行为是终止进程
我们可以用这个函数来体会io速度的差别以及模拟操作系统.后者我们在说信号处理的时候再介绍

cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
int main()
{
int count = 0;
alarm(1);
while (true)
{
std::cout << "count : "
<< count << std::endl;
count++;
}
return 0;
}

cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
int count = 0;
void handler(int signumber)
{
std::cout << "count : " << count << std::endl;
exit(0);
}
int main()
{
signal(SIGALRM, handler);
alarm(1);
while (true)
{
count++;
}
return 0;
}
图1是带io的,图2是不带io的,很容易就能够看出来io是一种很慢的操作.因为要涉及刷新缓冲区的问题.纯计算能跟它拉开好几个数量级