【Linux】信号1——理解信号,信号的产生

一、理解信号

1、初步理解信号

        你在网上买了很多件商品,在等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递,也就是你能 “识别快递”。

        当快递员到了你楼下,你也收到快递到来的通知,但是此时你正在打游戏,需 5min 之后才能去取快递。那么在在这 5min 之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成 “在合适的时候去取”。

       在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间内,你并没有拿到快递,但是你知道有一个快递已经来了。本质上是你 “记住了有一个快递要去取”。

当你时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种:

  • 1、执行默认动作(幸福的打开快递,使用商品);
  • 2、 执行自定义动作(快递是零食,你要送给你你的女朋友);
  • 3. 忽略快递(快递拿上来之后,扔掉床头,继续开一把游戏)。

快递到来的整个过程对你来讲是异步的,你不能准确断定快递员什么时候给你打电话。 

生活中有很多信号,比如信号弹,上下课铃声,红绿灯,快递发短信取件码,狼烟,军训哨声,闹钟,外卖的电话……等等,这里就有下面两个问题:

  • 1.为什么我们能认识上面这些信号呢?

因为曾经有人教过我们这些信号是什么,然后我们记住的。

什么是认识信号?

  • 识别信号
  • 知道信号的处理方法

也就意味着,我们记住了常见的信号

2.假如现在闹钟没有响,我们是否知道闹钟响了之后该干什么?

当然知道,因为曾经有人教过我们,教我们的是:信号是什么,为什么,怎么办。

这里只想说明进程能够认识信号,以及信号不管到没到来进程都知道该怎么做。  

3.闹钟一响,你是不是立马起床了?

不一定吧 ,信号产生了,我们可能不立即处理这个信号,因为可能我们在做更重要的事情。

所以信号产生到被处理的时候是有时间窗口期的!!在这个时间段,我们必须记住信号的到来

 上面的你,我们指的是进程

  1. 进程必须识别并处理信号,并且信号没有产生时,也要具备处理信号的能力
  2. 进程即便是没有收到信号,也能知道哪些信号该怎么处理
  3. 当进程真的收到一个具体的信号的时候,进程可能并不会立即处理这个信号,在合适的时候处理
  4. 一个进程必须当信号产生,到信号开始被处理,就一定会有一定的时间窗口,这要求进程临时保存哪些信号已经发生的能力

1、进程收到信号就会立即处理吗?

进程收到某种信号的时候,并不是立即处理的。比如远处看到红绿灯变成红灯,我们会立即停下吗?并不会,我们会把看到红灯这件事记录在大脑中,等走到路口再停下

进程当前可能在执行优先级更高的东西,所以要选择合适的时候再处理这个信号。

2、没有被立即处理的信号放在哪?

我们看到红灯的时候,会把看到红灯这件事存在大脑中。

既然信号不能被立即处理,已经到来的信号会被暂时保存起来,以供在合适的时候处理,应该保存在哪里呢??——》进程控制块 task_struct

3、谁负责把信号存到指定位置?

信号的本质就是数据,发送信号 ——》向进程控制块 task_struct写入数据 ——》但是进程控制块属于内核,内核不相信任何人,所以由谁来写入数据 ——》 OS!!

2,见见信号

先复习一下makefile

通过一段代码理解信号

#include <iostream>
#include <unistd.h>
using namespace std;

int main()
{
	while (1){
		cout<<"hello signal!"<<endl;
		sleep(1);
	}
	return 0;
}

我们知道该程序的运行结果就是死循环地进行打印,而对于死循环来说,最好的方式就是使用Ctrl+C对其进行终止。

为什么使用Ctrl+C后,该进程就终止了? 

        实际上当用户按Ctrl+C时,这个键盘输入会产生一个硬中断,被操作系统获取并解释成信号(Ctrl+C被解释成2号信号),然后操作系统将2号信号发送给目标前台进程,当前台进程收到2号信号后就会退出。

注意:

ctrl+c为什么能杀掉我们的前台进程?

  • Ctrl+C产生的信号只能发送给前台进程。在一个命令后面加个&就可以将其放到后台运行,这样Shell就不必等待进程结束就可以接收新的命令,启动新的进程。

