小编个人主页详情<—请点击
小编个人gitee代码仓库<—请点击
linux系列专栏<—请点击
倘若命中无此运,孤身亦可登昆仑,送给屏幕面前的读者朋友们和小编自己!
目录
前言
【linux】进程间通信(四)消息队列、信号量原理讲解,IPC在内核中的数据结构设计——书接上文 详情请点击<——,本文会在上文的基础上进行讲解,所以对上文不了解的读者友友请点击前方的蓝字链接进行学习
本文由小编为大家介绍——【linux】信号(一)信号的产生
一、场景引入
- 小编将按照下面的知识线路为大家讲解信号,那么首先我们场景的引入

- 小编先以生活中的例子举例讲解,假设你是一名资深的宅男,并且对王者荣耀元歌有十分强的热爱,现在是晚饭时间,现在的你肚子饿了,在饿了么上点了一份外卖,点完之后,你开了一把王者荣耀巅峰赛,开始对局,对局到15分钟的时候,我方攻上敌方高地,此时饿了么通知,您的外卖到了,请下楼来取,此时你接收到信息会不会立即去取?
- 我现在不会立即去取因为我还有更重要的事情去做,这个更重要的事情即打游戏,我方都推到敌方高地了,我现在哪有时间去取外卖外卖,此时此刻外卖哪有我打游戏重要,这时候你心中默默记下一会打完游戏要去取外卖,这时候你手机切屏,回复道不好意思,请放到楼下1433223号外卖柜,于是骑手就放到你指定的外卖柜,2分钟过后,一声语音播报“Victory”胜利,你如释重负,赢了,终于将元歌打上了小国,游戏结束但是你始终记着还有外卖没拿,于是,下楼拿外卖,拿好外卖,打开快手,边吃外卖边在王者荣耀边路之怪听安的直播间继续学习元歌,心中不禁感慨,生活多么惬意!

- 这个过程中,我收到饿了么的信息,是信号的产生,但是我可能并不会立即下楼拿外卖,因为我还有更重要的事情(打游戏)去做,这个过程中,我一定是将这个信号记忆下来了,这个就是信号的保存,当我忙完现在我认为重要的事情(打游戏)之后,此时我始终记着我忙完我认为重要的事情之后要去拿外卖,此时我下楼去拿外卖,这属于信号的处理
- 同样的linux中信号的生命周期也是如此,即信号的产生,信号的保存,信号的处理
- 但是有一点需要强调,当我收到外卖之后,我可不可以立即去拿外卖?我可以立即去拿外卖,但是我不去拿,因为此时我可能还有更重要的事情去做,同样的,当进程收到信号的时候,进程也可以立即去处理信号,但是同样的,进程也可能有更重要的事情要去做,例如:IO写入或读取磁盘,实时任务的处理(例如该进程与刹车系统有关)等,所以进程在这个场景下不会处理信号,而是先将信号保存起来,等待合适的时机再去处理信号,当进程将更重要的事情忙完了,再去处理信号
二、预备工作
生活场景引入
- 接下来,小编带领大家进入信号的预备工作阶段,在这个阶段小编将会输出一堆结论,支持我们对信号的理解

- 其实信号在我们的生活中就有很多场景,我们可以举例出来很多,例如:红绿灯,闹钟,发令枪,古代的狼烟,外卖电话,快递取件码的信息,信号枪等等
- 但是为什么我们会认识这些信号?因为有人教我们,我们记住了这些常见的信号
- 如何理解认识?认识即 1. 认识/辨别信号,2. 知道信号的处理方法
- 那么此时对于我们来讲,即便现在我们周围没有上述信号发生,我们也能知道信号产生了,我们该做什么
- 信号产生了,我们可能并不会立即处理信号,而是在合适的时机去处理信号,因为我们此时可能有更重要的事情去做,所以如果我们不立即处理信号,那么信号产生到信号处理的中间一定会有一个时间窗口,在这个事件窗口内,我们必须记住信号
- 那么上述小编用到的“我们”,在生活中指的是我们自己,但是在计算机世界中指的是谁?指的是进程,所以我们就可以将上述的我们换成进程得出进程信号的结论
信号结论
- 进程必须 能识别 + 能够处理信号,信号没有产生,也要具备处理信号的能力。信号的处理能力,属于进程内置功能的一部分
- 进程即便是没有收到信号,也要知道哪些信号应该如何处理
- 当进程真的收到了一个具体的信号的时候,进程可能不会立即处理该信号,而是在合适的时机处理该信号
- 信号如何处理?1. 默认动作(操作系统给进程内置的默认动作,例如:当我们想要杀死一个正在运行进程,那么在另一个命令行中输入,kill -9 pid即可,那么此时这个进程收到9号信号,那么当进程收到9号进程的时候,默认动作就是终止自己这个进程),2. 忽略(进程忽视信号,忽视同样也是一种处理方式) 3. 自定义动作(signal信号的捕捉)
- 一个进程如果收到一个信号,此时进程有更重要的事情去做,所以此时进程不立即处理这个信号,那么从信号产生到信号被处理的这个过程中,一定会有时间窗口,进程具有临时保存哪些信号已经发生了的能力
ctrl + c的现象
- 当我们学习linux的时候,就已经学到如果想要杀死一个正在运行的进程可以按下ctrl + c,例如如下死循环的代码
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
while(true)
{
cout << "hello linux, pid: " << getpid() << endl;
sleep(1);
}
return 0;
}
运行结果如下
此时我们按下ctrl + c可以杀死一个正在运行的进程
- 那么如果我运行进程的时候给进程后加一个&,即./myprocess &,那么此时我按下ctrl + c却无法退出这个正在运行的进程了,这是为什么呢?
运行结果如下
此时我们按下ctrl + c无法杀死一个正在运行的进程
- linux中,一次登录,也就对应的是一个终端,一个终端对应的会配备一个bash,linux设计只允许一个进程是前台进程,可以允许多个进程是后台进程。有点类似于手机主屏幕打开一个应用程序,但是多个应用程序可以在后台运行(不考虑分屏,小窗)
- 前台进程和后台进程的区别就是谁来获取键盘输入,前台进程可以获取键盘输入,后台程序不可以获取键盘输入
- 诸如./myprocess运行可执行程序,那么此时可执行程序myprocess就是前台进程,而它的父进程bash则成为后台进程,所以我们按下ctrl + c可以杀掉前台进程myprocess
- 诸如./myprocess & 运行可执行程序,./myprocess搭配&,这个&可以让运行的进程变成后台进程,所以此时myprocess就是后台进程,后台程序是无法获取键盘输入的,所以ctrl + c自然无法输入给后台进程myprocess,所以自然的我们无法使用ctrl + c杀死后台进程,那么如何杀死后台进程另起一个命令行,在命令行中输入kill -9 后台进程pid即可杀死后台进程
- 那么既然myprocess成为了后台进程,此时前台进程是谁?我键盘的输入又输入给了谁?前台进程仍然是bash,因为一进入终端,默认bash就是前台进程,可以用来接收键盘输入,并且./myprocess & 运行可执行程序,此时myprocess就是后台进程,myprocess不是前台进程,没有改变bash,所以bash仍然是前台进程,也就是说我们呢的输入输入给了bash喽,那么我是不是输入命令仍然可以执行?是的
运行结果如下
- 此时小编在命令行中输入ls,按下回车,确实bash接收到了我们的命令,指令了ls指令,但是小编故意的输入pwd命名,先按下pw,再按下d仍然可以运行pwd指令,如何理解呢?
- 因为我们输入pwd指令其实是两件事情,一件事是将键盘输入pwd写到0号文件描述符对应的键盘文件缓冲区,bash从标准输入0号文件描述符对应的键盘文件缓冲区中读取pwd,另一件事情则是将pwd字符串回显给用户,这个回显的工作是将pwd写入到标准输出1号文件描述符对应的显示器文件缓冲区,并且将对应的数据刷新到显示器上进行显示,这两个文件缓冲区互不干扰,那么至于为什么屏幕上既显示pwhello linux,pid 23010,又显示d,那么则是由于我们没有对显示器这个公共资源做互斥工作,即显示器文件不是临界资源,进程输出和键盘回显同时显示器文件进行写入,才会造成显示器显示字符混乱的现象,至于我们先输入pw,再输入d没区别,最终都是按下空格的时候被bash进行读取,所以bash读取上来的仍然是pwd,向显示器回显以及bash读取键盘文件缓冲区并不冲突,互不干扰
- 小编,我还有一个疑问,当我们以./myprocess &,运行myprocess之后,myprocess此时是后台进程,那么前台进程是bash,bash也是一个进程,我们从键盘输入ctrl + c之后,这个ctrl + c被前台进程bash获取,那么为什么bash不退出?以及类似的右图,当仅仅有bash进程运行的时候,我们在键盘上输入ctrl + c,为什么bash不退出?


