初始信号
初始信号
什么是信号
Linux系统提供的让用户(进程)给其他进程发送异步信息的一种方式
- 在没有发生的时候,我们已经知道发生的时候,怎么处理了
- 我们可以识别各种类型的信号,结合第一条和第二条,我们能识别一个信号并处理(打个比方说,当我们过马路时看到红灯时,我们眼睛看到了红灯信号,然后我们的处理方式通常就是站在马路边上等待)
- 信号到来的时候,我们正在处理更重要的事情,我们暂时不能处理到来的信号,我们必须要将到来的信号进行临时保存
- 信号到了,可以不立即处理,自己在合适的时候处理
- 信号的产生是随时产生的,我们无法准确预料,所以信号是异步发送的(信号的产生,是由别人(用户、进程)产生的,我收到之前,我一直在忙我的事情,并发在运行的)
将以上的“我”换成进程,就是进程看待信号的方式
站在应用角度的信号
- 用户输入指令,在shell下启动一个前台进程
- 用户按下ctrl+c,键盘输入产生硬件中断,被OS获取,解释成信号,发送给目标前台进程
- 前台进程因为收到信号,进而引起进程退出
注意:
1、ctrl+c
产生的信号只能发送给前台进程,一个命令后面加上&可以将其放到后台执行,这样shell就不必等待该进程结束就可以接受新的命令,启动新的进程
2.、shell可以同时接受运行一个前台进程和任意多个后台进程,只有前台进程才能接到例如ctrl+c
这种控制链产生的信号
3、 前台进程在运行过程中用户随时可能按下ctrl+c而产生一个信号,也就是说该进程的用户代码空间执行到任何地方都可能因为收到信号而终止或者产生其他行为,所以信号相对于进程的控制流程来说是异步的
查看Linux系统定义的信号列表
- 每个信号都有一个编号和一个宏定义名称,比如2号信号就是
#define SIGINT 2
- 编号34以上的是实时信号,这篇博客主要是讨论编号34以下的信号,不讨论实时信号,这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细的说明:在命令行输入
man 7 signal
即可查看
信号的常见处理方式
常见的信号处理方式有以下三种
- 忽略此信号
- 执行该信号的默认处理动作
- 提供一个信号处理的函数,要求在内核处理信号的时候切换到用户态执行这个处理函数,这种方式称之为捕捉(catch)一个信号
这里要先介绍一个linux的系统调用signal
1、当我们要忽略一个信号时(这里用2号信号为例),就将第二个参数传递SIG_IGN即可
2、执行该信号的默认处理动作,2号信号的默认处理动作就是终止当前正在运行的前台进程,这里就不再演示了
3、让该信号执行自定义的动作
信号的产生
通过终端按键产生信号
SIGINT的默认处理动作是终止当前进程,SIGQUIT的默认处理动作是终止进程并Core Dump(之前演示时用到的ctrl+c就是向进程发送2号的方式)
进程收到了的大部分信号的动作都是进程自己终止,终止有两种方案,一种叫做core,一种叫做term,它们又什么区别?
什么是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
而term就是不产生任何文件的普通终止行为
如何开启core dump?
之前已经提到了core dump默认是关闭的,使用命令ulimit -c可以查看相关的信息
使用ulimit -c可以通过设置文件大小打开core dump功能
core dump有什么用?
当一个进程发生了除0异常,如果是term结束,在系统层面上直接不管了,直接把代码和数据还有内核数据结果直接释放掉,这个进程就没了
但是如果信号除0动作是core,则会将进程在内存中的核心数据(主要是与调试有关)转储到磁盘中形成core、core.pid的文件,这样可以定位到这个进程为什么会退出,以及是执行到哪一行代码退出了
core其实是一种机制,如果进程在执行过程中出异常了,就将进程在内核中的核心数据转储到磁盘当中以文件的方式保存,这种方式叫做core dump:核心转储
所以命令ulimit -c其实是在设置core文件的上限大小(默认情况下为0,代表即便是core退出,也不要形成核心转储文件)
说了这么多,core文件中既然包含了相关的错误信息,我们就可以根据文件中的错误信息进行调试
int main()
{
int a=10;
a/=0;
return 0;
}
比如这段代码,在代码中产生一个除0异常,然后运行程序
发现多了一个文件core,这个文件存的就是程序的错误原因
大部分信号当中,只要是退出信息(Action)是core,都是可以通过打开core dump功能,事后根据core文件进行定位问题和调试
云服务器默认是将进程以core形式退出,进行了特定的设定,默认core是被关闭的
为什么默认关闭core dump?
Linux系统是有服务关闭自动重启的功能的,如果服务一异常关闭,然后重启,一关闭又重启…就会留下很多的core文件,时间一长就会导致云服务器的磁盘被占满->防止未知的core dump一直在进行,导致磁盘服务器被打满
设置了core文件大小但是没有产生core文件的可能原因
我在第一次使用已经设置了core文件的大小但是出现异常时并没有出现core文件,我查了一下,有以下几种说法
- 程序设置了用户id(即调用setuid),但当前用户并非该程序文件的所有者
- 程序设置了组id(即调用setgid),但当前用户并非该程序文件的组所有者
- 用户没有当前目录或指定core文件产生目录的写权限
- core文件太大,磁盘空间不足
但是我检查了这几点发现这些都不是原因,后来我了解到,可能是core文件产生的位置有关,core的缺省位置是程序所在目录,可以通过修改/proc/sys/kernel/core_pattern
来指定core文件生成位置了名称。
然后我查看了core_pattern文件,发现文件内容是一段脚本程序,后来查看说明文件,才知道core_pattern中如果首先指定了一个 ‘|’ 管道符,则会将生成的core文件传递给后面所跟的脚本去处理。
至此,也就确定了问题的原因,| 管道符后面的脚本将我们的core文件给吞了,解决方法自然就是去掉这个脚本,换成自己指定的目录
但直接去修改core_pattern文件并没有成功,保存时会提示FSync错误,查阅资料得知,这个文件有特殊限制,只能通过命令:
sudo bash -c "echo 这里是写入内容 > /proc/sys/kernel/core_pattern "
我使用的是
sudo bash -c "echo core > /proc/sys/kernel/core_pattern "
来进行写入,即指定程序所在目录为core文件生成目录,core文件名称为"core"。
通过系统函数向进程发信号
kill函数是向任意进程发送信号,而raise是向自己发送信号。两个函数的返回都是成功返回0,错误返回-1
abort函数的作用是使当前进程接收到信号而异常重者,和kill(getpid(),6)
用法有点相似(6号信号是SIGABRT
)
由软件条件产生信号
SIGPIPE就是很典型的由软件条件产生的信号,这个信号是在进程间通信方式之一——管道中,读端已经关闭而写端仍然还在写入时,这个时候系统就会通过向写端发送SIGPIPE信号关闭写端
还有个信号SIGALRM信号
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。
这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。打个比方,某人要小睡一觉,设定闹钟为30分钟之后响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就是10分钟。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数
但是"闹钟"只能响一次,所以如果要让这个闹钟多次响,需要捕捉SIGARM信号,在处理完信号后,再设置一个闹钟
#include<iostream>
#include<signal.h>
int main()
{
int cnt=0;
alarm(1);
while(true)
{
std::cout<<cnt<<std::endl;
cnt++;
}
return 0;
}
这个代码的作用就是在1秒钟之内不停的数数,1秒种到了因接收到了SIGARM就停止
硬件异常产生信号
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。
一点简单的拓展,有关于键盘产生信号
键盘是一个硬件,作为一个键盘它需要知道以下几点
-
按键按下了
-
哪些按键按下了
-
字符输入(字符设备(键盘都是字符输入)),键盘还可以进行组合键输入(ctrl +c),相当于输入的是命令
不管你是键盘输入还是组合键输入,在键盘层面上,就是按键,但是到底输入的字符还是命令,这个是由操作系统决定的(键盘和OS联合解释),因为一切皆文件,所以键盘设备也可以看作是操作系统打开的一个文件,所以键盘获取到的数据就会放到键盘的输入缓冲区里,上层就通过0号文件描述符将其读出来
操作系统怎么知道键盘上面有数据输入?
要么是操作系统去定期的检测键盘是否有数据被按下,但是这样的话效率就太低了
其实操作系统时使用了一种硬件中断的技术,当操作系统刚开机的时候,已经形成了一张表,这张表上已经注册了很多对软硬件进行操作的方法
这个所谓的中断向量表就是函数指针数组。此时的CPU正在执行进程代码。这个中断向量表其实也是属于操作系统的数据
CPU只和内存打交道,并且CPU内部有很多的针脚(物理性的),这些针脚在主板上是可以和键盘进行连接的(每个针脚都有特定的编号),未来键盘在进行按键的时候会给CPU中特定的针脚触发硬件中断,CPU就知道哪个针脚上面有高电平了就可以识别这个针脚。
总之,CPU已经知道了有触发的针脚上面已经有高电平了,有因为每个针脚上面又有特定的编号,也就意味着当键盘被按下时会向CPU的2号针脚处发送高电平(也可以把这个2号称之为中断号)。即CPU能识别到 -
有高电平
-
是几号针脚
CPU内部还有寄存器,寄存器req就会将中断号存起来(到此,硬件到软件的动作就做完了)
然后,CPU识别到对应的寄存器中有数字,CPU就直接要求操作系统先暂停当前进程,然后让操作系统拿着中断号去中断向量表去查对应的方法,然后根据中断向量表中对应的方法去执行读取数据的任务。因为中断向量表中的方法都是操作向量表提供的,所以在调用里面的方法的时候,操作系统就能够知道数据到来了并将数据读入内存中
读到数据之后就要对数据做“判定”,看是字符还是控制命令,如果是字符,就将其放入到缓冲区中让上层读取;如果是控制命令,就将其解释成信号,让后再将这个信号发送给进程(解释信号就是到进程的pcb中将位图上对应的比特位由0置1)
之前提到,当信号在到来的时候,如果进程正在处理更重要的事情,就会暂时不处理信号,而是将信号进行临时保存,保存在哪里呢?保存在进程的pcb中。一个进程可能会收到多个信号,所以pcb内部就需要对收到的信号进行管理,因为信号的编号是1、2……31,所以pcb中采用的就是位图的方法保存信号
关于异常产生信号
emsp; CPU中有很多寄存器,有些寄存器是用来做计算的,也有些寄存器是用来CPU内部中的管理,其中就有一个寄存器叫做EFLAGS/RFLAGS,这个是标志寄存器,包含了状态和控制标志,其中有一个标志称为CF(这个标志称作为是溢出标志,Overfliow Flag),当CPU内部进行运算
int a=10;
a/=0;
CPU内部会将10放到一个寄存器中(假设为eax),然后将0放到一个寄存器中(假设为ebx),按照代码就是让eax中数初一abx中的数,然后将结果写回到eax中,但是在运算10除以0的时候,得到的是一个巨大的数字,据发生了溢出,然后溢出标志位设为1,也就是说计算出现了错误(计算错误,表现到了CPU的寄存器上也就是硬件上),此时操作系统识别到了操作系统内部有错误,CPU就不再调度进程了,CPU就告诉操作系统,操作系统发现确实出现了异常,就直接向对应进程的pcb中发送对应的溢出信号(修改对应的位图),至此,就完成了信号的发送