运行的时候我们一直输入ls,发现没有什么反应!!且可以被ctrl+c杀掉,这就是前台进程

我们输入指令快一点,就能有反应,这个就是后台进程,这个是不能被ctrl+c杀掉的

它会一直运行,杀掉这些后台进程的方法如下

  • Shell可以同时运行一个前台进程和任意多个后台进程,但是只有前台进程才能接到像Ctrl+C这种控制键产生的信号。
  • 因为linux,一次登录中,一个终端一般会配上1个bash,每一个登录,只允许1个进程是前台进程,可以允许多个进程是后台进程
  • 我们的bash进程是1个前台进程,当我们使用./test的时候,bash进程变成后台进程,所以我们执行ls是没有用的
  • 当我们以./test &的方式执行,bash还是前台进程,ls就有用
  • 所以,前台进程和后台进程的区别就是谁拿到键盘输入的内容,谁就是前台进程
  • 所以我们按ctrl+c只有前台进程收得到!!!!
  • 我们为什么不能按ctrl+c来终止我们的bash进程?因为bash进程对ctrl+c的处理是做了特殊处理的

注意:signal函数是一个注册的意思,程序执行到它的代码语句的时候不干什么,只是把对应信号的默认处理动作改成自定义动作,这个在这个进程是一直有效的

        前台进程在运行过程中,用户随时可能按下Ctrl+C而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都可能收到SIGINT信号而终止,所以信号相对于进程的控制流程来说是异步的。

  1. 同步(synchronized):同步是指一个进程在执行某个请求的时候,如果该请求需要一段时间才能返回信息,那么这个进程会一直等待下去,直到收到返回信息才继续执行下去。
  2. 异步(Asynchronous):异步是指进程不需要一直等待下去,而是继续执行下面的操作,不管其他进程的状态,当有信息返回的时候会通知进程进行处理,这样就可以提高执行的效率了,即异步是我们发出的一个请求,该请求会在后台自动发出并获取数据,然后对数据进行处理,在此过程中,我们可以继续做其他操作,不管它怎么发出请求,不关心它怎么处理数据。

 3、查看系统定义的信号列表 

我们前面也简单的接触过信号,kill -l 就可以查看信号

我们发现没有0号信号

仔细观察可以发现这里不是 64 种信号,因为中间并不是连续的,一种有 62 种信号(其中,没有0号信号,没有 32 和 33 信号)。  其中,1~31 叫做普通信号(这是我们学习的范围),而 34~64 叫做实时信号(这个我们不讲),每个实时信号中都包含了 RT 两个字母。 实时信号是一种响应特别强的信号,比如着火,而普通信号则对应我们每天早上的闹钟。

  • 我们也很快知道,信号就是一个数字

  这么多信号,其对应功能是什么呢?

  • 可以通过 man 7 signal 进行查询

往下翻就能看到

  • 每个信号都有一个编号和一个宏定义名称,这些宏定义可以在 signal.h 中找到,例如其中有定义 #define SIGINT 2

我们回到ctrl+c那个例子,

我们先来了解一个系统调用函数:signal函数是用来修改特定信号的处理动作的!!!

        我们可以使用signal函数对2号信号进行捕捉,证明当我们按Ctrl+C时进程确实是收到了2号信号。

第一个参数,是信号的序号,上面已经列出了信号以及对应的序号,既可以用序号,也可以使用信号名

第二个参数,是接收到信号以后处理信号的函数指针

返回值:调用成功,则返回上一次信号处理函数被调用的返回值;调用失败,返回SIG_ERR

              因为存在无法找到对应的函数的情况,所以就无法搭建起信号和信号处理函数之间的联系

signal(2,signalhandler);    
//建立起2号信号和signalhandler函数之间的关系
//等实际收到2号信号时,就会执行signalhandler函数

        例如,下面的代码中将2号信号进行了捕捉,当该进程运行起来后,若该进程收到了2号信号就会打印出收到信号的信号编号。

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void handler(int sig)
{
	printf("get a signal:%d\n", sig);
}