- 其实是由于bash是用于服务用户,获取用户输入与操作系统交互的一层外壳程序,所以bash的作用天然决定了bash不能退出,它要一直运行,不断的获取用户输入,所以当我们给bash输入ctrl + c的时候,bash进程内部对于ctrl + c进行了特殊处理,当遇到ctrl + c的时候不退出bash,bash进程保持运行,并且继续显示bash命令行等待用户的输入
ctrl + c的理解
- 其实我们在键盘上输入ctrl + c的时候本质是被进程解释收到了信号,几号信号?2号信号SIGINT,其中这个SIG是前缀表示信号的意思,INT其实是英语单词interrupt的所以,意思是打断,即打断当前进程的运行,进程的运行被打断了之后进程也就自然而然退出终止了,我们使用 kill -l 可以查看linux中信号的种类

- linux的信号中读者友友可以数一下一共有多少个?一共有62个信号,因为没有32号信号,没有33号信号。其中1到31号信号是普通信号,34到64信号是实时信号。当进程收到的实时信号的时候,需要立马处理实时信号。我们仅研究普通信号。
signal
- 小编,空口无评,你如何证明呢?如何证明我们输入ctrl + c之后,进程确实是收到了2号信号才退出的。我有一计,那么就是自定义动作,即使用signal修改进程对于特定信号的处理动作,既然进程收到2号信号的默认动作是打断自己的运行,自然的正在运行的进程就终止了,我们可以使用signal修改这个默认动作,修改为我们定义的自定义动作,比如打印但是不退出,下面我们来认识一下signal这个函数

- 首先我们观察系统调用signal,这个函数的参数第一个是signum,signum需要我们传入设置信号的编号,这里我们就应该意识到什么了,信号本质都是数字,那么为什么会有诸如SIGHUP,SIGINT之类的呢?其实这些诸如SIGHUP,SIGINT之类也就是宏定义罢了,所以这些宏定义的本质也就是数字


- 所以signal的第一个参数我们可以传入一个数字编号,同样的也可以传入这个数字对应的宏定义,例如2号信号对应的宏定义是SIGINT,所以signal的第一个参数我们可以传入2,也可以传入SIGINT,本质都是数字,那么signal的第二个参数handler是一个被typedef的一个参数为int,返回值为void的一个函数指针类型,所以这应该就是我们想要让进程关于这个信号执行的自定义动作了,这个handler的参数是int,这个int一定要用于接收信号编号,因为可能不同的信号可以执行同一个自定义动作,而自定义动作中可能有关于信号的编号打印之类的动作,不同的信号编号不同,所以handler的参数用于接收信号的编号,当进程真的收到了信号后,会将信号传入给handler进行调用
- signal系统调用只需要调用设置一次,设置一次后,此时在进程这一次的生命周期内,当收到信号的时候,就不会去执行操作系统为该进程收到信号内置的动作,而是会去执行我们使用signali调用设置的自定义动作handler
- 同时我们还应该知道,signal仅仅是设置对应的信号执行自定义动作,如果进程没有收到对应的信号,那么这个自定义动作将不会执行
#include <iostream>
#include <unistd.h>
#include <csignal>
using namespace std;
void handler(int signo)
{
cout << "process get a signal: " << signo << endl;
}
int main()
{
signal(SIGINT, handler);
while(true)
{
cout << "hello linux, pid: " << getpid() << endl;
sleep(1);
}
return 0;
}
运行结果如下
- 此时观察上述现象,当小编以./myprocess运行可执行程序的时候,myprocess是前台进程,可以接收用户键盘的输入,那么此时小编在键盘上输入ctrl + c,那么此时进程就不会执行默认动作了,而是会去执行我们的自定义打印不退出动作,那么此时如何退出呢?很简单kill -9 pid即可杀掉进程

