🎬 个人主页:HABuo

📖 个人专栏:《C++系列》《Linux系列》《数据结构》《C语言系列》《Python系列》《YOLO系列》

⛰️ 如果再也不能见到你,祝你早安,午安,晚安


目录

📚一、信号的储备知识及概念

📚二、信号产生

📖2.1 通过键盘发送信号

📖2.2 使用系统调用向目标进程发送信号

📖2.3 硬件异常产生信号

📖2.4 软件条件产生信号

📚三、核心转储问题

📚四、总结


前言:

从本篇博客开始进入进程信号部分的学习,进程信号将从信号产生、信号保存、信号递达等内容展开,本篇博客主要介绍信号及信号产生相关知识。比较轻松易懂!


📚一、信号的储备知识及概念

前面的学习中我们使用过kill -9指令,当时我们说这个是给进程发送9号信号,杀死进程。只是说,但是从来没有从底层上分析过。包括ctrl+c,我们常使用ctrl+c终止进程,事实上它也是给进程发送信号来完成的!

信号的学习将伴随以下流程:

预备工作主要是知道下面几个共识性问题

  • 进程必须能 识别 + 处理信号。
  • 凭什么进程能够识别信号?是事先程序员就内置了一套代码用于识别+处理信号
  • 当进程收到信号的时候,进程可能正在执行更重要的代码,所以信号不一定会被立即处理
  • 因为不一定立即处理,所以进程本身必须要有对于信号的保存能力(必定储存在PCB)
  • 信号如何处理?1. 默认动作,2. 忽略 3. 自定义动作(signal信号的捕捉)

Linux下的进程信号:

[1,31]:普通信号
[34,64]:实时信号

CTRL+c就是向进程发送2号信号

📚二、信号产生

本篇文章讲解的则是信号的产生

信号的产生一共有四种方式:

  1. 通过键盘发送信号
  2. 使用系统调用向目标进程发送信号
  3. 硬件异常产生信号
  4. 软件条件产生信号

📖2.1 通过键盘发送信号

前面学习中我们使用CTRL+c或CTRL_/的方式终止进程就是通过键盘发送信号,CTRL+c产生的是2号信号,也就是SIGINT信号,CTRL+/产生3号信号,也就是SIGQUIT

使用man 7 singal查看信号描述

上面我们了解到的结论进程接收到信号,有默认行为,我们也可以自定义,也可以忽略,这里action就是默认行为,term就是终结的缩写,即中断,core也是中断。

📖2.2 使用系统调用向目标进程发送信号

在函数中调用kill,rais,abort等函数也能产生信号

kill是向指定进程发送指定的信号

raise是向自己发送指定的信号

abort是终止当前进程(发送6号信号)

📖2.3 硬件异常产生信号

硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以O的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。

进一步解释为什么OS知道我除0了?OS怎么知道我野指针了?

当cpu计算发现出现除0错误会将对应的状态寄存器中溢出标记位由0置1,OS知道CPU运算异常后会给进程发送8号异常信号,继而终止进程

MMU单元存在于CPU中,当发生对0地址访问时,页表在阻拦的同时,也会将对应的MMU单元设置为异常,OS读到MMU异常后就会给进程发送异常信号

试验代码:

void catchSig(int signo)
{
    std::cout << "获取到一个信号,信号编号是: " << std::endl;
}
int main()
{
     signal(SIGFPE, catchSig);//信号自定义行为,相当于注册行为,捕捉不到信号不执行
     int a = 10;
     a /= 0; 
     signal(11, catchSig);
     while (true)
     {
         std::cout << "我在运行中...." << std::endl;
         sleep(1);
     }
    return 0;
}

这段代码运行后,就会发现,为什么除0只发生一次,但结果却是一直信号被捕捉,这是为什么

这是因为:

---> 收到信号,不一定会引起进程退出 -- 没有退出,有可能还会被调到
---> CPU内部的寄存器只有一份,但是寄存器中的内容,属于当前进程的上下文!
---> 你有没有能力或者动作修正这个问题呢?没有
---> 当进程被切换的时候,就有无数次状态寄存器被保存和回复的过程
---> 所以每一次恢复的时候,就让OS识别到了CPU内部的状态寄存器中的溢出标志位是1