int main()
{
	signal(2, handler); //注册2号信号
	while (1){
		printf("hello signal!\n");
		sleep(1);
	}
	return 0;
}

此时当该进程收到2号信号后,就会执行我们给出的handler方法,而不会像之前一样直接退出了,因为此时我们已经将2号信号的处理方式由默认改为了自定义了。

由此也证明了,当我们按Ctrl+C时进程确实是收到了2号信号。

我们按ctrl+c也不能终止掉这个进程,这个就是我们把这个进程对2号信号的默认处理方式已经修改成我们传入的自定义方法handler方法

我们可以让它退出来

#include <stdio.h>
#include <signal.h>
#include<cstdlib>
#include <unistd.h>

void handler(int sig)
{
	printf("get a signal:%d\n", sig);
	exit(1);
}

int main()
{
	signal(2, handler); //只要设置一次,往后都有效
	while (1){
		printf("hello signal!\n");
		sleep(1);
	}
	return 0;
}

注意:下面这两种写法是等价的

signal(2, handler);
signal(SIGINT, handler);

 因为2号信号就是SIGINT,我们可以查看kill -l

  • 此外我们一旦使用signal函数,那么它在全局都是有效的 
  • 有些信号可以使用signal函数捕捉,但是有些信号不能被signal捕捉的,9(杀)和19号(暂停)

4.键盘数据是怎么输入给内核的?ctrl+c又是如何变成信号的?

键盘被按下肯定是OS先知道!

键盘是一个文件,键盘文件有内核的fie_struct,它也有自己的缓冲区,键盘输入的数据先放到这个文件缓冲区,操作系统把键盘的外设的数据拷贝到文件缓冲区,再通过read将它的数据读取到进程上面!!

  • OS怎么知道键盘上有数据了?

cpu有很多对应的针脚,键盘和cpu直接相连,一旦键盘有数据了,键盘可以发送一个硬件中断,让cpu来读取我的数据!!外设多了,每个外设都能发送一个中断,那么操作系统怎么知道是哪个外设发的呢?操作系统引入了中断号的概念,硬性规定1号中端号就是键盘发来的!!那么操作系统就知道键盘上有数据了

  • 我们都知道计算机只认识0和1,那么寄存器凭什么保存数据?

我们发送中断的过程,就是发送高低电平的过程,寄存器就能记录下来了

        操作系统开机的时候就操作系统会生成并维护一张中断向量表,这个中断向量表存的是方法的地址,主要是直接访问外设的方法(主要是磁盘,显示器,键盘)

         中断号到了cpu,操作系统就立马以中断号为索引去中断向量表立马找对应的方法,这个方法就能帮操作系统将键盘的数据拷贝到文件缓冲区

        换言之,我们在键盘上写好数据,输入按回车之后,键盘就会通过cpu和键盘相连的针脚给cpu发中断,这个中断通过充放电的方式被cpu寄存器记录下来,中断让cpu停下来,操作系统根据发来的中断得知是哪个外设,就立马以中断号为索引去中断向量表立马找对应的方法,这个方法就能帮操作系统将键盘的数据拷贝到文件缓冲区

我们刚刚学的信号和这个是没有任何关系的,但是它们的功能特别类似的,我们的信号就是通过软件方式来对进程模拟的硬件中断

键盘上面的按键不止是输入的,还有控制的按键,如果我们键盘上输入的是ctrl+c的呢?

  • 操作系统肯定会判断我们输入的是数据还是控制!!!如果是ctrl+c,操作系统就会把它转换成2号信号发送给信号 

来总结一下,键盘是基于硬件中断的方式来工作的!!!!

  • 显示器是怎么显示我们的输入的东西的?

首先Linux下一切皆文件,显示器和键盘也是,每个打开的文件在操作系统中都会有1个struct file,并且每个文件配有1个文件缓冲区,很显然显示器和键盘也有,当我们从键盘获取数据到键盘的文件缓冲区后(这个我们上面讲过),显示器要显示键盘上输入的数据,就得把键盘的文件缓冲区的数据拷到自己的文件缓冲区

 这个也就能回答一个问题了