- 此时我们已经验证了,使用signal可以更改进程对于信号的默认处理动作,当进程收到对应的信号的时候,会去执行我们指定给用户的自定义动作handler,并且还验证了我们在键盘上输入ctrl + c之后,此时前台进程会收到2号信号SIGINT
- 那么我们给我们的自定义动作handler设置为打印并退出终止进程,如何退出终止进程exit即可
#include <iostream>
#include <unistd.h>
#include <csignal>
#include <cstdlib>
using namespace std;
void handler(int signo)
{
cout << "process get a signal: " << signo << endl;
exit(1);
}
int main()
{
signal(SIGINT, handler);
while(true)
{
cout << "hello linux, pid: " << getpid() << endl;
sleep(1);
}
return 0;
}
运行结果如下
此时小编在键盘上输入ctrl + c,进程会去执行我们signal设置的自定义动作handler,打印并退出终止进程
键盘数据如何输入给内核,ctrl + c如何变为信号的——谈谈硬件
- 键盘被按下,键盘输入数据,键盘是外设,外设不和CPU打交道,外设和内存打交道,操作系统管理内存,那么操作系统要将数据从键盘拷贝到键盘文件对应的内核级别的文件缓冲区上,由于是内核级别的缓冲区,所以操作系统要知道键盘被按下才能进行数据的拷贝,那么问题来了,操作系统如何得知键盘上有数据?如下图

- 操作系统也是程序,操作系统要被加载到内存中才能运行,所以首先操作系统加载到内存中,同样的内存中还有用户层,用户层中有用户缓冲区,最左边是硬件设备,键盘,键盘上有很多的按键,这里小编就画了一个按键代指用户想要按下的键位进行讲解,上图的下面是CPU,CPU表面有很多的针脚,针脚实物图如下,这些CPU针脚和主板相连,主板对应外设的位置也有外设对应的针脚,所以外设对应的针脚可以与CPU的针脚进行交互


- 那么作为用户,我们使用键盘进行输入,例如输入的是abcd回车,当我们按下回车的时候,此时会通过中断单元触发硬件中断,那么此时键盘对应CPU上就会有对应的针脚有高电频,用于表征这个针脚对应的硬件发生硬件中断,假设键盘对应的针脚号是5,那么此时CPU的寄存器就会获取针脚上的高电频转换成中断向量号5进行暂时的存储,这里就有一个疑问了,为什么CPU寄存器可以存储数据?实际上是寄存器充放电01序列进行表征数据。
- 当CPU寄存器获取到数据的时候,CPU就会去与操作系统进行交互,操作系统的头部会维护一张中断向量表,这个中断向量表中存储的是硬件读写的方法的地址,这些读写方法的地址会指向外设的读写方法,例如:磁盘,显示器,键盘,网卡的读写方法等,假设数组下标5号位置存储的就是键盘的读方法,此时CPU寄存器上的数据,即中断向量号5就会传输给操作系统,那么操作系统就会去操作系统头部的中断向量表中的5号下标处找到对应的方法进行调用,那么此时假设5号位置对应的方法是键盘的读方法
- 由于操作系统是硬件的管理者,所以就会去找到对应的键盘,执行键盘的读方法,所以此时键盘上我们输入的数据abcd就会从键盘拷贝到操作系统的键盘文件对应的文件缓冲区中,由于键盘是外设IO资源,那么由于操作系统内有很多进程想要访问键盘,但是键盘没有数据,键盘未就绪,所以进程只能在键盘描述对象的阻塞队列中排队,此时键盘对应的文件缓冲区中有数据了,所以此时操作系统就会将键盘描述对象的状态调整为就绪,所以阻塞的进程此时状态就被调整为运行,进程被调度就可以通过0号文件描述符从键盘文件的缓冲区中读取数据,所以此时数据就从内核的文件缓冲区读取到了用户层的用户缓冲区
- 同时键盘上不仅仅可以输入字符信息,还可以输入组合键,这些组合键有些可以控制进程,例如ctrl+c,那么当调用键盘的读方法的时候,这个ctrl+c不能放到内核的键盘文件对应的缓冲区中,因为诸如ctrl+c这类的组合键的作用是控制进程的,所以操作系统在调用键盘的读方法读取数据的时候,会对数据进行检查,如果数据是类似于ctrl+c的组合键,那么此时操作系统就要拦截这个ctrl+c,不要让这个组合键写入内核的键盘文件对应的缓冲区中,转而将这个ctrl+c转化为2号信号,发送给对应的进程
- 操作系统可是软硬件资源的管理者,那么操作系统就要管理计算机所有的外设硬件,如何管理,先描述在组织,使用struct结构体描述硬件的属性,之后再将硬件描述对象采用某种数据结构组织起来,这样对硬件的管理就转化为了对某种数据结构的增删查改,但是外设硬件是有状态的,同时还有一些输入数据类似的,难道计算机硬件这么多,我操作系统每隔一段时间都要将所有硬件都扫描一遍,每隔一段时间扫描一遍,那么扫描的频率又该如何设置呢?要知道这些外有的可能是随时都会有可能都会有数据的,例如:键盘,你操作系统知道键盘上什么时候会有数据吗?
- 不,你操作系统不知道,只有用户知道自己什么时候使用键盘,所以如果操作系统操作系统每隔一段时间都要将所有硬件都扫描一遍,那么就是与外设进行交互,第一点效率肯定很慢,那么这么来回的扫描所有的外设那么我操作系统难道不做其它的事情吗?不,操作系统肯定是要去做其它事情的,那么如果外设上有数据了,我操作系统该如何办呢?很简单那,通过类似于上面键盘这种硬件中断的方式,让所有的外设如果有数据了都进行硬件中断即可,此时操作系统就不必每隔一段时间都要将所有硬件都扫描一遍,只需要等待硬件发生硬件中断,对应的CPU上的寄存器获取到数据,与操作系统进行交互调用对应的方法即可
- 同样的我们也可以理解一点,当进程向磁盘中进行写入数据或者从磁盘中进行读取数据的时候的时候,以进程向磁盘中写入数据为例,当进程向磁盘中读取数据的时候,此时进程的状态就要变成阻塞状态,进程知道什么时候数据写入完成吗?不知道,操作系统知道吗?不知道,那么谁知道?
- 磁盘知道,那么此时磁盘拿到数据后那么就会向磁盘中进行写入,当写入成功或者失败的时候,此时磁盘就要发生硬件中断,CPU的针脚有对应的高电频,所以寄存器获取中断向量号,那么CPU就会与操作系统进行交互,那么此时操作系统就得知,哦,你磁盘写入完成了,所以操作系统才会将对应阻塞等待的进程唤醒将它的状态调整为运行状态。
- 同时我们还可以看出,虽然冯诺依曼体系结构中指出,CPU不会和外设打交道,其实CPU会和外设打交道,只不过CPU和外设是通过针脚打交道,多是发送一些控制信号进行交互,但是外设的数据是不和CPU打交道的,外设的数据和内存打交道
- 这里小编还要告诉一点,其实我们学习的信号,就是用的软件的方式,对进程模拟的硬件中断
三、信号的产生
- 上述的阶段完成了信号的预备工作,得出了一些信号的概念以及结论,同时小编还讲解了一些硬件方面知识的铺垫,帮助大家更好的理解信号,下面我们进入信号的产生