📖2.4 软件条件产生信号

当我们在学习管道时会遇见这种情况,管道读端不光不读了,并且还把读端关闭了,而写端还在写,此时操作系统会自动终止写进程.怎样终止的呢?事实上就是OS向写端发送了SIGPIPE信号而终止了进程而这个信号的产生属于软件层面上引起的异常,因为此时read想要从文件描述符中读取数据,但是此时该文件描述符并没有打开,所以在软件层面上资源未就绪就去访问,所以这属于软件层面的异常

闹钟同样属于软件条件:闹钟可以设置时间,时间到了同样也会发出信号

  • alarm的参数是seconds,即秒,闹钟的返回值是进程提前被终止,比所定闹钟提前了多少时间

闹钟设定的时间到了之后,我们的进程会收到14号信号SIGALRM,默认行为也是终止!

需要注意:

  • 闹钟不像前面我们对信号进行捕捉之后产生自定义行为就会一直打印,alarm不会,它是一次性的!
  • 每个进程都可以进行设置闹钟,那么多闹钟,万一乱套了怎么办?因此闹钟需不需要进行管理呢?怎么管理,先描述再组织!因此内核中必然存在描述闹钟信息的结构体!

📚三、核心转储问题

前面我们看到信号的默认行为由term、core,它们都是终止进程,但是有什么区别呢?

区别就是core是会进行核心转储,而term是正常的终止进程

核心转储:

核心转储指的是,当进程出现某种异常时,是否由操作系统将当前进程在中断位置的相关上下文转移到磁盘中,如果核心转储被设置,则会转移到磁盘,反之则不会,若信号的默认做法是core,则在终止进程的同时会发生核心转储,目录中会多出一个文件:core.XXX(进程pid)

能进行核心转储的可执行,可以在调试的时候直接执行

直接定位出现异常的位置!

可以使用ulimit -a 查看部分信息的上限

使用ulimit -c 修改设置核心转储

 补充编程技巧

利用上述的特性,我们就可以建立一个指针数组,数组中放置所有结构体第一个变量是该指针数组类型的地址,想要找到对应结构体的其它属性,直接强转到该类型即可,这就类似于C++中多态的思想!事实上,上篇博客中的共享内存、消息队列、信号量在内核中就是这样维护的!


📚四、总结

本篇博客我们主要学习的信号的储备知识以及信号的发送,小结一下:

信号的几个共识性问题

信号产生的四种方式

  1. 通过键盘发送信号
  2. 使用系统调用向目标进程发送信号
  3. 硬件异常产生信号
  4. 软件条件产生信号

其中系统调用

int kill(pid t pid, int sig)//向指定进程发送指定信号
int raise(int sig)//给自己发送指定信号
void abort(void)//发送6号信号终止进程

硬件异常中除0,野指针问题的理解

都是操作系统获取相应硬件的异常信息之后,向进程发送信号,其中除0是cpu中的状态寄存器的溢出标志位被设置,野指针是cpu中的MMU单元被设置,都是通过硬件异常提供给OS信息!

文中实验代码,捕捉信号自定义行为时,一直循环打印的原因

---> 收到信号,不一定会引起进程退出 -- 没有退出,有可能还会被调到
---> CPU内部的寄存器只有一份,但是寄存器中的内容,属于当前进程的上下文!
---> 你有没有能力或者动作修正这个问题呢?没有
---> 当进程被切换的时候,就有无数次状态寄存器被保存和回复的过程
---> 所以每一次恢复的时候,就让OS识别到了CPU内部的状态寄存器中的溢出标志位是1

核心转储问题

当进程出现异常的时候,我们将进程在对应的时刻,在内存中的有效数据转储到磁盘中,会多出一个文件:core.XXX(进程pid)

gdb调试时,输入core-file core.XXX(进程pid)就可以直接定位到异常位置!

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