✨个人主页:****北 海
🎉所属专栏:****Linux学习之旅
🎃操作环境:****CentOS 7.6 阿里云远程服务器
文章目录
🌇前言
**在
Linux
中,进程具有独立性,进程在运行后可能 “放飞自我”,这是不利于管理的,于是需要一种约定俗成的方式来控制进程的运行,这就是
进程信号
,本文将会从什么是进程信号开篇,讲述各种进程信号的产生方式及作用**
不同的信号指示灯代表着不同的执行动作
🏙️正文
1、进程信号基本概念
1.1、什么是信号?
信号 是信息传递的承载方式,一种信号往往代表着一种执行动作,比如:
- 鸡叫 => 天快亮了
- 闹钟 => 起床、完成任务
- 红绿灯 => 红灯停,绿灯行
- ……
当然这些都是生活中的 信号,当产生这些 信号 时,我们会立马想到对应的 动作 ,这是因为 我们认识并能处理这些信号
我们能进行处理是因为受过教育,学习了执行动作,但对进程来说,它可没有接受过九年义务教育,也不知道什么时候该干什么事
于是程序员们给操作系统植入了一批 指令,一个指令表示一种特殊动作,而这些指令就是 信号(进程信号)
通过
kill -l
查看当前系统中的信号集合表
kill -l
这些就是当前系统中的 进程信号,一共
62
个,其中
1~31
号信号为 普通信号(学习目标),用于 分时操作系统;剩下的
34~64
号信号为 实时信号,用于 实时操作系统
- 分时操作系统:根据时间片实行公平调度,适用于个人电脑
- 实时操作系统:高响应,适合任务较少、需要快速处理的平台,比如汽车车机、火箭发射控制台
实时操作系统
普通信号只保存它有无产生,实时信号可以保持很长时间
因为我们的系统属于 分时操作系统,所以只需要研究 **
1~31
号信号即可**,当然也不是全部研究,部分信号只做了解即可
1.2、信号的作用
早在 《Linux进程学习【进程状态】》 我们就已经使用过 信号 了,比如:
kill -9 pid
终止进程运行kill -19 pid
暂停进程运行kill -18 pid
恢复进程运行
就连常用的
ctrl+c
和
ctrl+d
热键本质上也是 信号
这么多信号,其对应功能是什么呢?
- 可以通过
man 7 signal
进行查询
man7 signal
简单总结一下,
1~31
号信号对应的功能如下(表格内容引用自
2021dragon
Linux中的31个普通信号)
信号编号信号名功能1
SIGHUP
如果终端接口检测到一个连接断开,则会将此信号发送给与该终端相关的控制进程,该信号的默认处理动作是终止进程2
SIGINT
**当用户按组合键(一般采用
Ctrl + C
)时,终端驱动程序产生此信号并发送至前台进程组中的每一个进程,该信号的默认处理动作是终止进程**3
SIGQUIT
**当用户按组合键(一般采用
Ctrl + \
)时,终端驱动程序产生此信号并发送至前台进程组中的每一个进程,该信号不仅终止前台进程组,同时会产生一个
core
文件**4
SIGILL
**此信号表示进程已执行一条非法指令,该信号的默认处理动作是终止进程,同时产生一个
core
文件**5
SIGTRAP
**该信号由断点指令或其他
trap
指令产生,该信号的默认处理动作是终止进程,同时会产生一个
core
文件**6
SIGABRT
**调用
abort
函数是产生此信号,进程异常终止,同时会产生一个
core
文件**7
SIGBUS
**当出现某些类型的内存故障时,常常产生该信号,,该信号的默认处理动作是终止进程,同时产生一个
core
文件**8
SIGFPE
**此信号表示一个算术运算异常,比如除0、浮点溢出等,该信号的默认处理动作是终止进程,同时产生一个
core
文件**9
SIGKILL
该信号不能被捕捉或忽略,它向系统管理员提供了一种可以杀死任一进程的可靠方法10
SIGUSR1
这是一个用户定义的信号,即程序员可以在程序中定义并使用该信号,该信号的默认处理动作是终止进程11
SIGSEGV
**指示进程进行了一次无效的内存访问(比如访问了一个未初始化的指针),该信号的默认处理动作是终止进程并产生一个
core
文件**12
SIGUSR2
**这是另一个用户定义的信号,与
SIGUSR1
相似,该信号的默认处理动作是终止进程**13
SIGPIPE
如果在管道的读进程已终止时对管道进行写入操作,则会收到此信号,该信号的默认处理动作是终止进程14
SIGALRM
**当用
alarm
函数设置的定时器超时时产生此信号,或由
setitimer
函数设置的间隔时间已经超时时也产生会此信号**15
SIGTERM
**该信号是由应用程序捕获的,使用该信号让程序有机会在退出之前做好清理工作。与
SIGKILL
信号不同的是,该信号可以被捕捉或忽略,通常用来表示程序正常退出**16
SIGSTKFLT
该信号指示协处理器上的堆栈故障(未使用),该信号的默认处理动作是终止进程17
SIGCHLD
**在一个进程终止或停止时,
SIGCHLD
信号被发送给其父进程。按系统默认,将忽略此信号。如果父进程希望被告知其子进程的这种状态改变,则应捕捉此信号。信号捕捉函数中通常要调用一种
wait
函数以取得子进程
PID
及其终止状态**18
SIGCONT
可以通过发送该信号让一个停止的进程继续运行19
SIGSTOP
**这时一个作业控制信号,该信号用于停止一个进程,类似于交互停止信号(
SIGTSTP
),但是该信号不能被捕捉或忽略**20
SIGTSTP
**交互停止信号,当用户按组合键(一般采用
Ctrl+Z
)时,终端驱动程序产生此信号并发送至前台进程组中的每一个进程**21
SIGTTIN
后台进程读终端控制台时,由终端驱动程序产生此信号并发送给该后台进程,该信号的默认处理动作是暂停进程22
SIGTTOU
后台进程向终端控制台输出数据,由终端驱动程序产生此信号并发送给该后台进程,该信号的默认处理动作是暂停进程23
SIGURG
套接字上有紧急数据时,向当前正在运行的进程发出此信号,报告有紧急数据到达,该信号的默认处理动作是忽略24
SIGXCPU
**进程执行时间超过了分配给该进程的
CPU
时间,系统产生该信号并发送给该进程,该信号的默认处理动作是终止进程,同时会产生一个
core
文件**25
SIGXFSZ
**如果进程写文件时超过了文件的最大长度设置,则会收到该信号,该信号的默认处理动作是终止进程,同时会产生一个
core
文件**26
SIGVTALRM
**虚拟时钟超时时产生该信号,与
SIGALRM
信号类似,但是该信号只计算该进程占用
CPU
的使用时间,该信号的默认处理动作是终止进程**27
SIGPROF
**该信号类似与
SIGVTALRM
,它不仅包括该进程占用
CPU
的时间还包括执行系统调用的时间,该信号的默认处理动作是终止进程**28
SIGWINCH
当窗口大小发生变化时,内核会将该信号发送至前台进程组,该信号的默认处理动作是忽略29
SIGIO
**此信号指示一个异步
I/O
事件,该信号的默认处理动作是终止进程**30
SIGPWR
电源故障,该信号的默认处理动作是终止进程31
SIGSYS
**该信号指示一个无效的系统调用,该信号的默认处理动作是终止进程,同时会产生一个
core
文件**
注意:**其中的
9
号 和
19
号信号是非常特殊的,不能修改其默认动作**
1.3、信号的基本认知
进程信号由 信号编号 + 执行动作 构成,一个信号对应一种动作,对于进程来说,动作无非就这几种:终止进程、暂停进程、恢复进程,
3
个信号就够用了啊,为什么要搞这么多信号?
- 创造信号的目的不只是控制进程,还要便于管理进程,进程的终止原因有很多种,如果一概而论的话,对于问题分析是非常不友好的,所以才会将信号细分化,搞出这么多信号,目的就是为了方便定位、分析、解决问题
- 并且 普通信号 就
31
个,这就是意味着所有普通信号都可以存储在一个int
中,表示是否收到该信号(信号的保存)
所以信号被细化了,不同的信号对应不同的执行动作,虽然大部分最终都是终止进程
进程的执行动作是可修改的,默认为系统预设的 默认动作
- 默认动作
- 忽略
- 自定义动作
所以我们可以 更改信号的执行动作(后面会专门讲信号处理相关内容)
信号有这么多个,并且多个进程可以同时产生多个信号,操作系统为了管理,**先描述、再组织,在
PCB
中增加了 信号相关的数据结构:
signal_struct
,在这个结构体中,必然存在一个 位图结构
uint32_t signals
存储
1~31
号信号的有无信息**
//信号结构体源码(部分)structsignal_struct{
atomic_t sigcnt;
atomic_t live;int nr_threads;
wait_queue_head_t wait_chldexit;/* for wait4() *//* current thread group signal load-balancing target: */structtask_struct*curr_target;/* shared signal handling: */structsigpending shared_pending;/* thread group exit support */int group_exit_code;/* overloaded:
* - notify group_exit_task when ->count is equal to notify_count
* - everyone except group_exit_task is stopped during signal delivery
* of fatal signals, group_exit_task processes the signal.
*/int notify_count;structtask_struct*group_exit_task;/* thread group stop support, overloads group_exit_code too */int group_stop_count;unsignedint flags;/* see SIGNAL_* flags below *//*
* PR_SET_CHILD_SUBREAPER marks a process, like a service
* manager, to re-parent orphan (double-forking) child processes
* to this process instead of 'init'. The service manager is
* able to receive SIGCHLD signals and is able to investigate
* the process until it calls wait(). All children of this
* process will inherit a flag if they should look for a
* child_subreaper process at exit.
*/unsignedint is_child_subreaper:1;unsignedint has_child_subreaper:1;//……};
下面对 进程信号 做一波概念性的总结
1.信号是执行的动作的信息载体,程序员在设计进程的时候,早就已经设计了其对信号的识别能力
2.信号对于进程来说是异步的,随时可能产生,如果信号产生时,进程在处理优先级更高的事情,那么信号就不能被立即处理,此时进程需要保存信号,后续再处理
3.**进程可以将 多个信号 或 还未处理 的信号存储在signal_struct
这个结构体中,具体信号编号,存储在
uint32_t signals
这个位图结构中**
4.**所谓的 “发送” 信号,其实就是写入信号,修改进程中位图结构中对应的比特位,由0
置为
1
,表示该信号产生了**
5.**signal_struct
属于内核数据结构,只能由 操作系统 进行同一修改,无论信号是如何产生的,最终都需要借助 操作系统 进行发送**
6.信号并不是立即处理的,它会在合适的时间段进行统一处理
所以 进程信号 可以分为三步:信号产生 =》 信号保存 =》 信号处理
本文讲解的就是 信号产生 部分相关知识,下面正式开始学习 信号产生
===== 信号产生的方式 =====
2、键盘键入
信号产生(发送)的第一种方式:键盘键入
通俗来说就是命令行操作
2.1、ctrl+c 终止前台进程
系统卡死遇到过吧?程序死循环遇到过吧?这些都是比较常见的问题,当发生这些问题时,我们可以通过 **键盘键入
ctrl + c
发出
2
号信号终止前台进程的运行**
下面是一段死循环代码:
#include<iostream>#include<unistd.h>usingnamespace std;intmain(){while(true){
cout <<"我是一个进程,我正在运行…… PID: "<<getpid()<< endl;sleep(1);}return0;}
运行程序后,会一直循环打印,此时如果想要终止进程,**可以直接按
ctrl + c
发出
2
号信号,终止前台进程**
**此时发出了一个
2
号信号
SIGINT
终止了该进程的运行**
如何证明呢?如何证明按
ctrl + c
发出的是
2
号信号呢?
证明自有方法,前面说过,**一个信号配有一个执行动作,并且执行动作是可以修改的,需要用到
signal
函数(属于 信号处理 部分的内容,这里需要提前用一下)**
**
ctrl + c
终止的是当前正在运行的前台进程,如果在程序运行时加上
&
表示让其后台运行,此时会发现无法终止进程**
**像这种后台进程
ctrl + c
是无法终止的,可以通过
kill -9 PID
发出
9
信号终止它**
2.1.1、signal 注册执行动作
signal
函数可以用来 修改信号的执行动作,也叫注册自定义执行动作
signal
**调用成功返回上一个执行方法的值(其实就是下标,后面介绍),失败则返回
SIG_ERR
,并设置错误码**
返回值可以不用关注,重点在于
signal
的参数
- 参数1 待操作信号的编号
- 参数2 待注册的新方法
参数1 就是信号编号,为
int
,单纯地传递 信号名也是可以的,因为信号名其实就是信号编号的宏定义
参数2 是一个函数指针,意味着需要传递一个 **参数为
int
,返回值为空的函数对象**
- 参数
int
是执行动作的信号编号
voidhandler(int)//其中的函数名可以自定义
显然,
signal
函数是一个 回调函数,当信号发出时,会去调用相应的函数,也就是执行相应的动作
我们先对
2
号信号注册新动作,在尝试按下
ctrl + c
,看看它发出的究竟是不是
2
号信号
#include<iostream>#include<signal.h>#include<unistd.h>usingnamespace std;voidhandler(int signo){
cout <<"当前 "<< signo <<" 号信号正在尝试执行相应的动作"<< endl;}intmain(){//给 2 号信号注册新方法signal(2, handler);while(true){
cout <<"我是一个进程,我正在运行…… PID: "<<getpid()<< endl;sleep(1);}return0;}
当我们修改
2
号信号的执行动作后,再次按下
ctrl + c
尝试终止前台进程,结果失败了!执行动作变成了我们注册的新动作
这足以证明
ctrl + c
就是在给前台进程发出
2
号信号,
ctrl + c
失效后,可以通过
ctrl + \
终止进程,发出的是
3
号信号(
3
号信号在发出后,会生成 核心转储 文件)
普通信号只有
31
个,如果把所有普通信号的执行动作都改了,会发生什么呢?难道会得到一个有着 金刚不坏 之身的进程吗?
#include<iostream>#include<signal.h>#include<unistd.h>usingnamespace std;voidhandler(int signo){
cout <<"当前 "<< signo <<" 号信号正在尝试执行相应的动作"<< endl;}intmain(){//给所有普通信号注册新方法for(int i =1; i <32; i++)signal(i, handler);while(true){
cout <<"我是一个进程,我正在运行…… PID: "<<getpid()<< endl;sleep(1);}return0;}
大部分信号的执行动作都被修改了,**但
9
号信号没有,因为
9
号信号是
SIGKILL
,专门用于杀死进程,只要是进程,他都能干掉**
19
号信号
SIGSTOP
也无法修改执行动作,所以前面说过,
9
号
SIGKILL
和
19
号
SIGSTOP
信号是很特殊的,经过特殊设计,不能修改其执行动作!
2.2、硬件中断
当我们从键盘按下
ctrl + c
时,发生了这些事:**
CPU
获取到键盘 “按下” 的信号,调用键盘相应的 “方法” ,从键盘中读取数据,读取数据后解析,然后发出
3
号信号**
其中
CPU
捕获键盘 “按下” 信号的操作称为 硬件中断
**
CPU
中有很多的针脚,不同的硬件对应着不同的针脚,每一个针脚都有自己的编号,硬件与针脚一对一相连,并通过 中断控制器(比如
8259
)进行控制,当我们按下键盘后**
- 中断控制器首先给
CPU
发送信息,包括键盘对应的针脚号 - 然后
CPU
将获取到的针脚号(中断号)写入 寄存器 中 - 最后根据 寄存器 里的 中断号,去 中断向量表 中查表,找到对应硬件的方法,执行它的读取方法就行了
这样
CPU
就知道是 键盘 发出的信号,然后就会去调用 键盘 的执行方法,**通过键盘的读取方法,读取到
ctrl + c
这个信息,转化后,就是
2
号信号,执行终止前台进程的动作**
键盘被按下 和 键盘哪些位置被按下 是不一样的
- 首先键盘先按下,
CPU
确定对应的读取方法 - 其次才是通过
读取方法
从键盘中读取数据
注:键盘读取方法如何进行读取,这是驱动的事,我们不用关心
硬件中断 的流程与 进程信号 的流程雷同,同样是 先检测到信号,然后再去执行相应的动作,不过此时发送的是 中断信号,执行的是 调用相应方法罢了
信号 与 动作 的设计方式很实用,操作系统只需要关注是否有信号发出,发出后去中断向量表中调用相应的方法即可,不用管硬件是什么样、如何变化,做到了 操作系统 与 硬件 间的解耦
3、系统调用
除了可以通过 键盘键入 发送信号外,还可以通过直接调用 系统接口 发送信号,毕竟
bash
也是一个进程,本质上就是在进行程序替换而已
3.1、kill 函数
信号的发送主要是通过
kill
函数进行发送
返回值:**成功返回
0
,失败返回
-1
并设置错误码**
参数1:**待操作进程的
PID
**
参数2:待发送的信号
下面来简单用一下(程序运行
5
秒后,自己把自己杀死)
#include<iostream>#include<signal.h>#include<unistd.h>usingnamespace std;intmain(){int n =1;while(true){
cout <<"我是一个进程,已经运行了 "<< n <<" 秒 PID: "<<getpid()<< endl;sleep(1);
n++;if(n >5)kill(getpid(), SIGKILL);}return0;}
**
kill
函数当然也可以发送其他信号,这里就不一一展示了,其实命令行中的
kill
命令就是对
kill
函数的封装,
kill -信号编号 -PID
其中的参数2、3不正是
kill
函数所需要的参数吗?所以我们可以尝试自己搞一个
myKill
命令**
3.2、模拟实现 myKill
这里就直接利用 命令行参数 简单实现了
#include<iostream>#include<string>#include<signal.h>usingnamespace std;voidUsage(string proc){// 打印使用信息
cout <<"\tUsage: \n\t";
cout << proc <<" 信号编号 目标进程"<< endl;exit(2);}intmain(int argc,char*argv[]){// 参数个数要严格限制if(argc !=3){Usage(argv[0]);}//获取两个参数int signo =atoi(argv[1]);int pid =atoi(argv[2]);//执行信号发送kill(pid, signo);return0;}
下面随便跑一个进程,然后用自己写的
myKill
命令给进程发信号
**我们可以把这个程序改造下,改成进程替换的方式,让后将自己写的命令进行安装,就能像
kill
一样直接使用了**
3.3、raise 函数
发送信号的还有一个
raise
函数,这个函数比较奇怪,只能 自己给自己发信号
返回值:**成功返回
0
,失败返回
非0
**
就只有一个参数:待发送的信号
可以这样理解:**
raise
是对
kill
函数的封装,每次传递的都是自己的
PID
**
#include<iostream>#include<signal.h>#include<unistd.h>usingnamespace std;intmain(){int n =1;while(true){
cout <<"我是一个进程,已经运行了 "<< n <<" 秒 PID: "<<getpid()<< endl;sleep(1);
n++;if(n >5)raise(SIGKILL);//自己杀死自己 }return0;}
3.4、abort 函数
abort
是
C
语言提供的一个函数,它的作用是 **给自己发送
6
号
SIGABRT
信号**
没有返回值,也没有参数
值得一提的是,**
abort
函数即使在修改执行动作后,最后仍然会发送
6
号信号**
#include<iostream>#include<signal.h>#include<unistd.h>usingnamespace std;voidhandler(int signo){
cout <<"收到了 "<< signo <<" 号信号,已执行新动作"<< endl;}intmain(){signal(6, handler);// signal(SIGABRT, handler); //这种写法也是可以的int n =1;while(true){
cout <<"我是一个进程,已经运行了 "<< n <<" 秒 PID: "<<getpid()<< endl;sleep(1);
n++;if(n >5)abort();}return0;}
**即使执行了我们新注册的方法,
abort
最后仍然会发出
6
号信号终止进程**
同样是终止进程,
C
语言 还提供了一个更好用的函数:
exit()
,所以
abort
用的比较少,了解即可
总的来说,**系统调用中举例的这三个函数关系是:
kill
包含
raise
,
raise
包含
abort
,作用范围是在逐渐缩小的**
4、软件条件
信号产生(发送)的第三种方式:软件条件
其实这种方式我们之前就接触过了:**管道读写时,如果读端关闭,那么操作系统会发送信号终止写端,这个就是 软件条件 引发的信号发送,发出的是
13
号
SIGPIPE
信号**
4.1、alarm 设置闹钟
系统为我们提供了 **闹钟(报警):
alarm
**,这个 闹钟 可不是用来起床的,而是用来 定时 的
返回值:**如果上一个闹钟还有剩余时间,则返回剩余时间,否则返回
0
**
参数:想要设定的时间,单位是秒
**当时间到达闹钟中的预设时间时,闹钟会响,并且发送
14
号
SIGALRM
信号**
比如这样:
#include<iostream>#include<signal.h>#include<unistd.h>usingnamespace std;intmain(){alarm(5);//设定一个五秒后的闹钟int n =1;while(true){
cout <<"我是一个进程,已经运行了 "<< n <<" 秒 PID: "<<getpid()<< endl;sleep(1);
n++;}return0;}
我们也可以更改
14
号
SIGALRM
信号的执行动作,让闹钟不断响起(自举)
#include<iostream>#include<signal.h>#include<unistd.h>usingnamespace std;voidhandler(int signo){
cout <<"收到了 "<< signo <<" 号信号,已执行新动作"<< endl;int n =alarm(10);
cout <<"上一个闹钟剩余时间: "<< n << endl;}intmain(){signal(SIGALRM, handler);alarm(10);//设定一个十秒后的闹钟while(true){
cout <<"我是一个进程,我正在运行…… PID: "<<getpid()<< endl;sleep(1);};return0;}
系统中不止一个闹钟,所以
OS
需要 先描述,再组织,将这些闹钟管理起来
可以借助闹钟,简单测试一下当前服务器的算力
4.2、测试算力
如何简单粗暴的测试算力? **设个
1
秒后响起的闹钟,看看程序能将一个值累加至多少**
#include<iostream>#include<signal.h>#include<unistd.h>usingnamespace std;intmain(){alarm(1);//设定一个一秒后的闹钟int n =0;while(true){
cout << n++<< endl;};return0;}
这个云服务这么拉吗?只能累加几万次
其实不是,主要是因为当前程序涉及了
IO
,这是非常耗时间的,可以取消
IO
并修改
SIGALRM
的执行动作为打印变量,看看能累加多少次
#include<iostream>#include<signal.h>#include<unistd.h>usingnamespace std;int n =0;voidhandler(int signo){
cout << n << endl;
exit(1);}intmain(){signal(SIGALRM, handler);alarm(1);//设定一个一秒后的闹钟while(true)
n++;return0;}
可以看到,取消
IO
后,**累加了
5
亿多次**,近
10000
倍的差距
通过这个简单的小程序证明了一件事:**
IO
是非常慢的,能不
IO
就不
IO
**
注:因为当前是云服务器,存在 网络延迟 的影响,所以实际差异更大
注意:闹钟是一次性的,只能响一次
5、硬件异常
最后一种产生(发送)信号的方式是:硬件异常
所谓 硬件异常 其实就是我们在写程序最常遇到的各种报错,比如 除 0、野指针
5.1、除 0 导致异常
先来看一段简单的错误代码
#include<iostream>usingnamespace std;intmain(){int n =10;
n /=0;return0;}
显然是会报错的是,毕竟 **
0
不能作为常数**
根据报错信息,可以推测出此时发送的是
8
号
SIGFPE
信号(浮点异常)
让我们通过
signal
更改
8
号信号的执行动作,尝试逆天改命,让 除 0 合法?
#include<iostream>#include<signal.h>#include<unistd.h>usingnamespace std;voidhandler(int signo){
cout <<"虽然除 0 了,但我不终止进程"<< endl;}intmain(){signal(SIGFPE, handler);int n =10;
n /=0;return0;}
结果:一直在死循环似的发送信号,明明只发生了一次 除 0 行为
想要明白背后的原理,需要先认识一下 状态寄存器
5.2、状态寄存器
在
CPU
中,存在很多 寄存器,其中大部分主要用来存储数据信息,用于运算,除此之外,还存在一种特殊的 寄存器 =》 状态寄存器,这个 寄存器 专门用来检测当前进程是否出现错误行为,如果有,就会把 **状态寄存器(位图结构)中对应的比特位置
1
,意味着出现了 异常**
当操作系统检测到 状态寄存器 出现异常时,会根据其中的值,向出现异常的进程 轮询式 的发送信号,目的就是让进程退出
比如上面的 除 0 代码,发生异常后,
CPU
将 状态寄存器 修改,变成 异常状态,操作系统检测到 异常 后会向进程发送
8
号信号,即使我们修改了
8
号信号的执行动作,但 **因为状态寄存器仍然处于异常状态,所以操作系统才会不断发送
8
号信号,所以才会死循环式的打印**
能让 状态寄存器 变为 异常 的都不是小问题,需要立即终止进程,然后寻找、解决问题
毕竟如果让 除 0 变为合法,那最终的结果是多少呢?所以操作系统才会不断发送信号,目的就是 终止进程的运行
5.3、野指针导致异常
除了 除 0 异常外,还有一个 臭名昭著 的异常:野指针问题
比如:
#include<iostream>usingnamespace std;intmain(){int* ptr =nullptr;*ptr =10;return0;}
Segmentation fault
段错误 这是每个
C/C++
程序猿都会遇到的问题,因为太容易触发了,出现段错误问题时,操作系统会发送
11
号
SIGSEGV
信号终止进程,可以通过修改执行动作验证,这里不再演示
那么 野指针 问题是如何引发的呢?
借用一下 共享内存 中的图~
野指针问题主要分为两类:
- 指向不该指向的空间
- 权限不匹配,比如只读的区域,偏要去写
共识:**在执行
*ptr = 10
这句代码时,首先会进行 虚拟地址 -> 真实(物理)地址 之间的转换**
指向不该指向的空间:这很好理解,就是页表没有将 **这块虚拟地址空间 与 真实(物理)地址空间 建立映射关系,此时进行访问时
MMU
识别到异常,于是
MMU
直接报错,操作系统识别到
MMU
异常后,向对应的进程发出终止信号**
**
C
语言中对于越界 读 的检查不够严格,属于抽查行为,因此野指针越界读还不一定报错,但越界写是一定会报错的**
权限不匹配:页表中除了保存映射关系外,**还会保存该区域的权限情况,比如
是否命中 / RW
等权限,当发生操作与权限不匹配时,比如
nullptr
只允许读取,并不允许其他行为,此时解引用就会触发
MMU
异常,操作系统识别到后,同样会对对应的进程发出终止信号**
页表中的属性
- 是否命中
RW
权限UK
权限(不必关心)
注:**
MMU
是内存管理单元,主要负责 虚拟地址 与 物理地址 间的转换工作,同时还会识别各种异常行为**
一旦引发硬件层面的问题,操作系统会直接发信号,立即终止进程
到目前为止,我们学习了很多信号,分别对应着不同的情况,其中有些信号还反映了异常信息,所以将信号进行细分,还是很有必要的
6、核心转储
Linux
中提供了一种系统级别的能力,当一个进程在出现异常的时候,
OS
可以将该进程在异常的时候,核心代码部分进行 核心转储,将内存中进程的相关数据,全部
dump
到磁盘中,一般会在当前进程的运行目录下,形成
core.pid
这样的二进制文件(核心转储 文件)
6.1、核心转储的概念
对于某些信号来说,**当终止进程后,需要进行
core dump
,产生核心转储文件**
比如:
3号 SIGQUIT
、
4号 SIGILL
、
5号 SIGTRAP
、
6号 SIGABRT
、
7号 SIGBUS
、
8号 SIGFPE
、
11号 SIGSEGV
、
24号 SIGXCPU
、
25号 SIGXFSZ
、
31号 SIGSYS
都是可以产生核心转储文件的
**不同信号的动作(
Action
)**
Trem
-> 单纯终止进程Core
-> 先发生核心转储,生成核心转储文件(前提是此功能已打开),再终止进程
但在前面的学习中,我们用过
3
、
6
、
8
、
11
号信号,都没有发现 核心转储 文件啊
难道是我们的环境有问题吗?
确实,当前环境确实有问题,因为它是 云服务器,而 云服务器 中默认是关闭核心转储功能的
6.2、打开与关闭核心转储
通过指令
ulimit -a
查看当前系统中的资源限制情况
ulimit -a
可以看到,**当前系统中的核心转储文件大小为
0
,即不生成核心转储文件**
通过指令手动设置核心转储文件大小
ulimit -c 1024
现在可以生成核心转储文件了
就拿之前的 野指针 代码测试,因为它发送的是
11
号信号,会产生
core dump
文件
核心转储文件是很大的,而有很多信号都会产生核心转储文件,所以云服务器一般默认是关闭的
云服务器上是可以部署服务的,一般程序发生错误后,会立即重启
**如果打开了核心转储,一旦程序 不断挂掉、又不断重启,那么必然会产生大量的核心转储文件,当文件足够多时,磁盘被挤满,导致系统IO
异常,最终会导致整个服务器挂掉的**
**还有一个重要问题是core
文件中可能包含用户密码等敏感信息,不安全**
关闭核心转储很简单,设置为
0
就好了
ulimit -c 0
6.3、核心转储的作用
如此大的核心转储文件有什么用呢?
答案是 调试
没错,核心转储文件可以调试,并且直接从出错的地方开始调试
这种调试方式叫做 事后调试
调试方法:
gcc / g++
编译时加上-g
生成可调试文件- 运行程序,生成
core-dump
文件 gdb 程序
进入调试模式core-file core.file
利用核心转储文件,快速定位至出错的地方
之前在 进程创建、控制、等待 中,我们谈到了 **当进程异常退出时(被信号终止),不再设置退出码,而是设置
core dump
位 及 终止信号**
也就是说,父进程可以借此判断子进程是否产生了 核心转储 文件
🌆总结
以上就是本次关于 Linux进程信号【信号产生】的全部内容了,作为进程信号系列的开篇之作,包含了很多内容,首先是对信号的产生、保存、处理相关概念进行了学习,然后针对信号产生,阐述了四种不同的方式,最后学习了核心转储的相关概念,掌握了一种特殊的调试方式
相关文章推荐
Linux进程间通信【消息队列、信号量】
Linux进程间通信【共享内存】
Linux进程间通信【命名管道】
Linux进程间通信【匿名管道】
Linux基础IO【软硬链接与动静态库】
Linux基础IO【深入理解文件系统】
版权归原作者 北 海 所有, 如有侵权,请联系我们删除。