操作系统:信号究竟是什么?如何产生?

avatar
作者
猴君
阅读量:0

OS信号

一、信号的概念

信号是一种向目标进程发送通知消息的一种机制,属于软中断。本质上是用软件来模拟中断行为!

 在生活中存在很多信号,诸如红绿灯、闹钟铃声、古代狼烟、防空警报等等。以红绿灯为例,我们是如何认识红绿灯信号的。根本原因在于我们在小时候就已经有人提前告诉你如何去识别它、对应的灯亮了意味这什么,要做什么!

 同理,在操作系统中已经提前内置了信号信息。我们通过kill -l查看:

在这里插入图片描述

  • 每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到。其中 1~31为普通信号,43~64为实时信号(不关心),没有32、33号信号!这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明: man 7 signal

 在操作系统中,信号还没有产生之前,进程就能识别它(数字代号或宏),如何处理。信号的到来,我们并不清楚是什么时候,所以信号相对进程来说是异步的。信号产生后,进程不一定立即处理它,而是在合适的时候进程处理。所以我们需要将已经到来的信号进行保存

  • 所以信号如何产生?操作系统如何保存信息?

二、信号的产生

 在操作系统中,产生信号有4种方式:终端按键产生、系统调用产生、硬件异常产生、软件条件产生!

1)终端按键产生信号

 下面通过终端按键向前台进程和后台进程发送信号为例!

1、 前台进程、后台进程

 下面我们创建一个process.cc源文件,让其死循环输出信息。

#include <iostream> #include <unistd.h>  int main() {     int cnt = 0;     while(true)     {         std::cout << "running ..." << ++cnt << std::endl;         sleep(1);     }     return 0; } 
  1. 我们编译运行后,产生一个前台进程。我们可以在终端输入ctrl c发送2号信号来终止前台进程!

请添加图片描述
 我们在键盘上按下ctrl c后,会产生硬件中断。操作系统会识别到硬件数据就绪,此时操作系统读键盘上的数据,发送给目标进程。前台进程因为收到2号信号,进而引起信号退出!!

  1. 我们也可以通过ctrl z发送20号信号暂停前台进程!但由于前台进程不能被暂停,否则键盘将失效。此时当前被暂停的前台进程后转化为后台进程。shell外壳进程快速从后台切换为前台进程。

 下面我们将前台进程输入重定向到log.txt,死循环打印消息。然后ctrl z发送20号信号,此时前台进程会变为后台进程。具体效果如下:

请添加图片描述

 我们发现ctrl z向目标进程发送20号信号后,前台进程变为后台进程,并且被暂停!

  1. jobs指令可以查看当前系统中的后台进程。
  2. bg 指令+ 后台进程编号可以重新启动后台进程。fg 指令+ 后台进程编号可以将后台进程提到前台,变为前台进程!
  3. 前台进程只能有一个(键盘只有一个),后台进程可以有多个。两者本质区别在于前台进程可以接收用户输入,后台不行。shell进程比较特殊,不会被ctrl c杀掉。并且根据具体情况,Os会自动将shell提到前台或后台!!

2、验证终端按键是否产生信号

 上述我们通过终端按键让进程产生一系列行为。当ctrl c真的向目标进程发送了2号信号吗?ctrl z真的向目标进程发送了20号信号吗?我们需要进一步验证!

 操作系统提供了一个signal系统调用即可,可以自定义捕捉信号。

 #include <signal.h>    //函数原型如下,signal()第二个参数用于自定义捕捉信号  typedef void (*sighandler_t)(int);  sighandler_t signal(int signum, sighandler_t handler); 

下面我们以自定义捕捉2号信号,分别通过终端ctrl c和用户主动发送2号信号,对比进程行为!!

【源代码如下】:自定义捕捉2号信号,让进程受到2号信号退出时,打印一段消息!!

#include <iostream> #include <sys/types.h> #include <unistd.h> #include <signal.h>  void handler(int signo) {     std::cout << "自定义捕捉信号: " << signo << std::endl;     exit(0); }  int main() {     std::cout << "pid: " << getpid() << std::endl;     //自定义捕捉2号信号,signal()会将待捕捉信号种类数字传给handler()     signal(2, handler);     int cnt = 0;     while(true)     {         std::cout << "running ..." << ++cnt << std::endl;         sleep(1);     }     return 0; }  

