您现在的位置是:首页 >技术杂谈 >【Linux】进程信号及信号产生网站首页技术杂谈

【Linux】进程信号及信号产生

好想有猫猫 2023-07-14 16:00:02
简介【Linux】进程信号及信号产生


在这里插入图片描述

一. 生活层面的信号

在学习进程信号前,我们不妨认识一下现实中有哪些信号。
日常生活中,眼神,语气,手势,等等,都可以是一个信号,都可以传递一定信息。而我们常见的红绿灯,红灯代表禁止通行,绿色代表允许通行,黄灯代表转换过渡。这些并不是我们一开始就知道,是因为大众的共识性。而各种手势,也是在幼儿园老师有所教导,我们才得以认知。
所以信号的创建是需要被人们所认知的。

我们可以通过生活中的一些事情引出信号的预备知识
进程信号的预备知识

网购是当今热门的购物选择,当我们网购的东西送到时,我们就会收到,“快递到了”这一信号,
首先,这一信号是大众的共识性认知,我们可以“识别信号
其次,在收到这一信号时,我们可能立刻就去取快递了,但是也有可能我们正在做着一些不方便离开的事情,所以我们不会立刻去取快递。这就说明,信号的处理和信号的接收并不一定衔接。我们可以在“合适的时候处理信号
并且,如果我们不立刻去取快递,我们还需要记录“快递到了”这一信号,因为我们需要之后处理。我们需要“记住有一个信号要处理
最后,对于快递的处理,1. 默认动作(打开快递);2. 自定义动作(如果是卖给女朋友的礼物,就送给女朋友);3. 忽略
PS:快递的到来是不定时的,接收快递和我们已经在做的事是异步的

总结一下:

  1. 信号创建首先是需要能被人们识别
  2. 接收信号后可以不立刻处理信号,等到合适的时候再处理
  3. 因为可以不立刻处理信号,那么就需要存在记录信号的能力
  4. 信号的产生对于进程来说是异步

二. 进程信号

我们可以通过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+cctrl+/
第二种产生方式是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<<"	"<<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就会报错,触发硬件异常

  1. 虚拟地址没有映射,MMU硬件报错
  2. 有映射,但是没有修改的权限,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。

六. 总结

  1. 所有的信号产生,最终都要OS来执行,因为OS是软硬件的管理者,也是进程的管理者
  2. 信号的处理可以不立刻处理,进程可以在合适的时候再对信号进行处理
  3. 如果不立即处理,那么信号需要被记录到pcb结构体
  4. 一个进程在没有收到信号的时候,是知道如何处理某个信号的,因为在编码时已经提供给每个进程了
  5. 任何一种信号产生,不管是通过键盘,还是指令,系统调用…本质都是操作系统往进程的pcb结构体中写入信号

结束语

本篇内容到此就结束了,感谢你的阅读!

如果有补充或者纠正的地方,欢迎评论区补充,纠错。如果觉得本篇文章对你有所帮助的话,不妨点个赞支持一下博主,拜托啦,这对我真的很重要。
在这里插入图片描述

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。