1. 用户输入命令,在Shell下启动一个前台进程。 . 用户按下Ctrl-C ,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程 . 前台进程因为收到信号,进而引起进程退出
//signal.cc
#include
using namespace std;
int main()
{
while(1){
cout<<"I am a process! I am waiting signal"<
信号是进程之间事件异步通知的一种方式,属于软中断。
我们学习信号是学习它的整个生命周期,按照时间轴,分为信号产生,信号保存,信号处理。但是在这之前先需要学习一些预备知识。
进程能够识别的信号是已经写好的,它有62个:
kill -l
这其中没有32号和33号信号,所以一共有62个信号。而且这里我们只学习普通信号,对实时信号暂不做研究。
在使用这些信号时,可以用信号名,也可以用信号编号,它是一样的,都是宏定义后的结果。
根据我们对Linux的了解,信号存放在哪里呢?既然信号是给进程的,而进程是通过内核数据结构来管理的,所以我们可以推断出,信号放在进程的task_struct结构体中。
既然它是在PCB中,而且数量是31个,task_struct中必定不会设置31个变量来存放信号,数组还有可能,但是信号的状态只分为有和没有两种,所以再次推断,31个信号放在一个32位的整形变量中,每个比特位代表一个信号。
就像在学习基础IO和进程间通信的时候,那些flags标志中的不同的比特位代表着不同的意义,这31个信号量也是这种方式:
具体的保存细节后面本喵再详细讲解。
问题来了,内核数据结构的修改,这个工作是由谁来完成的?毫无疑问是操作系统,因为task_struct就是它维护的,而且是存在于内存中的,只有操作系统才有权力去修改它,用户是无法直接操作的,因为操作系统不相信任何人。
所以说,无论哪个信号,最后的本质都是由操作系统发生给进程的,这里的发送本质就是在修改task_struct中存放信号那个变量的比特位。
所谓的注册,就是告诉操作系统,当某个进程接收到某个信号后的处理方式。
既然是告诉操作系统,那么肯定会用到系统调用,该系统调用的名字是signal():
int signal:要注册的信号编号
sighandler_t handler:自定义的函数指针
可以将信号的处理方式写成一个函数,然后将函数名传递个signal,此时当进程接收到signum指定的信号编号时,就会执行我们定义的函数。
void handler(int signo)
{
cout<<"进程接收到的一个信号,编号:"<
#include
#include
#include
using namespace std;
void handler(int signo)
{
cout<<"进程接收到的一个信号,编号:"<
运行起来后发现,按上ctrl+c后,进程不会结束了。
所有信号的默认处理方式都是结束进程,只是不同的信号代表的意义不一样。
SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump,现在我们来验证一 下。
#include
#include
#include
using namespace std;
int signo_arr[2]={2,3};
void handler(int signo)
{
cout<<"进程接收到的一个信号,编号:"<
可以看到通过键盘产生了2号和3号信号。
系统调用kill():
- pid_t pid:要给发信号的pid
- int sig:要发送的信号编号
- 返回值:发送成功返回0,失败返回-1
该系统调用是一个进程给另一个进程发送指定信号,可以向任意进程发送任意信号。
信号接收端:
//mytest.cc
#include
#include
#include
#include
using namespace std;
int main()
{
while(1){
cout<<"我是一个正在运行的进程pid:"<
信号发送端:
//mysignal.cc
#include
#include
#include
#include
using namespace std;
int signo_arr[2]={2,3};
void handler(int signo)
{
cout<<"进程接收到的一个信号,编号:"<
这是一个带有命令行参数的程序,输入的选项中的pid值和信号编号,在程序中在调用kill系统调用向指定pid的进程发送指定信号。
运行结果:
左边在执行mysginal的时候,输入对应的信号编号和pid,右边正在运行的进程就会接收到指定的信号而停止运行。
系统调用raise():
int sig:要发送的信号
返回值:发送成功返回0,失败返回-1
该系统调用是由进程自己调用,也就是进程自己可以给自己发送任意信号。
通过命令行参数指定信号,在程序执行5秒钟后给自己发送该信号。
无论命令行输入哪个信号的编号,在5秒钟后,该进程都会给自己发送输入的信号,让进程结束。
系统调用abort():
没有参数,没有返回值
该系统调用只能给自己发送指定的信号,该信号是SIGABRT,信号编号是6。
运行结果:
在运行5秒钟后,该进程接收到了6号信号SIGABRT。
虽然有3个系统调用来产生信号,但是归根到底都是在使用kill系统调用。
//raise本质
kill(getpid(),signo);
abort()可以给自己发送SIGABRT信号。
//abort本质
kill(getpid(),SIGABRT);
除0操作导致的硬件异常:
在这段代码中,有除0操作,我们知道,除0得到的是无穷大的数,所以在编程的时候是不允许出现的。
这其实就是一种硬件异常产生的信号。
CPU会从内存中将代码中的变量拿到寄存器中进行运算,如果有必要,还会将运算的结果放回到内存中。
此时就意味着硬件产生了异常。而操作系统是一个进行软硬件资源管理的软件,CPU的中状态寄存器的溢出标志位置一后,操作系统可以第一时间拿到。
除0导致硬件异常以后,操作系统会给对应的进程发送SIGFPE信号。
当进程接收到SIGFPE信号以后,默认的处理方式就是结束进程。
现在我们对这个SIGFPE信号注册一个自定义处理方式:
只打印接收到的信号编号,进程不退出。
在进程运行起来后,怎么就开始鞭尸了呢?也就是这个信号被操作系统不停的发送给这个进程。
所以就会导致上面不停调用自定义处理函数,不停打印接收到的信号编号。
解引用空指针导致的硬件异常:
这同样是一种硬件异常产生的信号。
当进程接收到编号为11的SIGSEGV信号以后,默认的处理动作就是结束进程。
将这个信号注册自定义处理方式,同样打印接收到的信号编号,但是不结束进程,可以看到,和除0操作一样,也是在鞭尸,不停的打印。
- 硬件异常所产生的信号,如果不结束这个进程,我们是没有能力去处理这个进程的。
- 随着时间片的轮转,这个导致硬件异常的进程还会不停的调到,所以操作系统会不停的向进程发送信号。
读端关闭触发的信号:
比如在学习匿名管道的时候,当读端关闭的时候,写端所在进程就会收到编号为13的SIGPIPE信号结束进程。
#include
#include
#include
#include
#include
#include
using namespace std;
int signo_arr[2]={2,3};
void handler(int signo)
{
sleep(1);
cout<<"进程接收到的一个信号,编号:"<
运行结果:
在读端关闭以后,写端的自定义处理方式中就接收到了系统发给的SIGPIPE信号,编号为13。
闹钟触发的信号:
闹钟就是系统中的定时器,使用的时候同样需要通过系统调用实现:
- 参数:要定的时长。
- 返回值:距离定的时间还差多少。
当定时1秒钟时间到了以后,自定义处理方式中打印出接收到的信号编号是14号的SIGALRM信号,并且统计出了1秒钟进行加1操作的次数。
如果想每隔一秒条件达成一次,产生一次SIGALRM信号,可以在这样处理:
运行结果:
软件中某个条件达成以后,操作系统就会产生相应的信号,比如上面的SIGPIPE信号和SIGALRM信号。
闹钟的管理:
操作系统中会有很多个进程,我们可以创建一个闹钟,那么其他进程也可以创建闹钟,这样就会存在很多个闹钟,那么这些闹钟是怎么管理的呢?先描述再组织。
首先需要创建一个闹钟的结构体,伪代码:
struct alarm
{
unit64_t when;//定时时长
int type;//闹钟类型,一次性还是周期性
task_struct* p;//所属进程的地址
struct_alarm* next;//下一个闹钟的地址
//其他属性
}
大概就是这样的一个结构体来描述闹钟,必须由的肯定是定时时长,所属进程。
接下来就是组织了,用某一种数据结构来管理这些闹钟对象,为了方便管理,可以选择优先级队列prority_queuq来管理。
操作系统会周期性的检测链表中的这些闹钟,伪代码:
curr_timestamp > alarm.when;//超时了
//OS发送SIGALRM信号到alarm.p;
具体的实现细节有兴趣的小伙伴可以看看源码是怎么管理的,这里本喵只是介绍一种思想。
是否有一个疑问,31个信号的默认处理方式都是结束进程,并且还可以自定义处理方式,那么为什么要这么多信号呢?一个信号不就行了吗?
在man的7号手册中介绍了信号的名称,对应的编号,默认处理方式,以及产生该信号的原因。
以信号2和3为例,他两的默认处理方式一个是Term,一个是Core。
那么这两个方式的区别在哪里呢?
- Term方式仅仅是结束进程,结束了以后就什么都不干了。
- 但是Core不仅结束进程,而且还会保存一些信息。
在数据越界非常严重的时候,该进程会接收到SIGSEGV信号,来结束进程。
11号信号的默认处理方式是Core。
在云服务器上,默认情况下是看不到Core退出的现象的,这是因为云服务器关闭了core file选项:
- core file size(红色框)的大小是0,意味着这个选项是关闭的。
- 从这里还可以看到别的关于这个云服务器的信息,比如能够打开的最多文件个数,管道个数,以及栈的大小等等信息。
为了能够看到Core方式的明显现象,我们需要将core file选项打开:
此时该选项就打开了,表示的意思就是核心转储文件的大小是1024个数据块。
core.1739:被叫做核心转储文件,其中后缀1739是接收到该信号进程的pid值。
对于一个奔溃的程序,我们最关心的是它为什么崩溃,在哪里崩溃?
核心转储的文件我们可以拿着它进行调试,快速定位到出现异常而崩溃的位置。
这就是核心转储的重要意义,它相比Term方式,能够让我们快速定位出现异常的位置。
感谢阅读!!!!!!!!!!