- 信号的产生有5种方式,分别是1. 键盘组合键 2. kill命令:kill -signo pid 3. 系统调用 4. 异常 5. 软件条件,下面小编将逐个介绍这5种方式
- 同时我们还应该认识到无论信号的产生方式是什么,信号最终都是由操作系统发送给进程的,为什么?因为操作系统是进程的管理者
键盘组合键
- 通过上面的讲解我们已经知道键盘组合键例如ctrl+c可以产生信号,产生的是2号信号SIGINT
- 同时ctrl+\也可以产生信号,产生的是3号信号,
- ctrl+\有什么作用呢?下面是一个死循环打印的代码,我们来验证一下
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
while(true)
{
cout << "hello linux, pid: " << getpid() << endl;
sleep(1);
}
return 0;
}
运行结果如下
- 很显然小编按下了ctrl+\ 此时进程quit终止,所以ctrl+\组合键的作用是让一个进程终止,那么究竟是不是操作系统给进程发送了3号信号SIGQUIT呢?

- 下面小编采用signal修改进程收到3号的默认动作终止,而是让进程收到3号进程的时候,执行我们的handler自定义方法,即打印信号编号并终止即可
#include <iostream>
#include <unistd.h>
#include <csignal>
#include <cstdlib>
using namespace std;
void handler(int signo)
{
cout << "process get a signal: " << signo << endl;
exit(1);
}
int main()
{
signal(3, handler);
while(true)
{
cout << "hello linux, pid: " << getpid() << endl;
sleep(1);
}
return 0;
}
运行结果如下
- 此时小编输入ctrl+\ 那么进程收到的确实是3号信号SIGQUIT
- 小编,小编,既然系统提供让我们对信号自定义捕捉的接口signal,让进程收到信号的时候,不去执行默认动作,而是让进程执行我们的自定义动作handler,那么我们是不是可以将所有信号进行捕捉,那么这样这个进程不就无敌了,谁也没办法干掉这个进程,那么这个进程就可以永远的运行了???那么我们是否可以使用signal将所有信号全部捕捉吗,下面小编带领大家验证一下
#include <iostream>
#include <unistd.h>
#include <csignal>
#include <cstdlib>
using namespace std;
void handler(int signo)
{
cout << "process get a signal: " << signo << endl;
}
int main()
{
cout << "i am process, my pid: " << getpid() << endl;
for(int i = 1; i <= 31; i++)
{
signal(i, handler);
}
cout << "set signal end, please kill signo pid" << endl;
while(true)
{
sleep(1);
}
return 0;
}
运行结果如下
此时当执行发送信号9的时候,进程被杀死了,说明此时9号信号无法被signal捕捉,9号信号无法设置自定义动作
此时当执行发送信号19的时候,进程被暂停了,说明此时19号信号无法被signal捕捉,19号信号无法设置自定义动作
- 由于我们只研究普通信号,即1到31号信号,那么普通信号中,经过我们的验证其中9号信号和19号信号是不能被signal捕捉的,同时我们思考一下为什么只有9号和19号信号是不能被捕捉的,下面我们分别验证一下9号信号和19号信号的作用
#include <iostream>
#include <unistd.h>
#include <csignal>
#include <cstdlib>
using namespace std;
int main()
{
while(true)
{
cout << "i am process, my pid: " << getpid() << endl;
sleep(1);
}
return 0;
}
运行结果如下
我们可以看到9号信号是杀死进程的
19号信号是暂停信号的
- 通过上面的验证我们可以看出,9号信号是杀死进程的,19号信号是暂停信号的,那么恰恰的杀死和暂停也是操作系统控制信号所必须的,所以我们可以认为虽然系统允许你使用signal自定义捕捉绝大部分信号,但是唯独9号信号和19号信号是你不能捕捉的,因为一旦我允许你对9号信号和19号信号进行捕捉的话,那么这个进程一旦运行任何人包括操作系统都无法终止或暂停这个进程,那么我操作系统的作用就是对下管理好软硬件资源,那么这个属于软件资源的进程我操作系统都没法管理,那我还玩什么?所以操作系统绝对不允许对9号信号和19号信号进行捕捉的
- 其实9号信号杀死进程我们可以理解,但是为什么19号信号暂停信号也是用于控制进程,为什么要暂停进程?例如:当一些进程正在处理一些重要的资源的,但是此时进程出现错误,操作系统对于发生错误的进程一般都是杀掉,但是如果这个进程正在处理一些重要的资源的时候出错了,那么我操作系统可以暂停这个进程,减少损失,同时期望用户可以使用一些手段获取这些重要资源,防止资源丢失,如果操作系统贸然杀掉这样处理有重要进程的资源造成资源丢失,那么后果很严重,所以暂停进程是有作用的
kill命令
- 上面,小编也演示了很多kill命令的使用,即kill -signo pid可以给指定pid的进程发送特定的信号signo
系统调用
- 系统调用发送信号的方式有三种,kill,raise,abort,下面小编将逐个进行介绍
kill
- kill不仅有命令行的指令,而且还有对应的系统调用kill