我们输入l,不按下回车,我们往屏幕写的数据就会写进我们的输入缓冲区里面(注意不是键盘的文件缓冲区啊),然后我们的屏幕立马读取并回显,但是我们的键盘的输入缓冲区的数据(l)还在,我们接着写,不按回车,那么键盘的输入缓冲区的数据就会累计,按了回车就把数据交给内核处理 

 二,进程信号的产生

2.1.通过终端按键产生信号

系统卡死遇到过吧?程序死循环遇到过吧?这些都是比较常见的问题,当发生这些问题时,我们可以通过 键盘键入 ctrl + c 发出 2 号信号终止前台进程的运行

当面对下面的死循环程序时,我们可以按Ctrl+C可以终止该进程。

但实际上除了按Ctrl+C之外,按Ctrl+\也可以终止该进程。

按Ctrl+C终止进程和按Ctrl+\终止进程,有什么区别?

  1. 按Ctrl+C实际上是向进程发送2号信号SIGINT
  2. 而按Ctrl+\实际上是向进程发送3号信号SIGQUIT。

查看这两个信号的默认处理动作,可以看到这两个信号的Action是不一样的,

  • 2号信号是Term
  • 3号信号是Core

Term和Core都代表着终止进程,但是Core在终止进程的时候会进行一个动作,那就是核心转储。 

按Ctrl+C终止进程和按Ctrl+\终止进程,有什么区别?

  1. 按Ctrl+C实际上是向进程发送2号信号SIGINT
  2. 而按Ctrl+\实际上是向进程发送3号信号SIGQUIT。

查看这两个信号的默认处理动作,可以看到这两个信号的Action是不一样的,

  • 2号信号是Term
  • 3号信号是Core

Term和Core都代表着终止进程,但是Core在终止进程的时候会进行一个动作,那就是核心转储。 

什么是核心转储?

在云服务器中,核心转储是默认被关掉的,我们可以通过使用ulimit -a命令查看当前资源限制的设定。

②查看结果显示core文件的大小为0,即表示核心转储是被关闭的。

 通过ulimit -c size命令来设置core文件的大小。

core文件的大小设置完毕后,就相当于将核心转储功能打开了。此时如果我们再使用Ctrl+\对进程进行终止,就会发现终止进程后会显示core dumped。

 并且会在当前路径下生成一个core文件,该文件以一串数字为后缀,而这一串数字实际上就是发生这一次核心转储的进程的PID。

说明一下: ulimit命令改变的是Shell进程的Resource Limit,但myproc进程的PCB是由Shell进程复制而来的,所以也具有和Shell进程相同的Resource Limit值。

核心转储功能有什么用?

        当我们的代码出错了,我们最关心的是我们的代码是什么原因出错的。如果我们的代码运行结束了,那么我们可以通过退出码来判断代码出错的原因,而如果一个代码是在运行过程中出错的,那么我们也要有办法判断代码是什么原因出错的。

        当我们的程序在运行过程中崩溃了,我们一般会通过调试来进行逐步查找程序崩溃的原因。而在某些特殊情况下,我们会用到核心转储。

        核心转储指的是操作系统在进程收到某些信号而终止运行时,将该进程地址空间的内容以及有关进程状态的其他信息转而存储到一个磁盘文件当中,这个磁盘文件也叫做核心转储文件,一般命名为core.pid。

        而核心转储的目的就是为了在调试时,方便问题的定位。

如何运用核心转储进行调试?

我们用下面这段代码进行演示:

很明显,该代码当中出现了除0错误,该程序运行3秒后便会崩溃。

此时我们便可以在当前目录下看到核心转储时生成的core文件。

使用gdb对当前可执行程序进行调试,然后直接使用core-file core文件命令加载core文件,即可判断出该程序在终止时收到了8号信号,并且定位到了产生该错误的具体代码。 

说明一下: 事后用调试器检查core文件以查清错误原因,这种调试方式叫做事后调试。

core dump标志

 还记得进程等待函数waitpid函数的第二个参数吗:

