目录
信号入门
生活中的信号
常见信号
信号产生的前置知识
组合键转化成信号
编辑
前后台进程
理解组合键如何转化为信号
信号的产生
通过终端按键产生信号
Core Dump(核心转储)
通过系统调用函数产生信号
kill函数
raise函数
abort函数
由代码异常产生信号
除0异常
野指针的使用
由软件条件产生信号
使用系统调用alarm函数产生信号
信号是进程之间事件异步通知的一种方式,属于软中断。
注:信号与信号量没有任何关系,信号只是用来通知进程发生了什么事情,但并不给进程传递任何数据。
在生活中我们会遇到各种信号,如:红路灯,闹钟,古代的狼烟等。看到这些信号我们就知道应该做什么了。为什么我们知道要怎么做呢?因为我们在遇到之前就被告知遇到要怎么做了,因此,结合生活,我们可以对进程信号有以 下推论:
- 当收到信号时,进程可以识别信号,并执行对应信号的动作。
- 当信号没有产生时,进程就知道如何处理信号。
- 当进程收到信号时,可能不会立即执行信号。
- 当进程无法立即处理信号时,信号会被保存下来,等合适时间再处理信号
- 信号的产生相对于进程是异步的。
异步对于同步来说,指两个或两个以上的对象或事件的发生并没有相关顺序性。
因此信号也可以认为:是一种向目标进程发送通知消息的一种机制。
kill -l #该命令可以查看常见的信号
man 7 signal #查看信号的相关描述
Linux内核支持62种不同的信号,这些信号都有一个名字,这些名字都以SIG这三个字符开头。在头文件signal.h中,这些信号都被定义成正整数,成为信号编码。其中,1到31被成为普通信号,34到64的信号被称为实时信号,实时信号对处理的要求较高。
#include
#include
using namespace std;
int main()
{
while(1)
{
cout<<"hello world"<
Ctrl+c的本质是给进程发送2号信号,进程接收到信号后执行默认动作,结束进程。这里Ctrl+z也是一个组合键,会把进程暂停。
注:这里像Ctrl+c这样从键盘中获取的命令只能作用于前台进程,对后台进程无效。
那么什么是前后台进程呢?
进程在运行的时候,分为前台进程(可以接受来自键盘上的指令,在命令行操作时,只有一个)。
后台进程(不可以接受来自键盘上的指令,可以有多个)。
生成前台进程:
./xxx(可执行文件名)
生成后台进程:
./xxx(可执行文件名) &
区分前后台进程可以依据其是否可以接受到键盘指令:
如:
#include
#include
using namespace std;
int main()
{
while(1)
{
cout<<"hello world"<
当我们前台执行时,按Ctrl+c
发现,进程接受到键盘指令,立即终止。
后台执行,按Ctrl+c
发现,进程未接受到键盘指令,没有终止。
这里我们再介绍几个与其相关的指令:
jobs //查看前后台进程指令
fg number(进程编号,可以用jobs找到进程对应的编号) //将后台进程变为前台进程
bg number //让暂停的进程开始执行(开始执行后会变为后台进程)
注:我们知道shell也是一个进程。因此在我们执行一个前台进程时,OS会把shell变为后台进程,当进程结束/变为后台进程时,OS会马上把shell变为前进程。Ctrl+c这样的键盘命令对shell无效。
其实键盘的工作方式是通过中断方式进行的,键盘与CPU通过针脚相连,在设计时就对针脚进行编号,当我们敲击键盘按钮时,会向针脚里释放高低电压,然后CPU可以接受到,向寄存器中写入对应的编号(中断号),这时OS可以获得编号,然后将其转化成对应的信号发送给进程。
这里我们知道信号是被进程接受的,那么在进程中又是如何判断它接受了那个信号?
在Linux中使用了数据结构中的位图来存储进程接受了那些信号。这里我们把比特位的下标来表示编号,比特位的内容来表示是否接受到了信号:为0表示没有接受,1表示接受到信号。
注:该位图结构是存储在进程的内核数据结构task_struct中的。因此只有操作系统才可以修改该位图的内容。同时我们据此还可以得出一条结论:无论你发送得那个信号,信号是由什么产生的。最终都是由操作系统向进程发送信号的。
因为,进程是依据task_struct结构体里的一个位图里的内容来判断是否接受信号,而只有才做系统才可以修改该位图的内容,让进程知道自己已经接受到了信号。
在上面的内容中已经提到,在键盘上的组合键Ctrl+c可以向进程发送2号信号。
这里又如何证明是发送了2号信号呢?这里就不得不提signal这个系统调用函数。
在使用signal函数后,当进程接受到signum信号时,就会调用handler函数(handler是一个函数指针类型的变量,函数指针指向一个返回值为void,参数为一个int。函数内容由自己决定)并将signum传给handler函数。signal函数的返回值是对于sugnum信号的旧的处理方法。其实signal函数相当于修改某个信号的执行动作。这里OS中也定义了两个动作:SIGDFL(信号默认动作),SIGIGN(信号的忽略)
#include
#include
#include
using namespace std;
void handler(int signo)
{
cout<<"handler: "<
这里我们可以看出用组合键Ctrl+c与kill -2 的动作一样,所以Ctrl+c是向进程发送2号信号。
注:
- signal函数仅仅是修改进程对特定信号的后续动作的处理,并不会直接调用对应的处理动作。而是当进程接受到特定的信号时,才会去调用对应的处理动作。如果后进程在后续并没有接收到对应的信号,那么被修改的信号动作并不会执行。
- 还要注意要想修让改信号的动作实现,必须要在执行对应信号前,去执行signal这个函数。
- 在修改信号动作时,并不是要求我们必须让进程停止下来。因此我们可以对于一些我们想要特殊处理的信号动作仅仅是打印一些东西。
这里可能会有人提问,既然这样如果我们把1至31的信号动作全部修改(即不会让进程终止),那么一个无限执行的进程是不是就会一直执行下去了?
答案当然不是的,在大佬们创造操作系统时,早已想到了这种情况,因此在操作系统中会强制要求一些信号的执行动作不能被signal函数修改。
代码验证:
#include
#include
#include
using namespace std;
void handler(int signo)
{
cout<<"handler: "<
这里我们从图中可以看出9号信号的动作并没有被修改,当然这里并不是只有一个这样的信号:19号信号也没有被修改,这里我就不验证了。
这里我们可以看到由一般的信号的默认行为是终止进程(有Term和Core)。这里Core更像是异常情况,一般报Core的问题更加严重,往往需要用户关心,需要用户进一步去排查。但是我们在运行程序时,Core与Term出现时,并没有区别。这里如果我们像观察可以这样(针对云服务器):
首先:先编写一个会出现Core问题的代码:
#include
#include
#include
int main()
{
std::cout<<"proccess pid:"<
编译运行:
这里我们查看signal手册:
但是这里我们并没有观察到core 这个文件。这是为什么呢?
这里core dump是核心转储,在进程终止时触发核心转储,它会在当前崩溃的目录下,形成一个以
core.进程pid文件。这个文件里记录着进程运行崩溃时核心的上下文数据(将进程运行时的数据导入磁盘里)。这里我们要想看见,首先要用指令:
ulimit -a(查看系统基本配置项)
这里我们可以看见core的默认单位blocks被设置成了0,说明当前服务器对于程序出异常默认产生core文件这件事是关闭的。这里我们可以通过指令改变:
ulimit -c 1024(设置core dump产生文件及大小)
然后再次查看:ulimit -a
注意:ulimit -c 这个是内存级的设置,如果我们把Xshell关闭,那它会重新恢复成0.
那么这里我们有两个问题:1.为什么程序异常(Core行为)会把进程运行的数据导入磁盘里?2.为什么要将进程出现Core行为时,产生core文件关闭?
1.支持程序员后续的调试。
2.这里我们可以发现,我们才写了几行代码,产生的core文件大小就为561152,这里在工作中,那些项目是这个的很多倍,如果出现Core异常,产生的Core文件可能会把机器的磁盘全部写满,因此这里默认关闭。(在虚拟机上是打开的)
作用:向指定的任意进程发送指定的信号
代码演示:
自己随便写一个程序:
#include
#include
#include
using namespace std;
int main()
{
while(1)
{
cout<<"getpid: "<
自己通过调用系统调用接口kill函数自己实现kill指令:
#include
#include
#include
#include
using namespace std;
void Usage(char* number)
{
cout<<"\nUsage: "<
执行第一个程序,然后以用kill指令的方式,执行第二个程序
这里我们发现test程序向第一个程序发送了9号信号。
作用:只能向自己所在的进程发送指定的信号。
代码演示:
#include
#include
#include
using namespace std;
void handler(int signo)
{
cout<<"handler: "<
这里我们发现程序并没有休眠10秒而是在收到2号信号时立即执行了信号动作。
作用:给该函数所在的进程发送6号信号SIGBRT,来终止进程。
代码演示:
#include
#include
#include
using namespace std;
void handler(int signo)
{
cout<<"handler: "<
注:6号信号虽然允许修改信号动作,但是abort函数规定:如果6号信号被修改后不会使进程退出,这里会强制进程退出,这也是为甚在执行时会在打印Aborted后进程会退出的原因。
直接举例:
#include
using namespace std;
int main()
{
int a=10;
a/=0;
return 0;
}
这里规定除数不能为0,否则会发生异常,使进程退出。
那么为什么除数为0会出现异常呢?
这里我们可以根据出现异常的本质来解释:
首先这里出现异常并不是C语言的问题,这里我们要知道,在代码中的计算是这样的:CPU读取代码里的数据将其保存在寄存器中,然后按照使用的运算符进行计算将计算结果也保存在寄存器中,但是,这里我们要知道,在计算机中0是一个非常小的数,导致计算结果超级大发生溢出,导致CPU中一个状态寄存器的溢出标记为置为1(可以用来表示计算结果是否溢出,0为正常,1为溢出),而在进程执行的时候,一旦状态寄存器的溢出标记置为1,CPU就是告诉操作系统出现异常,让操作系统类似于使用kill函数一样向进程发出一个信号,导致进程退出,然后执行下一个进程,使用CPU的状态寄存器恢复正常。
注:因此再次强调这里除0不是语言的问题,而是CPU硬件方面产生了问题,因此无论你是用什么语言写都会出现这样的异常。
那么这里除0错误发出的是几号信号呢?
这里我们可以通过指令:
man 7 signal
出现除0错误操作系统会发出8号信号。
这里也可以用代码验证:
#include
#include
#include
using namespace std;
void handler(int signo)
{
cout<<"handler: "<
注:虽然修改了8号信号的动作,但是我们并没有结束进程。
看出这里并没有打印一次就退出,而是无限循环的执行8好信号的动作,这里我们可以推断出,进程一直在接受8号信号并执行。那这是为什么呢?
这里我们要操作系统向进程发送异常的目的就将该进程杀掉,然后执行下一个进程,使CPU的状态寄存器里的溢出标志位置为0。这里由于我们修改了8号信号的动作导致进程不退出,状态寄存器里的溢出标记位一直位1,它会不断的向操作系统请求,让其发送8号信号,因此这里会无限循环打印。
#include
#include
#include
using namespace std;
void handler(int signo)
{
cout<<"handler: "<
异常产生原因:
这里我们知道一个进程是有虚拟地址空间物和理地址空间,他们之间通过页表可以映射找到。
这里我们有一个硬件MMU(内存管理单元,帮我们完成从虚拟地址空间到物理地址空间的转化,集成在CPU上)。而我们在对*p赋值时,我们会通过MMU在页表找到*p对应的物理空间完成赋值,这里由于p=nullptr,所以在页表中并不存在*p和物理地址的映射关系,导致MMU里面的一个标记位变为1,引起硬件问题,导致操作系统收到请求,向进程发送异常以解决问题。
如果学过进程间的通信,知道在使用匿名管道时,如果读端关闭,写端会把立马终止。
写端终止,原因就是操作系统识别到读端关闭后,写端再写就没有任何意义了,所以向写端发送了SIGPIPE信号。 像管道的读端关闭写端还在写的情况,其实时不符合软件条件的(管道通信的条件,管道也是一种软件),那么操作系统就会向不符合软件条件的进程发送特定的信号终止进程。
因此操作系统不仅会因为硬件出现异常发送信号,当软件出现问题也会向进程发送信号。因为操作系统是管理软硬件资源的。
这里我们要知道,信号大多数是由异常产生的,但并不是所有信号都是异常产生的,这里我们介绍一个并不是由异常产生的信号:
调用 alarm 函数可以设定一个闹钟 , 也就是告诉内核在 seconds 秒之后给当前进程发 SIGALRM 14号 信号 , 该信号的默认处理动 作是终止当前进程。
这个函数的返回值是 0 或者是以前设定的闹钟时间还余下的秒数。打个比方 , 某人要小睡一觉 , 设定闹钟为 30 分钟之后响,20 分钟后被人吵醒了 , 还想多睡一会儿 , 于是重新设定闹钟为 15 分钟之后响 ,“ 以前设定的闹钟时间还余下的时间 ” 就 是10 分钟。如果 seconds 值为 0, 表示取消以前设定的闹钟 , 函数的返回值仍然是以前设定的闹钟时还余下的秒数。
可以看出,count在1s内加了9万多次,这里算是比较小的,其实是由count和网络传输数据慢导致的。如果想单纯的看计算力,可以这样:
#include
#include
#include
using namespace std;
int count=0;
void handler(int signo)
{
cout<<"handler: "<
注:如果设置了一个闹钟,这个闹钟一旦被触发,就会自动被移除。
下面代码可以做到每隔一秒就发送依次SIGALARM。
#include
#include
#include
using namespace std;
void handler(int signo)
{
alarm(1);
cout<<"handler: "<
这里我们要如何理解软件条件产生信号呢?
操作系统先识别到某种软件条件出发或不满足,然后操作系统构建信号发送给指定的进程。