![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/b97950c98b1edd93aa382ca9171647a9.jpeg#pic_center)
一. 生活层面的信号
在学习进程信号前,我们不妨认识一下现实中有哪些信号。
日常生活中,眼神,语气,手势,等等,都可以是一个信号,都可以传递一定信息
。而我们常见的红绿灯,红灯代表禁止通行,绿色代表允许通行,黄灯代表转换过渡。这些并不是我们一开始就知道,是因为大众的共识性。而各种手势,也是在幼儿园老师有所教导,我们才得以认知。
所以信号的创建是需要被人们所认知的。
我们可以通过生活中的一些事情引出信号的预备知识
进程信号的预备知识
网购是当今热门的购物选择,当我们网购的东西送到时,我们就会收到,“快递到了”这一信号,
首先,这一信号是大众的共识性认知,我们可以“识别信号
”
其次,在收到这一信号时,我们可能立刻就去取快递了,但是也有可能我们正在做着一些不方便离开的事情,所以我们不会立刻去取快递。这就说明,信号的处理和信号的接收并不一定衔接。我们可以在“合适的时候处理信号
”
并且,如果我们不立刻去取快递,我们还需要记录“快递到了”这一信号,因为我们需要之后处理。我们需要“记住有一个信号要处理
”
最后,对于快递的处理,1.默认动作
(打开快递);2.自定义动作
(如果是卖给女朋友的礼物,就送给女朋友);3.忽略
PS:快递的到来是不定时的,接收快递和我们已经在做的事是异步的
总结一下:
- 信号创建首先是需要能被人们
识别
。 - 接收信号后可以不立刻处理信号,
等到合适的时候再处理
- 因为可以不立刻处理信号,那么就需要存在
记录信号的能力
- 信号的产生对于进程来说是
异步
的
二. 进程信号
我们可以通过kill -l
命令查看所有的进程信号
其中,1到31是非实时信号
;34到64是实时信号
。
本篇博客仅学习部分非实时信号。
实时信号,只需要保存有无产生
,不需要立刻处理,具体处理可以之后进行。
操作系统可被分为实时系统
和非实时系统
。Linux和Windows都是非实时系统
,而实时系统是高响应的,需要对任何命名立刻响应。比如车载系统中的刹车
。
而非实时信号刚好有32个,操作系统使用位图
来存储非实时信号。存储在进程的pcb结构体
中,所以发送信号其实是将信号写入进程pcb的位图,修改位图的比特位
,将0->1
。
比特位的位置:信号的编号
比特位的内容:是否收到该信号
在Linux中,当我们不小心写了一个死循环的程序,我们可以通过ctrl+c
终止这个程序
ctrl+c
其实就是通过键盘输入给OS,OS捕捉,然后发送一个信号给当前进程,然后终止这个程序。
另外还有一点需要注意,ctrl+c
只能终止前台进程,如果我们将程序变为后台运行,则无法通过ctrl+c
终止,不过可以使用kill+信号
终止进程
其实ctrl+c
本质是让OS给指定进程发送2号信号SIGINT
接下来我们通过singal()
函数验证一下
signal函数可以接收信号,并由我们指定接收该信号后,执行的动作。
比如ctrl+c
是发送了2号信号,执行动作是终止程序
int signum
:接收的信号
sighandler_t handler
:sighandler是一个函数指针
,函数的返回值是void,参数是int。
接下来,我们用一个程序证明ctrl+c
本质是发送了2号信号
可以看到,这次我们使用ctrl+c没能终止程序,而且打印出了"get signal:2",并且当我们发送2号信号,也是执行handler方法
首先,我们使用signal函数对2号信号进行捕捉
,并且让handler作为2号信号的处理动作
。
并且OS会将接收的信号传参给handler
。所以我们使用ctrl+c没能终止程序,而是打印出了handler的内存
我们上面也讲了,对信号的处理分三种:默认动作,自定义动作,忽略
而终止程序就是2号信号的默认动作
,而我们编写的handler就是自定义动作
我们可以通过man 7 signal
查看信号的默认动作
Term
就代表终止进程
PS:ctrl+\
是发送3号信号SIGQUIT,但是9号信号SIGKILL是管理员信号,即使使用signal捕捉执行自定义动作,kill -9还是执行默认动作即终止进程。
三. 硬件中断
我们按下crtl+c,那计算机是怎么知道我们输入了什么数据呢?
键盘其实是通过硬件中断
的方式,通知操作系统,我们按下了按键
。
注意:硬件中断只是让操作系统知道我们按下了按键,但是具体按了什么,操作系统此时还不知道
那么,什么是硬件中断呢?
内核中有
中断控制器
这样一个硬件,我们拿8259举例,当我们按下键盘,其实是发送了电脉冲
,然后通过中断控制器发送给特定的CPU特定的针脚
。当针脚处于高电频
时,就相当于写入数据
,CPU中的寄存器
会写入高电频针脚的编号
。这就是发送中断
的过程
中断向量表
类似于函数指针数组
,CPU获得中断后,就会找中断向量表中对应的函数指针
,调用对应的方式
。
比如9号针脚高电频,那么寄存器中就会写入9,然后找中断向量表中9号函数指针,调用“从键盘获取对应数据”的方法。这就是硬件中断。
注意:键盘被按下,键盘哪些被按下是两个不同的步骤。
键盘被按下,对应的是
硬件中断
键盘哪些被按下,对应的是中断向量表中的方法获取键盘输入的数据。
所以ctrl+c
发生信号的本质是:
先按下按键,CPU获得
硬件中断
,然后调用方法,OS去读取键盘输入的ctrl+c
数据,OS再将其解释为信号
,并发送给前台进程,写入其pcb结构体的信号的位图
。
四. 信号产生
信号的第一种产生方式是通过键盘
,ctrl+c
,ctrl+/
。
第二种
产生方式是kill指令
第三种产生方式是通过系统调用
第一个系统调用函数是kill()
int pid
:指定给某个进程发送信号
int sig
:发送几号信号
接下来,我们可以模拟实现kill命令
mykill.cc
模拟实现 kill 命令
#include<iostream>
#include<cstdlib>
#include<cerrno>
#include<cstring>
#include<unistd.h>
#include<signal.h>
#include<string>
#include<sys/types.h>
using namespace std;
//传参不正确,展示使用手册
void Usage(string proc)
{
cout<<"Usage:"<<endl;
cout<<"\t"<<proc<<" 信号编号 目标进程"<<endl;
}
// ./mykill 9 1234
int main(int argc,char*argv[])
{
//argc:命令行参数个数
if(argc!=3)
{
//第一个参数是 比如: ./进程名
Usage(argv[0]);
exit(1);
}
//信号
int signo=atoi(argv[1]);
//目标进程的pid
int target_id=atoi(argv[2]);
//发送信号
int n=kill(target_id,signo);
if(n!=0)
{
cerr<<errno<<" : "<<strerror(errno)<<endl;
exit(2);
}
return 0;
}
myproc.cc
死循环程序,需要被杀死的程序
#include<iostream>
#include<unistd.h>
using namespace std;
int main()
{
while(1)
{
cout<<"我是一个进程,我正在执行...,我的pid:"<<getpid()<<endl;
sleep(1);
}
return 0;
}
运行结果如下:
第二个系统调用函数是raise()
int sig
:几号信号
可以看到,调用完raise(2)后,程序就结束了,并没有输出后续内容。
第三个系统调用函数是abort()
调用abort()函数,会给当前进程发送6号信号SIGABRT
但是abort()是C语言的接口,其函数内部还有类似exit的操作
,所以即使使用signal函数捕捉6号信号,进行自定义动作,abort函数还是会使进程终止
第四种
信号产生的方式是由软件条件产生信号
unsigned int seconds:
seconds秒
之后给该进程发送14号信号SIGALRM
但是我们可以通过发送14号信号,提前触发alarm
。而当我们再调用alarm(),其返回值就是上一次alarm还剩余的时间
。
但如果我们提前触发alarm,但是没有重新设置
,则之后还会再收到一次alarm
。alarm(0)代表取消闹钟
比如我们设置一个alarm(30),但如果我在20秒时,给该进程发送14号信号,然后我再调用alarm(15),那么就会再设置一个15秒的闹钟,但是这个闹钟的返回值是上一次闹钟剩余时间,也就是10秒。
PS:alarm是系统接口,OS中有维护alarm的结构体,会定期查看是否有闹钟超时
第五种
信号产生的方式是硬件异常
我们在编写C/C++代码时,有时候可能会出现除0
和野指针
的情况
他们的报错分别是这样的
我们以前在语言层面的理解,就是程序崩溃了。
但是在操作系统层面,其实是OS给进程发送信号,终止进程。
除0引发的硬件异常
CPU中其实有很多寄存器,计算就发生在寄存器中。而有一个寄存器叫作
状态寄存器
,当本次计算溢出
时,状态寄存器就会被置为1
,反之为0
。
而一旦状态寄存器为1
,就会发生硬件异常
,操作系统会得知这个异常。CPU还会存储当前调度的进程的pcb结构体的地址
,操作系统在得知异常后,根据CPU记录的pcb地址,给该进程发送信号
除0本质是触发了硬件异常
,操作系统给该进程发送了8号信号
野指针引发的硬件异常
如果我们对空指针解引用,修改其内部的值。
空指针默认是虚拟地址的0地址,而通过页表映射到物理地址
修改指针内部的值,第一步是先进行虚拟地址到物理地址的转换
而页表其实是有MMU硬件
——内存管理单元
进行管理的
PS:MMU硬件是CPU的其中一个硬件
当出现以下情况之一,MMU就会报错,触发硬件异常
- 虚拟地址
没有映射
,MMU硬件报错有映射,但是没有修改的权限
,MMU硬件报错而MMU硬件报错,触发硬件异常,OS根据CPU中记录的当前进程的pcb地址,并发送信号,终止该进程。
野指针本质也是触发了硬件异常
,操作系统给该进程发送了11号信号
五. Term&Core
我们之前讲了,Term是终止进程的意思,但是刚刚的除0,野指针的8号,13号信号时Core,但好像也是直接终止进程,那么这两个有什么差别吗?
首先,OS可以将一个进程在异常的时候,可以将核心代码部分进行核心转储,将内存中进程的相关数据,全部转储到磁盘中。并在可执行程序的目录下,形成core.pid的核心转储文件。
我们可以通过ulimit -a指令查看当前进程的限制
而为什么我们之前没有看到核心转储文件呢,因为云服务器默认是关闭形成核心转储文件这一功能的,将可形成的核心转储文件大小设为0。
我们可以使用ulimit -c 大小
设置可形成的核心转储文件大小
而这时,我们就可以发现Term和Core二者的差别了。
我们发现,Action显示是Core的信号,在终止进程后,会显示出(core dumped)
,并在当前目录下形成了core文件
所以,
Term的终止就是终止
,没有多余动作
Core则是在终止时,会先进行核心转储,再终止进程
1. 核心转储的意义
我们打开core文件,发现其实一个二进制文件
。不是给我们看的
那核心转储的意义是什么呢?
其实,信号和退出码的作用是一样的,都是在进程结束后,反馈给程序员的信息
,当程序异常结束时,我们可以知道是正常结束,还是异常终止;异常终止又是因为什么?
而core文件其实就是给程序员后期调试
用的文件。
我们需要使用Debug方式
形成可执行程序,并且使用gdb调试
,才可以体会core文件的意义。(gcc / g++默认是形成release版本,最后加-g选项形成Debug版本)
通过core文件,我们在gdb的调试中,可以直接定位到产生异常的位置。
这种调试称为事后调试
2. 云服务器为什么关闭核心转储
一个大的程序,出现问题时,都无法立刻解决,程序可能会崩溃,但可能会有检测程序对其重启,而如果核心转储处于打开状态,一次崩溃就会形成一个core文件;如果这个程序在半夜崩溃,就会一直重启,崩溃
,反复进行,每一次重启的进程又不同,正如上述,同一个程序,只是变成进程后的pid不同,Core终止就会重新形成一个core文件
。
这样就会造成很多浪费
,所以云服务器一般关闭核心转储功能。
ulimit -c 0
就是关闭核心转储
3. core dump标志
在进程等待时,父进程获取子进程的退出信息使用的status位图结构
在正常终止时,位图的8~15位是退出码
被信号所杀时,0~6是终止信号,而第8位是core dump标志。
如果该进程核心转储功能是打开的,那么该进程的core dump为1,反之为0。
六. 总结
- 所有的信号产生,最终都要OS来执行,因为
OS是软硬件的管理者
,也是进程的管理者
- 信号的处理可以
不立刻处理
,进程可以在合适的时候
再对信号进行处理- 如果不立即处理,那么信号需要被记录到
pcb结构体
中- 一个进程在没有收到信号的时候,是知道如何处理某个信号的,因为在编码时已经提供给每个进程了
- 任何一种信号产生,不管是通过键盘,还是指令,系统调用…本质都是
操作系统往进程的pcb结构体中写入信号
结束语
本篇内容到此就结束了,感谢你的阅读!
如果有补充或者纠正的地方,欢迎评论区补充,纠错。如果觉得本篇文章对你有所帮助的话,不妨点个赞支持一下博主,拜托啦,这对我真的很重要。