【终端ctrl c效果】:

请添加图片描述

【发送2号信号效果】:
请添加图片描述

  • 我们发现两者行为一直,系统都受到了2号信号。进一步验证终端输入可以发送信号!!

2)调用系统函数向进程发信号

 操作系统提供了系统调用接口kill,用来向指定进程发送特定信号!

 //函数原型  #include <sys/types.h>  #include <signal.h>   int kill(pid_t pid, int sig);  //发送成功,返回0;否则返回-1 

【实例】:进程打印3次消息后,通过系统调用接口发送2号信号

#include <iostream> #include <sys/types.h> #include <unistd.h> #include <signal.h>  void handler(int signo) {     std::cout << "自定义捕捉信号: " << signo << std::endl;     exit(0); }  int main() {     int count = 3, cnt = 0;     signal(2, handler);//自定义捕捉2号信号     while(true)     {         std::cout << "running ..." << ++cnt << std::endl;         if(--count == 0)             kill(getpid(), 2);         sleep(1);     }     return 0; } 

【运行结果】:
请添加图片描述

  1. kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号。
  2. raise函数可以给当前进程发送指定的信号(自己给自己发信号)。
#include <signal.h> int raise(int signo); //是成功返回0,错误返回-1。 
  1. abort函数使当前进程接收到信号而异常终止。
#include <stdlib.h> void abort(void); //就像exit函数一样,abort函数总是会成功的,所以没有返回值。 

3)硬件异常产生信号

 下面以浮点数溢出和空指针解非法解引用错误为例

1、浮点数溢出,CPU产生信号

 我们知道除式中,除数为0是非法的。此时CUP硬件会发送8号信号,表示浮点数异常Floating point exception。我们先来看看相关现象,代码如下:(我们特意让进程一直被运行,并且8号信号自定义捕捉。进程收到8号信号时不退出

#include <iostream> #include <sys/types.h> #include <unistd.h> #include <signal.h>  void handler(int signo) {     std::cout << "自定义捕捉信号: " << signo << std::endl;     sleep(1); }  int main() {     std::cout << "pid: " << getpid() << std::endl;     signal(8, handler);      int x = 10;     x /= 0;     while(true)     {}     return 0; } 

【运行结果】:

请添加图片描述
 我们观察到进程确实收到了8号信号。

2 浮点数溢出,产生信号原理

 在CPU中存在许多寄存器,其中存在一个名为status的状态寄存器,其中存在一个标志位用来保存最近一次运算结果是否发送溢出!!一旦发送浮点数溢出,该标志位会被置为1,此时结果无意义!

 对于上述代码,假设我们CPU寄存器eax中保存10,ebx寄存器中保存0。10/0,本质上是除一个无限小的数,导致结果无限大,发生溢出。此时操作系统会识别到该信息,然后立即将当前进程从CPU上剥离,添加到某种异常处理队列。

 此时操作系统会将该异常解释为kill(targetprocess, signo)。然后将相关信号保存到进程PCB的信号位图中中!当异常处理完后,会被CPU再次调度运行,执行后续代码!但此时我们自定义捕捉了8号信号,没有让进程退出,会一直循环上述过程。
在这里插入图片描述

3. 空指针解引用错误,MMU产生信号原理

MMU(内存管理单元),它是一种负责处理中央处理器(CPU)的内存访问请求的计算机硬件,现如今一般别集成到CPU上。 它的功能包括虚拟地址到物理地址的转换(即虚拟内存管理)、内存保护等!

 对空指针进行非法解引用,即试图对0号地址进行写入。但此时页表中没有建立相关映射,此时MMU进程虚拟地址向物理地址转化时发送失败,MMU报错,相关标志位改变。该变化会被OS识别后向目标进程写信号!!

4)软件异常产生信号

 对于管道,比如匿名管道等存在同步机制的管道。当读端关闭,此时管道写端也会关闭退出。这就是一种典型的软件异常。当管道写端关闭,写端进行写入时会触发 SIGPIPE14信号。进而关闭读端退出!

 下面我i们以alarm函数为例,测试软件异常。
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。

