目录
信号的概念
从生活中的例子中感知信号
前台进程和后台进程
前台进程
后台进程
操作系统如何知道用户向键盘写入数据了?
进程如何得知自己收到了信号?
信号捕捉
signal函数
Core Dump(核心转储)
信号产生的方式
通过键盘按键产生信号
kill函数
raise函数
硬件异常产生信号
浮点数异常
非法地址异常
由软件条件产生信号
alarm函数
信号的概念
从生活中的例子中感知信号
- 你在网上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递。也就是你能“识别快递”
- 当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需5min之后才能去取快递。那么在在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成“在合适的时候去取”。
- 在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间,你并没有拿到快递,但是你知道有一个快递已经来了。本质上是你“记住了有一个快递要去取”
- 当你时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种:1. 执行默认动作(幸福的打开快递,使用商品)2. 执行自定义动作(快递是零食,你要送给你你的女朋友)3. 忽略快递(快递拿上来之后,扔掉床头,继续开一把游戏)
- 快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话
通过上面的例子我们可以总结出来一下四点:
- 信号没有产生的时候,其实我们已经直到怎么处理这个信号了。
- 信号的到来我们并不清楚具体什么时候,信号的到来对于我正在做的工作是异步产生的。
- 信号产生了,我们不一定要立即处理它,而是我们在合适的时候处理。
- 我们有一种能力,可以将已经到来的信号进行暂时保存。
让我们转换视角,将我们看成操作系统中被操作系统管理的进程;在进程运行期间根本不知道操作系统什么时候会给这个进程传递一种信息,当操作系统的信息来临时进程可以选择直接处理这个信息,也可以将信息暂时保存下来。
到此我们可以给信号下个定义:信号是一种向目标进程发送通知消息的一种机制。
在了解信号的产生之前我们还需要一点预备知识让我们对操作系统的信号机制了解的更全面一点。
前台进程和后台进程
前台进程
当我们循环执行一条输出语句时,执行我们的Linux指令操作系统是没有任何相应的;也就是操作系统没有能力接受用户的输入。
后台进程
只需要在运行可执行程序时指令的后面添加一个按位与的位运算符即可成为后台进程。后台进程有能力可以接受用户的输入,当我么输入指令后shell会执行我们的命令。
查看后台进程:
jobs
将后台进程调整为前台进程:
fg number(后台每个进程的任务号)
将前台进程调整为后台进程
ctrl + z
//先对前台进程进行暂停
bg number
//然后在进行调整
总结:
- 前台进程有且只有一个,一般情况下为shell。
- 操作系统会自动将shell提到前台或者后台。
- 前台进程可以被ctrl+c终止掉。
- 前台进程不可以被暂停,如果被暂停该前台进程必须被放到后台。
操作系统如何知道用户向键盘写入数据了?
操作系统是管理者,键盘、显示器、网卡等这些硬件也是要被操作系统管理起来的。当我们使用键盘进行写入操作时,键盘文件会发生变化;操作系统会将这些输入的数据进行各种处理。但是根据冯诺依曼体系结构外设是不可以和CPU进行打交道的,可以通过其他硬件间接联系。CPU中含有很多针脚,外设通过8259主板和CPU上的针脚关联。每个针脚对应一个外设,当我们键盘进行写入时,高低电平会通过主板让针脚发生“变化”。这样CPU就可以知道键盘进行写入了然后读取数据,对数据进行处理。这样的方式称作中断,引起中断的原因称作中断源。对于CPU上的针脚,操作系统也是需要进行管理的,使用一个函数指针数组;数组中的指针指向该针脚对应外设的读写操作函数。这个函数指针数组被称为断向量表。
因此信号的本质就是使用软件来模拟中断的行为。
进程如何得知自己收到了信号?
前面的文章中我们简单提到过信号系统中总共含有62中信号;其中 1-31为普通信号,34-64为实时信号,且没有0号信号。这篇文章我们只探讨普通信号,不对实时信号做任何解释和介绍。
可以使用kill -l查看系统信号
每个信号都含有一个数字编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定 义 #define SIGINT 2。
前面的文章我们介绍了每个进程都会被操作系统管理起来,形成一个结构体(PCB)其中含有进程的属性和信息;在这个结构体中就含有一个信号位图,每个比特位代表对应的信号每个比特位对应的内容表示是否收到了信号。当进程收到信号之后,会修改这个信号位图表示收到了某中信号。
信号捕捉
上面提到每个信号都有编号和宏定义,当进程收到某种信号时会调用系统默认的方法;Linux操作系统支持我们使用函数将默认方法改写。
signal函数
//头文件
#include<signal.h>
//函数名称
signal(signo,(void)(*)(int))
- 第一个参数表示我们要对哪个信号进行捕捉。
- 第二个参数是一个参数为整形的函数指针,指向我们重写的函数;这个函数的参数也为我们的第一个参数。
注意:对于9号进程来说是不可以被重写函数的。
Core Dump(核心转储)
首先解释什么是Core Dump。当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部 保存到磁盘上,文件名通常是core,这叫做Core Dump。进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)。一个进程允许产生多大的core文件取决于进程的Resource Limit(这个信息保存 在PCB中)。默认是不允许产生core文件的,因为core文件中可能包含用户密码等敏感信息,不安全。在开发调试阶段可以用ulimit命令改变这个限制,允许产生core文件。 首先用ulimit命令改变Shell进程的Resource Limit,允许core文件最大为1024K: $ ulimit -c 1024。
#include<iostream>
using namespace std;
int main()
{
int a=10;
a/=0;
return 0;
}
使用指令修改限制,允许产生core文件;专门制造一个浮点数异常编译并运行后会产生一个和本进程id相关的core文件;我们可以发现这个文件是非常大的,程序运行时如果出现这类异常会将我们的磁盘写满,造成无法进行修改这也是为什么系统默认不允许产生core文件。
信号产生的方式
通过键盘按键产生信号
通过键盘进行信号的产生就像我们上面使用键盘输入crtl +c可以终止一个进程,当这个键盘输入组合键时会产生一个硬件中断,被操作系统获取,解释成为2号信号,发送给目标前台进程。进程会修改自己PCB中的信号位图,表示已经收到信号。
前台进程因为接收到引号,直接对信号进行处理,进而终止进程。
此外常用的组合键含有:ctrl + z 默认暂停进程 ctrl + \ 默认退出进程
通过系统调用/指令产生信号
kill函数
//头文件
#include<sys/types.h>
#include<signal.h>
//函数
int kill(pid_t pid, int sig)
参数说明:
- pid:表示进程的pid,代表向哪一个进程发送。
- sig:表示发送信号的编号。
#include<iostream>
2 #include<unistd.h>
3 #include<signal.h>
4 #include<sys/types.h>
5 using namespace std;
10 int main()
11 {
12 cout<<"wait 2 signal"<<endl;
13 sleep(2);
14 kill(getpid(),9);
15 cout<<"WTF"<<endl;
return 0;
}
上面的代码演示了使用kill函数给自身进程产生了9号信号,并由操作系统发送成功;也可以通过另一个进程给一个指定的进程发送指定的信号。
raise函数
//头文件
#include<signal.h>
//函数
int raise(int sig);
参数说明:
- sig:代表信号编号。
- raise专门用来给自己发送指定的信号的。
#include<iostream>
2 #include<unistd.h>
3 #include<signal.h>
4 #include<sys/types.h>
5 using namespace std;
10 int main()
11 {
12 cout<<"wait signal"<<endl;
13 raise(9);
reutrn 0;
}
硬件异常产生信号
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,,MMU会产生异常,内核将这个异常解释SIGSEGV信号发送给进程。
浮点数异常
#include<iostream>
using namespace std;
int main()
{
int a=10;
a/=0;
return 0;
}
非法地址异常
#include<iostream>
using namespace std;
int main()
{
int* a=NULL;
*a=100;
return 0;
}
由软件条件产生信号
在之前管道的文章,匿名管道如果读端关闭,写端一直写入;操作系统就会发送SIGPIPE(13)号信号终止目标进程。SIGPIPE就是一种由软件条件产生的信号。
alarm函数
//头文件
#include <unistd.h>
//函数
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。
这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。打个比方,某人要小睡一觉,设定闹钟为30分钟之后响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就是10分钟。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数。
#include<iostream>
#include<unistd.h>
int cnt = 0;
int main()
{
alarm(1);
while(true)
{
cnt++;
cout<<cnt<<endl;
}
return 0;
}
今天对Linux下信号的产生的分享到这就结束了,希望大家读完后有很大的收获,也可以在评论区点评文章中的内容和分享自己的看法;个人主页还有很多精彩的内容。您三连的支持就是我前进的动力,感谢大家的支持!!!