- kill的调用很简单,第一个参数pid你想要发送给进程的pid,如果发送给自身调用的pid,那么可以使用getpid获取pid,第二个则是你想要发送的信号sig
- 下面小编可以带领大家通过底层封装kill系统调用的方式模拟命令行的kill指令,实现一个自己的kill指令
//mykill.cc
#include <iostream>
#include <unistd.h>
#include <csignal>
#include <cstdlib>
#include <sys/types.h>
#include <signal.h>
using namespace std;
void Usage(char *proc)
{
cout << "usage:" << endl;
cout << '\t' << proc << " -signum pid" << endl << endl;
}
int main(int argc, char *argv[])
{
if (argc != 3 || *(argv[1]) != '-')//说明不是类似于三个参数形式kill -9 pid, argv[1]
{ //或者用户没有输入'-', 那么反馈给用户如何使用
Usage(argv[0]); //使用说明
exit(1);
}
//kill -9 pid, argv[1]放的是"-9", argv[1] + 1将指针argv[1]向后挪忽略"-",这样就可以
int signum = stoi(argv[1] + 1); //传入"9", 将"9"转化为整数9
int pid = stoi(argv[2]);
int n = kill(pid, signum); //当kill指令向指定进程发送信号失败的时候, 会返回-1
if(n == -1)
{
perror("kill");
exit(2);
}
return 0;
}
//proc.cc
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
while (true)
{
cout << "i am process, my pid: " << getpid() << endl;
sleep(1);
}
return 0;
}
运行结果如下,接下来我们想要使用我们自己的kill指令杀掉正在死循环打印的proc进程
我们的kill指令运行成功,成功的杀掉了proc进程
raise
- raise的作用是向自身调用raise的进程发送sig信号

- 那么我们接下来使用raise向自身调用的进程发送2号信号,2号信号对于进程的默认动作是SIGINT打断自身进程,打断进程之后,进程终止退出,那么我们使用signal对2号信号进行捕获,执行我们的自定义动作handler,同时使用cnt计数,让raise发送三次,退出循环,程序自动结束,那么我们看我们的程序能否收到2号信号
#include <iostream>
#include <unistd.h>
#include <csignal>
#include <cstdlib>
#include <sys/types.h>
#include <signal.h>
using namespace std;
void handler(int signo)
{
cout << "process get a signal: " << signo << endl;
}
int main()
{
signal(2, handler);
int cnt = 3;
while(true)
{
cout << "i am process, my pid: " << getpid() << endl;
sleep(1);
raise(2);
cnt--;
if(cnt == 0)
{
break;
}
}
return 0;
}
运行结果如下,确实收到了2号信号
- 其实raise的底层是封装的系统调用kill,如何实现的,使用getpid获取自身传输给kill,同时将对应要发送的信号进行传入给kill即可
#include <iostream>
#include <unistd.h>
#include <csignal>
#include <cstdlib>
#include <sys/types.h>
#include <signal.h>
using namespace std;
void handler(int signo)
{
cout << "process get a signal: " << signo << endl;
}
int main()
{
signal(2, handler);
int cnt = 3;
while(true)
{
cout << "i am process, my pid: " << getpid() << endl;
sleep(1);
// raise(2);
kill(getpid(), 2);
cnt--;
if(cnt == 0)
{
break;
}
}
return 0;
}
运行结果如下
- 确实,我们使用kill系统调用同样可以达到和reise一样的效果
abort
- abort的作用是引起一个正常进程终止,termination是终止的意思

- abort什么都不用传入即可进行调用,下面我们来调用测试一下
#include <iostream>
#include <unistd.h>
#include <csignal>
#include <cstdlib>
#include <sys/types.h>
#include <signal.h>
using namespace std;
int main()
{
abort();
return 0;
}
运行结果如下
- 其实abort是向进程发送的6号信号SIGABRT,下面我们来使用signal对6号信号捕捉一下,让进程收到6号信号的时候执行我们的自定义动作handler

#include <iostream>
#include <unistd.h>
#include <csignal>
#include <cstdlib>
#include <sys/types.h>
#include <signal.h>
using namespace std;
void handler(int signo)
{
cout << "process get a signal: " << signo << endl;
}
int main()
{
signal(SIGABRT, handler);
while(1)
{
abort();
sleep(1);
}
return 0;
}
运行结果如下
- 那么问题来了,明明小编已经对6号信号进行了捕捉,并且abort也确实是发送的6号信号,那么进程收到6号信号后就去执行我们的自定义动作handler打印信号编号6号,没问题,可是为什么程序退出了?我们的程序明明是循环式调用abort,当一次调用完成后,那么应该再继续去调用abort发送6号信号执行自定义动作,可是为什么却直接退出了?
- 其实abort的作用就是使进程终止,那么尽管abort调用了这个6号信号产生终止,但是我的abort的作用可是让进程终止啊,这个6号信号还有可能被用户signal捕捉,一旦6号信号被用户捕捉了,那么我就不能终止进程了,那么我这个abort这个函数也就达不到预期终止进程的效果了,无论第一次发送6号信号是否成功,所以abort一定还做了双重保险,即abort在发送了第一次6号信号之后,会将当前进程6号信号的动作调整为默认动作,即终止进程,并且第二次再次发送6号信号,这样就可以保证进程一定能被终止
- 同样的abort底层还是可以使用kill进行模拟,同时如何重置信号动作为默认动作,那么可以通过signal,第二个参数通过传入SIG_DEL,这样就可以让信号的动作恢复为默认动作, SIG_DFL是一个函数指针类型的宏定义,它指向默认动作
#include <iostream>
#include <unistd.h>
#include <csignal>
#include <cstdlib>
#include <sys/types.h>
#include <signal.h>
using namespace std;
void handler(int signo)
{
cout << "process get a signal: " << signo << endl;
}
void myabort()
{
kill(getpid(), 6);
signal(6, SIG_DFL); //将6号信号的动作恢复默认动作
kill(getpid(), 6);
}
int main()
{
for(int i = 1; i <= 31; i++)
{
signal(i, handler);
}
while(1)
{
myabort();
sleep(1);
}
return 0;
}
运行结果如下
- 所以我们可以得出系统调用raise,abort,以及命令行的指令kill底层其实都是对系统调用kill的封装
异常
- 我们知道进程退出场景的方式有三种,1. 代码执行完毕,进程退出,结果正确,2. 代码执行完毕,进程退出,结果错误,3. 代码异常终止,进程退出。关于进程退出场景详细讲解请点击后方蓝字链接详情请点击<——
- 进程退出的场景其中的3. 代码异常终止,进程退出,异常的本质就是由于进程收到了信号,最具有代表性的就是除零错误,野指针的访问,下面小编来分别演示一下,并且进行对应的解释
除零错误
#include <iostream>
using namespace std;
int main()
{
int a = 5;
a /= 0;
return 0;
}
运行结果如下
- 除零错误是操作系统给进程发送了8号信号SIGFPE,这一点小编就不带领大家进行验证了,因为SIGFPE中的FPE很明显就是Floating point exception的首字母缩写,并且之前的文章中,小编已经带领大家验证过了,感兴趣的读者友友可以点击后方蓝字链接阅读学习详情请点击<——