pid_t waitpid(pid_t pid, int *status, int options);

 waitpid函数的第二个参数status是一个输出型参数,用于获取子进程的退出状态。

        status是一个整型变量,但status不能简单的当作整型来看待,status的不同比特位所代表的信息不同,具体细节如下(只关注status低16位比特位):

  1. 若进程是正常终止的,那么status的次低8位就表示进程的退出状态,即退出码。
  2.  若进程是被信号所杀,那么status的低7位表示终止信号,而第8位比特位是core dump标志,即进程终止时是否进行了核心转储。

        打开Linux的核心转储功能,并编写下列代码。

        代码中父进程使用fork函数创建了一个子进程,子进程所执行的代码当中存在野指针问题,当子进程执行到*p = 100时,必然会被操作系统所终止并在终止时进行核心转储。

        此时父进程使用waitpid函数便可获取到子进程退出时的状态,根据status的第7个比特位便可得知子进程在被终止时是否进行了核心转储。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>

int main()
{
	if (fork() == 0){
		//child
		printf("I am running...\n");
		int *p = NULL;
		*p = 100;
		exit(0);
	}
	//father
	int status = 0;
	waitpid(-1, &status, 0);
	printf("exitCode:%d, coreDump:%d, signal:%d\n",
		(status >> 8) & 0xff, (status >> 7) & 1, status & 0x7f);
	return 0;
}

 可以看到,所获取的status的第7个比特位为1,即可说明子进程在被终止时进行了核心转储。

因此,core dump标志实际上就是用于表示程序崩溃的时候是否进行了核心转储。 

云服务器为什么默认关闭core dump ,本地虚拟机环境是打开的?

        core dump是给编译器看的(二进制文件),只要程序core dump就要在磁盘中形成临时文件。一旦服务挂掉,搞运维的最重要的动作不是找到挂掉的原因,而是先让服务器跑起来,让服务正常运行,再找原因。服务挂掉了一定是大量的重启操作,大公司有自己的自动化重启程序,让它自动重启。

        如果代码是自己写的,经常跑起来就挂,就会产生大量的临时文件,如果把部署目录塞满了。写不进去相关的日志信息,甚至是临时文件把系统盘打满了,系统无法正常运行。一般云服务器允许你异常,崩溃,但不允许core dump.

        不是所有的信号都需要core dump

其他组合按键?

我们可以通过以下代码,将1~31号信号全部进行捕捉,将收到信号后的默认处理动作改为打印收到信号的编号。

#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void handler(int signal)
{
	printf("get a signal:%d\n", signal);
}
int main()
{
	int signo;
	for (signo = 1; signo <= 31; signo++){
		signal(signo, handler);
	}
	while (1){
		sleep(1);
	}
	return 0;
}

此时,当我们按下组合按键Ctrl+C、Ctrl+\、Ctrl+Z后,便可以得知这些组合按键分别是向前台进程发送几号信号了。

但如果我们此时向该进程发送9号信号,该进程并不会打印收到了9号信号,而是执行收到9号信号后的默认处理动作,即被终止。

说明: 有些信号是不能被捕捉的,比如9号信号。因为如果所有信号都能被捕捉的话,那么进程就可以将所有信号全部进行捕捉并将动作设置为忽略,此时该进程将无法被杀死,即便是操作系统。

2.2.通过kill命令向进程发信号

当我们要使用kill命令向一个进程发送信号时,我们可以以kill -信号名 进程ID的形式进行发送。

 也可以以kill -信号编号 进程ID的形式进行发送。

2.3.通过系统调用函数 

kill函数

实际上kill命令是通过调用kill函数实现的,kill函数可以给指定的进程发送指定的信号,kill函数的函数原型如下:


kill函数用于向进程ID为pid的进程发送sig号信号,如果信号发送成功,则返回0,否则返回-1。

我们可以用kill函数模拟实现一个kill命令,实现逻辑如下:

#include <iostream>
#include <cstdlib>
#include <sys/types.h>
#include <csignal>
using namespace std;

void Usage(string proc)
{
	cout<<"Usage: "<<proc<<" signum pid"<<endl;
}

