目录
一、引入信号:
1、什么是信号:
二、前后台进程:
三、信号的处理方式:
四、键盘数据与信号:
前言:
在Linux系统编程中,信号(Signal)是一种至关重要的进程间通信机制,它允许操作系统或进程以异步方式通知目标进程某种事件的发生,例如终止进程、处理异常或触发自定义逻辑,无论是开发高并发服务器,还是调试程序中的异常行为,理解信号的原理与应用都是不可或缺的技能
对于信号的学习,我们分成四个步骤
一、引入信号:
1、什么是信号:
在我们的生活中就有许多的信号,比如当我们在网购的时候,当快递到了菜鸟驿站的时候就会给我们发一个信息,这就算是一个信号的产生,如果此时我们正在打游戏,没有时间去处理它,但是会将这个信号记住(哦,我打完这把游戏就要去拿快递)这就是对信号的保存,然后等到我真的去拿快递的时候,这就是对信号的处理
所以一个信号每时每刻都会产生,并且对于信号要做的动作并不需要立即执行,所以就需要有对信号进行保存的机制,等到合适的时候就对这个信号进行处理
在生活中有着很多很多的信号,比如说:古代的狼烟,以前的冲锋号,下课的铃声等等
那我们是怎么认识这些信号的呢?----- 当然是在生活中,被教会的,比如当看到红绿灯就需要停下来等等,当听到下课铃声的时候就知道可以休息了
那么我们回到我们的Linux中,进程也会收到很多信号,当进程收到信号的时候,和我们日常生活一样, 并不需要立刻进行处理,而是等到一个合适的时机在进行处理,那么对于这些信号,进程怎么知道要怎么处理呢?------- 这当然是创建OS的大佬程序员们在创建的时候也创建了一些信号,这样当进程收到对应的信号的时候就会进行对应的动作
所以,对于进程来说:
1、进程必须具备 识别并且能够处理信号的能力 ---- 并且如果信号没有产生也就是进程没有收到信号,也要具备能够处理,识别信号的能力(这也对应着我们的生活中,尽管此时我旁边没有红绿灯,但是我仍然知道当见到红绿灯的时候要怎样做)------ 所以信号的处理能力是属于进程内置功能的一部分
2、当进程收到一个信号的时候,进程可以不立即处理这个信号,等到合适的时候在处理
3、进程在信号产生,到开始被处理的时候,在这段时间窗口就一定会具备保存信号的能力
讲了这么多,我们先来看看在OS中有哪些信号:
如上,可以看到有62种信号,其中没有32 33号信号(具体是有历史原因的,好像是与什么什么冲突了),在信号的学习中,我们只学习前31个信号,这些信号也被叫做普通信号,对于34以后的实时信号我们不展开学习
二、前后台进程:
我们插入地了解下什么是前台进程,什么是后台进程?
在Linux系统中,前台进程和后台进程是进程管理的核心概念,两者的运行方式和交互特性存在显著差异
什么是前台进程:
定义:用户通过终端直接启动并与之交互的进程
当我们在启动进程的时候一般都是启动的前台进程,并且前台进程是能够被ctrl+c热键杀掉的,并且我们发现在输入指令的时候是没有反应的
什么是后台进程:
定义:独立于终端运行,无需用户实时干预的进程
当我们在启动进程的时候再最后加上&符号,这样我们这个进程是后台进程了
如下,我们发现当启动后台进程的时候,我们输入指令的时候居然有反应了,是互不影响的,并且用热键ctrl+c是杀不掉我们的进程的,那么怎么杀死呢?-----用9号信号kill -9 对应的进程pid
对于一个终端,我们是只允许有一个前台进程的,这是因为我们只能有一个进程和用户进行交互,如果有多个进程就会乱了,当我们启动xshell的时候就启动了一个bash这样的前台进程,当我们又运行了一个前台进程的时候,bash进程就变为后台进程了,这也就是为什么启动前台进程的时候,输入指令没有反应,因为我们输入的指令是输入给前台进程的,我们自己执行的前台进程是没有处理这些指令的代码的,所以输入指令也就没有用,当我们输入热键ctrl+c的时候,这本质上是给我们当前进程发送了2号信号来使我们的进程退出的,
那么这个时候就又有问题了,为什么我对bash这个进程进行ctrl+c的时候bash进程没有退出呢?----- 这当然是bash进程内部对这些信号进行特殊处理啦
但是可以有很多个后台进程,这个时候我们输入的指令依然是输给前台进程bash的,这也就是为什么我们启动后台进程之后输入指令依然能够显示对应的动作了,所以ctrl+c也就啥杀不死后台进程,这是因为我输入的指令发送的是前台进程bash,后台进程此时是收不到了----- 那么后台进程就无敌了吗? ----- 这显然也是不可能的,我们此时就需要使用信号,
比如kill -9 待杀死后台进程的pid即可
三、信号的处理方式:
当我们接受一个信号的时候,就会有三种处理方式
比如当我们在等红绿灯的时候:
1、默认状态,红灯停,绿灯行
2、忽略,不管红灯还是绿灯都停或者都行
3、自定义动作,比如当红灯亮的时候就唱歌,当绿灯亮的时候就跳舞
回到我们的进程中,首先学习一个接口:
void(*)(int) 表示一个函数指针,该函数接受一个int参数并返回void,其中sighandler_t就是新类型名,代表上述函数指针类型
如上,这就是对一个信号进行重定义,首先捕捉到对应的信号,再让信号进行我们重定义的动作
对于signal函数,就需要传两个参数
第一个是 要捕捉信号的序号或者对应的宏也可以
第二个参数是 指定信号处理方式,可为自定义函数、系统默认,或忽略
在了解这个函数了,我们就可以来看看ctrl+c这个热键了,这个实际上就是给我们当前进程发送2号信号,我们来验证看看:
#include <iostream>
#include <unistd.h>
#include <signal.h>using namespace std;void myhander(int signo)
{cout<<"process get a signal : "<< signo << endl;// exit(1);
}int main()
{signal(SIGINT,myhander); while(1){cout<<"i am a process "<< getpid() <<endl;sleep(1);}return 0;
}
其中signal第一个参数其实就是数字定义的宏,可以看看源码:
如上,这是一个进程,我们让他以前台进程的方式运行起来
如上,当运行起来后,我们使用常规的ctrl+c热键是杀不死这个进程的,这是因为ctrl+c热键本质上是向该进程发送2号信号,但是我们在代码中将2号信号的默认行为自定义为在显示器上打印一条消息了,这样ctrl+c就无法给该进程传输2号信号的默认动作了,就只能够通过别的信号,如上图右侧,通过9号信号来杀死该进程了
那么是不是我们将所有信号都重定义了,就无法杀死该进程了呢?
我们编写如下代码来看看结果:
#include <iostream>
#include <unistd.h>
#include <signal.h>using namespace std;void myhander(int signo)
{cout<<"a signal was change : "<< signo << endl;
}int main()
{for(int i = 1;i<=31;i++){signal(i,myhander);}while(1){cout<<"i am a process "<< getpid() <<endl;sleep(1);}return 0;
}
如上,我们将所有信号都进行捕捉,在进行验证
如上,我们先说结论:
9号信号和19号信号是不能够被捕捉的,因为这两个信号是强制用于终止/暂停进程
如果要验证可以像验证上述信号1,2,3一样
signal更深理解:
我们在使用signal函数的时候,一般放在最开始即可,因为signal只需要设置一次,在往后的生命周期中都有效(当捕捉到对应信号后就会重定义为自己新的行为),对于signal第二个参数,这只有在捕捉到信号的时候再回执行上述的函数,如果在以后的生命周期都未捕捉到对应的信号,也就不会调用上述myhander这个函数
四、键盘数据与信号:
键盘的数据是怎么输入给CPU进行处理的?ctrl+c又是怎么变成信号的?
首先,我们在冯诺依曼体系结构中了解到了:键盘是不能够直接将数据给CPU的,必须通过内存,然后在和CPU打交道,所以当键盘里面有数据了,必须是通过OS来和CPU打交道的
那么OS要怎么才能知道键盘中有数据了呢?难道没过一段时间OS都向每一个硬件都问它-----你有没有数据啊?这显然是不可能的,毕竟在我们的计算机中硬件数量很多,并且OS是不会做浪费时间的事情的,那么就需要一个解决方式:
这就需要我们的CPU了,看看上图,CPU与内存之间是不能够直接传输数据的,但是在CPU中有许多针脚,不同的硬件对应着不同的针脚,每一个针脚都有自己的编号,其功能由CPU内部的多个功能模块协同控制
当我们在键盘中按下后,控制信号通过中断单元来告诉CPU:我这键盘有数据了,你快来处理,然后CPU就会获取对应的针脚编号,然后写入寄存器中
在OS中有一个中断向量表这个概念:
中断向量表是一个函数指针,毕竟在Linux下一切皆文件,在这个表中,其成员函数指针就指向访问各种外设的方法
所以,当CPU中的一个对应的寄存器中获取到了对应的针脚编号,就会在OS中的中断向量表中对应的位置找到对应硬件的方法,执行它的读取方法就行了
这样,OS就知道要从键盘中读入数据了,此时,如果读到了ctrl+c之类的组合键,OS就会将这些转化成对应的信号发送给进程;如果读到了普通数据,就会写入对应进程的缓冲区中