- 那么既然是8号信号,下面小编就带领大家使用signal捕捉一下这个8号信号,让进程收到8号信号的时候,执行我们的自定义动作handler,即打印信号编号的动作
#include <iostream>
#include <signal.h>
using namespace std;
void handler(int signo)
{
cout << "i get a signal, number: " << signo << endl;
}
int main()
{
signal(SIGFPE, handler);
int a = 5;
a /= 0;
return 0;
}
运行结果如下
当小编一运行的时候,预料不到的事情发生了,程序居然死循环打印了,并且一直无法停下,明明小编的代码中,并没有死循环打印的代码,可是这里却偏偏出现了死循环打印
- 关于除零错误,我们现在最关心的是,为什么除零会错误?除零错误为什么会导致进程崩溃?如何理解上面代码现象的死循环打印?如下图

- 第一种情况,如果我们不使用signal对8号信号做捕捉,那么此时上述演示的现象是进程异常,进程收到信号,程序退出,如何理解?
- 先介绍一下CPU,CPU中有一套寄存器,例如有:eip/pc寄存器用于保存当前执行到哪一行代码了,以及eax,ebx,ecx等,还有一个状态寄存器,状态寄存器中有十分多的比特位用于表示不同的涵义,状态寄存器中有一个比特位表示溢出标志位,如何理解溢出标志位呢?溢出标志位其实就是一个比特位,那么比特位的数要么为0,要么为1,当溢出标志位的数为0的时候,表示数据没有溢出,当溢出标志位的数为1的时候表示数据溢出
- 那么当我们的程序使用 5 /= 0的时候,由于是一个数除以一个0,一个数除以0那么结果就是无穷大的一个数,此时就会造成数据溢出,那么此时CPU中的状态寄存器就会变成1,表示此时数据溢出了,CPU的状态寄存器属于硬件,CPU的状态寄存器出现错误了,那么状态寄存器属于CPU,所以CPU出现错误了,CPU是硬件,硬件出现错误了,由于操作系统是硬件的管理者,所以操作系统一定有某种方法和手段可以检测到CPU这个硬件出现错误了
- CPU上的一套寄存器上的数据是属于进程的上下文,寄存器上的数据出现了错误本质上还是你进程出现错误了,即虽然代码中执行了5 /= 0之后,修改的是CPU内部的状态寄存器,但是这个错误只会影响你当前这个进程,因为操作系统同时也是软件的管理者,所以操作一定可以通过某种方法和手段找到当前是哪一个进程在CPU上被调度运行,所以操作系统检测到当CPU状态寄存器出现问题的时候,那么就会找到当前是哪一个进程在CPU上被调度运行,那么给这个进程发送对应的8号信号SIGFPE,进程收到8号信号之后就会执行8号信号的默认动作,此时进程就出现异常,收到信号,异常终止了
- 当操作系统处理完成当前正在CPU上运行的进程之后,不影响下一个进程的继续调度,下一个进程把自己的上下文加载进来, 寄存器上的数据就被覆盖为下一个进程的上下文数据了,此时CPU就会继续调度运行下一个进程了
- 可是,如果我这个出现除零错误,即5 /= 0的进程收到了8号信号之后,不执行默认动作呢?此时进程不崩溃呢?即此时8号信号被用户使用signal捕捉了,当进程收到信号的时候就会去执行用户的自定义动作handler,所以此时进程就不崩溃,那么此时你这个进程就会一直被调度,即被重新从CPU上下来,将CPU一套寄存器的数据作为进程的上下文放到task_struct中带走,重新在CPU的运行队列中排队
- 那么再次排队到有除零错误的进程之后,此时操作系统一把你这个进程放到CPU上运行的时候,那么进程的上下文加载到CPU的一套寄存器中,那么状态寄存器的溢出标志位就为1,表示数据溢出,那么状态寄存器就会出问题,CPU就会出问题,操作系统就会检测到CPU的状态寄存器出问题了,那么此时呢?
- 操作系统就会找到正在CPU上运行的进程,操作系统给当前进程发送8号信号,意图让进程崩溃退出,可是8号信号被用户捕捉了,当进程收到8号信号的时候就要执行用户的自定义动作,打印信号编号,所以此时进程不崩溃,进程不崩溃就要一直被放到运行队列中调度运行,那么这不就成为了死循环吗,所以才会出现死循环打印信号编号的现象
野指针的访问
#include <iostream>
using namespace std;
int main()
{
int* p = nullptr;
*p = 10;
return 0;
}
运行结果如下
- 野指针的访问是操作系统给进程发送了11号信号SIGSEGV,这一点小编就不带领大家进行验证了,因为之前的文章中,小编已经带领大家验证过了,感兴趣的读者友友可以点击后方蓝字链接阅读学习详情请点击<——

- 那么既然是11号信号,下面小编就带领大家使用signal捕捉一下这个11号信号,让进程收到11号信号的时候,执行我们的自定义动作handler,即打印信号编号的动作
运行结果如下
同样的出现了死循环打印的问题,同样的,这个死循环打印的原因和除零错误的一样,这里小编就不再过多解释了
- 那么如何理解野指针的访问呢?为什么野指针的访问在我们没有捕捉11号信号的时候会造成程序异常,进程崩溃呢?如下图