//函数原型   #include <unistd.h>   unsigned int alarm(unsigned int seconds); //返回值是0或者是以前设定的闹钟时间还余下的秒数 

 下面我们设置一个3秒的闹钟,程序运行后向进程发送14号信号!

#include <iostream> #include <sys/types.h> #include <unistd.h> #include <signal.h>  int cnt = 0;  void handler(int signo) { 	//下面注释代码:我们可以主动发送14号信号,历史闹钟剩余时间,并取消历史闹钟     //int n = alarm(0);//取消历史闹钟,如果存在返回剩余时间     // int n = alarm(3); //从设闹钟,返回上一个闹钟剩余时间     //std::cout << "result:" << n << std::endl;          std::cout << "自定义捕捉信号: " << signo << "alarm" << std::endl;     exit(0); }  int main() {     std::cout << "pid: " << getpid() << std::endl;     signal(14, handler);     alarm(30);      while (true)     {         sleep(1);     }     return 0; } 

【运行结果】:
请添加图片描述

三、进程如何接收普通信号

 在Linux中,普通信号存在31个(1 ~ 31)而在计算机中,一个进程可能同时收到多个信号,所以我们需要对收到的信号进行管理!所以我们可以通过维护一张位图来表示收到的信号。即比特位的位置表示信号种类;比特位的内容表示是否收到内容!!并且在每一个进程中都会存在一张函数指针数组表,该表的下标和信号编号强相关!

struct task_struct { 	uint_32 sigmap;//信号位图 	//... }; 

 所以当进程收到信号后,操作系统会将该信号写入进程PCB所维护的信号位图中,即修改信号位图特定比特位!此时进程就完成了对信号的接收!

四、操作系统如何得知硬件数据就绪

4.1 OS得知硬件数据就绪方案

 操作系统需要知道硬件数据是否就绪无非就两种方式:操作系统定期主动区轮询所有硬件参看硬件数据是否就绪、硬件通过某种方式告知OS自身数据就绪。

 但显然第一种方式是不可取的。原因在于定期轮询硬件,间隔实践太长会导致运行速度下降;间隔时间过短,操作系统频繁访问硬件,会导致OS对进程调度效率下降。所以在计算机中,硬件数据是否就绪一般是通过某种方式去通知OS。

4.2 硬件如何通知OS数据就绪

 首先,在CPU的周围存在很多针脚。这些针脚会通过电路和一些电路板相连(比如8259),而这些板子又会和一些硬件相连,诸如键盘、磁盘、网卡等。即某些硬件会间接和CPU周围的针脚相连,并且这些针脚是存在编号的!

 当硬件数据就绪时,硬件会产生发送光电强弱信号传递给CPU针脚。当CPU识别到特定针脚的光电信号时,CPU会将光电信号转换为特定针脚对应的编号,并将该数字保存到寄存器中,而这些数字也被称为中断号,可以被程序读取。当CPU或OS识别到该寄存器中存在数据时,OS会立马停下手中的所有工作,将对应硬件中的数据加载到CPU中,完成数据的加载!!为了提供加载硬件数据的效率,OS会维护一张函数指针数组,即向量终端表。该数组的下标为中断号,表中的内容为中断号对应硬件的读取方法!

 所以当硬件数据就绪后,硬件发送光电信号,CPU将该硬件所连接的CPU针脚编号写到寄存器中。此时OS读取到寄存器中的中断号后,立即停止所有工作,通过中断向量表索引硬件的读取方法,将数据读取到CPU中!
在这里插入图片描述

五、操作系统是如何被调度的

 在计算机中,用户的所有行为都是以进程的形式存在的。所以OS只要把所有的进程调度好,就能完成所有的用户工作。但操作系统本身是如何被调的呢?

 其实在计算机中存在一个名为CMOS的硬件。该硬件周期的、高频的向CPU发送时钟中断。而OS本身就是一个死循环程序,当CMOS发送时钟中断时,CPU会通过中断向量表去调度OS的代码。进而完成对OS的调度!!

  • 操作系统的执行是基于硬件中断的。在OS被启动之前,计算机要完成对各种中断陷阱的初始化工作!!

    广告一刻

    为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!