//使用方法应该是mykill pid signum
int main(int argc, char* argv[])
{
	if (argc != 3){//必须按照上面的3个输入的方法来
		Usage(argv[0]);//一定是mykill
		exit(1);//用法不对,别执行了
	}
	
	pid_t pid = atoi(argv[1]);//字符串转换为int
	int signum = atoi(argv[2]);//字符串转换为int

	int n=kill(pid, signum);
	if(n==-1)
	{
		perror("mykill");
		exit(2);
	}
	return 0;
}

我们把这个可执行程序的名字取为mykill 

此时我们便模拟实现了一个kill命令,该命令的使用方式为./mykill 进程ID 信号编号

raise函数

raise函数可以给当前进程发送指定信号,即自己给自己发送信号,raise函数的函数原型如下:

raise函数用于给当前进程发送sig号信号,如果信号发送成功,则返回0,否则返回一个非零值。

例如,下列代码当中用raise函数每隔一秒向自己发送一个2号信号。

#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void handler(int signo)
{
	printf("get a signal:%d\n", signo);
}
int main()
{
	signal(2, handler);//自定义对2号信号的处理方法
	while (1){
		sleep(1);
		raise(2);//给自己发信号
	}
	return 0;
}

运行结果就是该进程每隔一秒收到一个2号信号。

abort函数

raise函数可以给当前进程发送SIGABRT信号(6号),使得当前进程异常终止,abort函数的函数原型如下:

void abort(void);

abort函数是一个无参数无返回值的函数。

例如,下列代码当中每隔一秒向当前进程发送一个SIGABRT信号。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

void handler(int signo)
{
	printf("get a signal:%d\n", signo);
}
int main()
{
	signal(6, handler);
	while (1){
		sleep(1);
		abort();
	}
	return 0;
}

与之前不同的是,虽然我们对SIGABRT信号进行了捕捉,并且在收到SIGABRT信号后执行了我们给出的自定义方法,但是当前进程依然是异常终止了。

说明一下: abort函数的作用是异常终止进程,exit函数的作用是正常终止进程,而abort本质是通过向当前进程发送SIGABRT信号而终止进程的,因此使用exit函数终止进程可能会失败,但使用abort函数终止进程总是成功的。

2.3.由软件条件产生信号

SIGPIPE信号                

        SIGPIPE信号实际上就是一种由软件条件产生的信号,当进程在使用管道进行通信时,读端进程将读端关闭,而写端进程还在一直向管道写入数据,那么此时写端进程就会收到SIGPIPE信号进而被操作系统终止。

        例如,下面代码当中,创建匿名管道进行父子进程之间的通信,其中父进程是读端进程,子进程是写端进程,但是一开始通信父进程就将读端关闭了,那么此时子进程在向管道写入数据时就会收到SIGPIPE信号,进而被终止。

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
	int fd[2] = { 0 };
	if (pipe(fd) < 0){ //使用pipe创建匿名管道
		perror("pipe");
		return 1;
	}
	pid_t id = fork(); //使用fork创建子进程
	if (id == 0){
		//child
		close(fd[0]); //子进程关闭读端
		//子进程向管道写入数据
		const char* msg = "hello father, I am child...";
		int count = 10;
		while (count--){
			write(fd[1], msg, strlen(msg));
			sleep(1);
		}
		close(fd[1]); //子进程写入完毕,关闭文件
		exit(0);
	}
	//father
	close(fd[1]); //父进程关闭写端
	close(fd[0]); //父进程直接关闭读端(导致子进程被操作系统杀掉)
	
    int status = 0;
	waitpid(id, &status, 0);
	printf("child get signal:%d\n", status & 0x7F); //打印子进程收到的信号
	return 0;
}

 运行代码后,即可发现子进程在退出时收到的是13号信号,即SIGPIPE信号。

SIGALRM信号

        调用alarm函数可以设定一个闹钟,也就是告诉操作系统在若干时间后发送SIGALRM信号(14号)给当前进程,alarm函数的函数原型如下:

        alarm函数的作用就是,让操作系统在seconds秒之后给当前进程发送SIGALRM信号,SIGALRM信号的默认处理动作是终止进程。