- 这里我们先看不出现野指针的访问,即进程正常运行执行的情况,此时进程执行正文代码,如何执行?去地址空间上映射的程序的正文代码处找到虚拟地址,接下来就要去页表进行虚拟地址到物理地址的转换,如何转化?那么就要通过MMU这个内存管理单元进行虚拟地址到物理地址的转换,当虚拟地址有效,处于进程正文代码的范围,此时虚拟地址到物理地址的转化就会成功(注意这里还有可能触发缺页中断,我们目前不考虑),那么就会物理地址上找到对应的指令放到CPU的指令相关的寄存器进行执行,所以此时进程就可以根据这个流程执行下去
- 可是如果碰到了野指针的访问,例如碰到了是int* p = nullptr; *p = 10; 我们要理解一下nullptr的本质,nullptr的本质就是(void*)0,即对0地址处进行访问,作为一个进程,你这个进程没有资格,也没有能力访问修改0地址处,并且0地址处也不属于你这个进程的正文代码对应的虚拟地址处。
- 所以此时如果执行到了*p = 10,CPU的指令相关的寄存器要执行这一条指令,那么就要先找到p的地址,p的地址是nullptr,即0地址处,这个0地址处是个虚拟地址,此时就要对0这个虚拟地址进行访问,那么接下来去页表上,找到0这个虚拟地址对应的物理地址,如何找?
- 通过MMU这个内存管理单元即可进行查找,可是MMU找了一圈之后,对不起,0地址并不属于该进程的正文代码的虚拟地址的有效范围内,并且进程也没有权限和能力访问0地址处,所以此时MMU这个内存管理单元就会告诉CPU地址转化失败,即虚拟地址到物理地址转化失败,CPU中指令相关的寄存器获取到的指令中nullptr这个地址转换错误
- 指令相关的寄存器就会出现错误,那么此时CPU就会出现错误,操作系统和CPU进行交互,所以此时操作系统就得知虚拟地址到物理地址转化失败,出现野指针错误,会给进程发送11号信号,进程接收到11号信号之后就会执行默认动作,出现异常,段错误,终止进程
软件条件
- 可是异常只能由硬件产生吗?难道软件不可以产生异常吗?其实软件也是可以的产生异常的,例如下面的场景
- 小编使用read系统调用接口打开4号文件描述符,我们知道当进程启动的时候,默认会打开3个文件描述符,即0,1,2,此时我们没有主动打开4号文件描述符,那么read还向4号文件描述符中进行读取数据,毫无疑问是会读取失败的,那么我们打印一下read的返回值n,并且如果n打开文件描述符失败,则会返回-1
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
int main()
{
int buffer[1024] = { 0 };
int n = read(4, buffer, sizeof(buffer));
cout << "n = " << n << endl;
if(n == -1)
{
perror("read");
exit(1);
}
return 0;
}
运行结果如下
- 此时毫无疑问,read打开4号文件描述符失败,这属于软件层面上引起的异常,因为此时read想要从4号文件描述符中读取数据,但是此时4号文件描述符并没有打开,所以在软件层面上资源未就绪就去访问,所以这属于软件层面的异常
- 软件层面发送信号还有管道的情况,例如管道写端正常,但是管道读端退出读了几次之后,管道读端退出的情况,那么此时写端继续去写就没有了意义,因为管道中已经没有读端继续去读取数据了,都没有人读取你写端的数据了,那么你写端还写什么写,我操作系统还要费心费力的帮你做数据拷贝,调度,你进程的运行还要占用内存空间,我操作系统不会做任何一件浪费时间和空间的事情,那么写端就会被操作系统通过发送13号信号SIGPIPE关闭

alarm
- 还有闹钟同样属于软件条件,闹钟可以设置时间,时间到了同样也会发出信号,闹钟函数是alarm的参数是seconds即秒,闹钟的返回值是上次闹钟响起,比预定时间提早了多少时间

- 那么小编设置一个5秒的闹钟,测试一下当闹钟响起,我们的死循环打印程序会发生什么?
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
int main()
{
alarm(5);
while(true)
{
cout << "i am process, my pid: " << getpid() << endl;
sleep(1);
}
return 0;
}
运行结果如下
当设定的闹钟响起,我们的进程会收到信号,进而终止进程
- 实际上闹钟设定的时间到了之后,我们的进程会收到14号信号SIGALRM

- 下面小编将带领大家探索一下alarm的返回值问题,并且捕捉一下14号信号SIGALRM,引入重复设置闹钟的场景
- 如下小编先使用signal捕捉一下14号信号SIGALRM,当我们的进程收到了14号信号的时候,那么就会执行我们的自定义动作hanlder,在我们的自定义动作hanlder中,小编先打印出信号编号,接下来设定一个5秒的闹钟,并且接收闹钟的返回值进行打印,主函数设定一个50秒的闹钟,并且死循环打印pid
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
void handler(int signo)
{
cout << "i get a signal, number: " << signo << endl;
int n = alarm(5);
cout << "剩余时间: " << n << endl;
}
int main()
{
signal(SIGALRM, handler);
alarm(50);
while(true)
{
cout << "i am process, my pid: " << getpid() << endl;
sleep(1);
}
return 0;
}
运行结果如下
- 当进程运行起来时候,闹钟就已经开始计时了,第一个闹钟计时时间是50秒,但是小编知道这个闹钟的本质就是50时间到了之后向调用闹钟的进程发送14号信号,但是由于小编已经提前使用了signal对14号信号进行捕捉,当进程收到14号信号的时候,就会执行自定义动作hanlder
- 那么小编在程序运行起来变成进程,50秒闹钟计时的时候,小编复制ssh渠道,在另一个终端的bash,小编使用kill指令给当前进程发送14号信号,这样闹钟就提前响了,进而进程收到14号信号会去执行hanlder,hanlder中就会再次设定5秒的闹钟,并且将上一次闹钟相比预定时间提早的时间进行返回,那么小编拿到这个返回值进行打印
- 并且我们还要理解一个事情,为什么后面,小编尽管不主动使用kill发送信号了,但是闹钟仍然会继续进行设定,最住要的原因是小编对闹钟对应的14号信号进行了捕捉去执行我我们的自定义handler,因为闹钟设定了到了时间之后,会给当前进程发送14号信号,那么此时进程会去执行我们的自定义handler,自定义handler又会继续去设定闹钟,闹钟响了之后又继续去设定,周而复始设定执行
- 所以我们可以类似上面的方式,让我们的进程按期执行某种检查或者其它工作,如下
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
void work()
{
cout << "print log" << endl;
}
void handler(int signo)
{
work();
cout << "i get a signal, number: " << signo << endl;
int n = alarm(3);
cout << "剩余时间: " << n << endl;
}
int main()
{
signal(SIGALRM, handler);
alarm(3);
while(true)
{
cout << "i am process, my pid: " << getpid() << endl;
sleep(1);
}
return 0;
}
运行结果如下
此时我们的进程就会每隔3秒执行work
- 当我们理解了闹钟的使用以及返回值,那么我们思考一下,我这一个进程可以设置闹钟,那么其它进程也可以设定闹钟,操作系统中有很多的进程,所以操作系统中必然会存在很多的闹钟,所以操作系统就要把闹钟管理起来,如何管理?先描述,再组织,如何描述呢?我们设置闹钟的时候,必然会用到时间,例如alarm(5),即设置一个5秒之后的闹钟,那么在计算机世界中,关于时间使用最多的是什么?
- 时间戳,那么获取调用alarm的时候的时间戳,再加上闹钟设定秒数,得出闹钟响的时候的时间戳,使用一个结构体描述闹钟的属性,例如闹钟是哪一个进程设定的,这个进程的pid,这个进程的task_struct的地址等,同时添加闹钟响的时候的时间戳,接下来使用数据结构体组织起来,请读者友友思考一下使用什么数据结构组织起来比较好?
- 使用一个小堆组织起来最好,因为堆天然具有排序,选出最小的功能,那么我们使用一个小堆将闹钟的描述对象按照闹钟响的时候的时间戳的大小组织起来,由于是小堆,那么闹钟的描述对象按照闹钟响的时候的时间戳的最小的时间戳就会被放到最上面,那么我操作系统就不再需要遍历闹钟的描述对象挨个检测时间戳了,转而只需要定期检查堆顶的描述对象对应的时间戳,只要当前的时间戳小于堆顶的描述对象对应的时间戳,那么整个堆的描述对象对应的时间戳时间都没有够时间,所以这样巧妙的利用数据结构就可以巧妙的提高效率
四、core dump标志
- 在前面的文章中,小编还留下了伏笔,即下图中第8位的core dump,下面我们就来讲解一下core dump