alarm函数的返回值:

  1. 若调用alarm函数前,进程已经设置了闹钟,则返回上一个闹钟时间的剩余时间,并且本次闹钟的设置会覆盖上一次闹钟的设置。
  2. 如果调用alarm函数前,进程没有设置闹钟,则返回值为0。

例如,我们可以用下面的代码,来看看

#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;

int main()
{
	int count = 0;
	alarm(3);//设一个3秒钟的闹钟,3秒之后闹钟响
	while (1){
		count++;
		cout<<"count: "<<count<<endl;
        sleep(1);
	}
	return 0;
}

 3秒之后闹钟响了直接退出了

我们来捕捉一下这个信号

#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;
void handler(int sig)
{
	printf("get a signal:%d\n", sig);
}

int main()
{
	signal(14, handler); 
	
	int count = 0;
	alarm(3);//设一个3秒钟的闹钟,3秒之后闹钟响
	while (1){
		count++;
		cout<<"count: "<<count<<endl;
        sleep(1);
	}
	return 0;
}

我们发现为什么这个闹钟就发送了1次14号信号呢?

因为它不是异常!!!

那么如果我想让闹钟每3秒响1次呢?

#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;
void handler(int sig)
{
	printf("get a signal:%d\n", sig);
	alarm(3);
}

int main()
{
	signal(14, handler); 
	
	int count = 0;
	int n=alarm(3);//设一个3秒钟的闹钟,3秒之后闹钟响
	while (1){
		count++;
		cout<<"count: "<<count<<endl;
        sleep(1);
	}
	return 0;
}

那么我们设置一个定时任务也不是什么难事了

#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;
void handler(int sig)
{
	//定时任务存放的地方
	alarm(3);
}

int main()
{
	signal(14, handler); 
	
	int count = 0;
	alarm(3);//设一个3秒钟的闹钟,3秒之后闹钟响
	while (1){
		count++;
		cout<<"count: "<<count<<endl;
        sleep(1);
	}
	return 0;
}

注意这个alarm的返回值

#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;
void handler(int sig)
{
	printf("get a signal:%d\n", sig);
	int n=alarm(3);
	cout<<"剩余时间:"<<n<<endl;
}

int main()
{
	signal(14, handler); 
	
	int n=alarm(30);
	while (1){
		cout<<"I am running…… , pid:"<<getpid()<<endl;
        sleep(1);
	}
	return 0;
}

每个进程都可以定闹钟,所以操作系统存在大量闹钟,操作系统要管理闹钟,所以先描述再组织 

2.4.由异常产生信号

我们之前在进程控制讲过异常,进程退出的情况有3种

  1. 此时代码运行完毕,结果正确 (这个时候main函数的返回值为0则为正确,0:success)
  2. 此时代码运行完毕,结果不正确(这个时候main函数的返回值非0则为不正确,返回值:报错信息)
  3. 此时代码异常终止,意思就是代码没跑完(这个时候main函数的返回值不具有意义,此时应该去看退出信号)

这第3种就是异常产生信号,本质上是因为进程在运行过程中收到了操作系统发来的信号进而被终止

我们看个例子

#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;

int main()
{
	cout<<"div before"<<endl;
	sleep(1);
	int a=10;
	a/=0;//除0错误
	sleep(1);

	cout<<"div after"<<endl;
}

这就是出异常了!!!

Fpe是这3个单词的开头,我们就可以用这个去查

8号信号干什么用的?

很明显了,

我们可以来通过程序来确认

#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;

void hander(int signo)
{
	cout<<"get a sigo :"<<signo<<endl;
}

int main()
{
	signal(8,hander);

	cout<<"div before"<<endl;
	sleep(5);
	int a=10;
	a/=0;//除0错误
	sleep(1);

	cout<<"div after"<<endl;
}

5秒后全是

这个时候进程不退出啊!!!

这里一直打印,信号为什么一直被捕捉????

我们一会再说

#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;


int main()
{

	cout<<"point error before"<<endl;
	sleep(5);
	
	int *p=nullptr;
	*p=100;//野指针问题

	sleep(1);

	cout<<"point error after"<<endl;
}