- 什么是core dump呢?其实core dump的英文是核心转储,即当进程出现异常终止的时候,就会进行core动作,对应上图中第8个比特位就会被设置成1,那么操作系统就会将当时进程在内存中运行时的一些信息,例如当时执行到多少行出现的异常终止以及一些其它信息进行存储到core dump文件中,便于程序员进行gdb调试,例如下面的当进程收到3,4,6,8,11信号的时候,都会进行core动作,将对应上图中第8个比特位就会被设置成1,那么换句话来说也就是说其它动作,例如term动作就不会将对应上图中第8个比特位设置成1,而是不进设置保持默认值为0,是的,下面我们来验证一下

- 那么下面我们就要设置一个父子进程,父进程等待子进程的场景,如下子进程循环打印pid信息,父进程则阻塞式等待子进程,当等待成功之后,将子进程的退出信息进行提取即可,其中神对core dump的提取,我们可以将status右移7位,此时core dump就在最低位,接下来让其与1进行按位与即可得到core dump的值

#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
int main()
{
pid_t id = fork();
if(id == 0)
{
//child;
while(true)
{
cout << "i am child, my pid: " << getpid() << endl;
sleep(2);
}
exit(0);
}
//father
int status = 0;
int rid = waitpid(id, &status, 0);
if(rid == id)
{
cout << "child quit info, child exit code: " << ((status >> 8)&0xFF) <<
", child core: " << ((status >> 7) & 1) << ", child exit signal: " <<
(status & 0x7F) << endl;
}
return 0;
}
运行结果如下
此时运行程序,小编向子进程发送2号信号,2号信号是Term动作,所以不会设置core dump,所以上图符合我们的预期,core dump为0,并且子进程收到的信号是2号信号
观察上图8号信号是进行了core动作,那么接下来,小编就给子进程发送8号信号,观察子进程的退出信息中core dump是否会设置为1
???什么,小编这我可就懵了,实验现象和你讲解的不一致,子进程并没有设置core dump为1,如何理解呢?
- 其实是由于core dump文件较大,小编使用的是云服务器,云服务器上core dump功能默认是关闭的,因为core dump如果打开了,那么每一个进程运行,如果收到的信号执行的是core动作,那么就会收集进程的退出信息,如果一直没有人处理且进程一直收到的信号,且信号执行的是core动作,那么core dump文件的大小就会远远不断的增加,最终把服务器卡死,所以说默认云服务器的core dump都是关闭的
- 那么core dump功能如何打开呢?那么我们先使用ulimit -a查看一下当前云服务器的core文件资源的大小,果然当前云服务器的core dump已经关闭了,因为当前云服务器的core文件资源的大小为0,那么我们如果想要打开当前云服务器的core dump功能就要设置当前云服务器的core文件资源的大小,如何设置呢?

- 那么使用ulimit -c size大小,即可进行设置,此时就设置成功了

- 那么8号信号是进行了core动作,那么接下来,小编就再次给子进程发送8号信号,观察子进程的退出信息中core dump是否会设置为1?
运行结果如下
此时果然小编打开了当前云服务器的core dump功能之后,子进程的退出信息中core dump就会设置为1了,并且还产生了一个core.24630这样一个core文件,并且观察下图,core.24630文件的大小还比较大
- 所以我们可以使用core文件进行gdb调试定义错误,下面小编演示一下除零错误
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
int main()
{
int a = 10;
a /= 0;
return 0;
}
运行结果如下
- 此时观察除零错误后面出现了core dumped,说明当前进程已经被核心转储了,即打开系统的core dump功能后,一旦进程出现异常,操作系统会将进程在内存中的运行信息,进行dump转储当当前进程的工作目录(磁盘中)中,形成core.pid文件,核心转储(core dump)
- 所以此时我们就可以使用gdb进行调试,如果想要使用gdb进行调试,那么就要在源文件编译形成可执行文件的时候,就要添加-g选项,使形成的可执行程序带有调试信息这样才可以

- 使用gdb进行调试,那么gdb 可执行文件,按下回车,直接输入core-file,一路回车即可,此时就可以直接定位到出错行,即这是一种先运行,如果出错了事后调试的行为

- 当使用完成core dump的功能后及时使用ulimit -c 0,将云服务器的core dump功能关闭

总结
以上就是今天的博客内容啦,希望对读者朋友们有帮助
水滴石穿,坚持就是胜利,读者朋友们可以点个关注
点赞收藏加关注,找到小编不迷路!
































浙公网安备 33010602011771号