这个是段错误,这个是异常

Seg是这第一个单词的前3个字母

我们来捕捉一下这个

#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;

void hander(int signo)
{
	cout<<"get a sigo :"<<signo<<endl;
}

int main()
{
	signal(11,hander);

	cout<<"point error before"<<endl;
	sleep(5);
	
	int *p=nullptr;
	*p=100;//野指针问题

	sleep(1);

	cout<<"point error after"<<endl;
}

5秒之后,也是一直打印 

所以进程收到信号不一定会退出!!!! 

当我们程序当中出现类似于除0、野指针、越界之类的错误时,为什么程序会崩溃?

换句话说,为什么类似于除0、野指针、越界之类的错误会给进程发信号呢?

        事实上进程在运行过程中收到了操作系统发来的信号进而被终止,不是这些错误发的

那操作系统是如何识别到一个进程触发了除0错误的呢?

cpu会从上往下执行我们的代码,那么cpu怎么知道我们执行到哪一行代码呢?

  • 通过寄存器 

        我们知道,CPU当中有一堆的寄存器,当我们需要对两个数进行算术运算时,我们是先将这两个操作数分别放到两个寄存器当中,然后进行算术运算并把结果写回寄存器当中。

        此外,CPU当中还有一组寄存器叫做状态寄存器(通过不同比特位来记录)它可以用来标记当前指令执行结果的各种状态信息,如有无进位、有无溢出等等。

        首先我们得明白,在程序运行过程中,一个进程独占1个cpu,cpu的寄存器会有特定的部分用来给这个进程记录状态(不会影响到其他的进程),当进程退出的时候,cpu会把这些状态的数据全还给进程的task_struct,然后下一个进程上来的时候,进程里的数据又会放到cpu的各种寄存器上面,接着运行。保持了进程的独立性。

        而操作系统是软硬件资源的管理者,在程序运行过程中,若操作系统发现CPU内的某个进程的某个状态标志位被置位(0->1),而这次置位就是因为出现了某种除0错误而导致的,那么此时操作系统就会马上识别到当前是哪个进程导致的该错误,并将所识别到的硬件错误包装成信号发送给目标进程,本质就是操作系统去直接找到这个进程的task_struct,并向该进程的位图中写入8信号,写入8号信号后这个进程就会在合适的时候被终止。

那对于野指针问题,或者越界访问的问题时,操作系统又是如何识别到的呢?

首先我们必须知道的是,当我们要访问一个变量时,一定要先经过页表的映射,将虚拟地址转换成物理地址,然后才能进行相应的访问操作。

         其中页表属于一种软件映射关系,而实际上在从虚拟地址到物理地址映射的时候还有一个硬件叫做MMU,它是一种负责处理CPU的内存访问请求的计算机硬件,因此映射工作不是由CPU做的,而是由MMU做的,但现在MMU已经集成到CPU当中了。

        当需要进行虚拟地址到物理地址的映射时,我们先将页表的左侧的虚拟地址导给MMU,然后MMU会计算出对应的物理地址,我们再通过这个物理地址进行相应的访问。

        而MMU既然是硬件单元,那么它当然也有相应的状态信息,当我们要访问不属于我们的虚拟地址时,MMU在进行虚拟地址到物理地址的转换时就会出现错误,然后将对应的错误写入到自己的状态信息当中,这时硬件上面的信息也会立马被操作系统识别到,进而将对应进程发送SIGSEGV信号。

        野指针错误就是虚拟地址转换成物理地址失败!

总结一下:
        C/C++程序会崩溃,是因为程序当中出现的各种错误最终一定会在硬件层面上有所表现,进而会被操作系统识别到,然后操作系统就会发送相应的信号将当前的进程终止。

现在我们就可以回答最初的那个问题了

为什么一直打印这个?

我们触发了除0错误,cpu寄存器已经记录下来了,你又不让退出,cpu就一直检测到异常,所以一直检测信号,除非我们把cpu的状态寄存器的除0错误的那个位置从1换成0

异常是不能修复的,一般都是直接退出的,它的设计是让你死明白的,不是让你去修复异常的!!

  • 33
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值