目录
引言
进程信号的概念
信号的产生
键盘产生信号
进程异常产生信号
通过系统调用产生信号
软件条件产生信号
三个重要概念
信号处理动作的种类
忽略
执行收到信号之后的默认处理动作
自定义处理动作
信号阻塞
信号,这是在生活中我们经常听到的名词,在宿舍听到舍友的手机上传来"TiMi~",这一定是舍友在即将王者峡谷中漫游的信号,在校运动会的100米决赛上听到“预备,1,2,啪”,这是开跑的信号,又比如马路上的红灯亮了,这是车辆行人要停下来的信号,马路上的绿灯亮了,这是允许通行的信号,生活中类似信号的场景有很多,计算机源于生活,所以在进程中毫无疑问也是有信号的,本期我们将开始进行进程信号的学习。
引言
在学习信号的概念之前,我们来思考一些问题。
Q1:比如上述的红绿灯例子,为什么我们看到红灯就知道要进制通行,为什么看到绿灯就知道允许通过?
这是因为我们之前学习过,被教育过,所以我们知道了看到红灯或者绿灯时应该去做什么。
Q2:进程收到信号时,知道自己要做什么事情吗?
进程一定知道,因为编写操作系统的程序员在编写进程的源代码时,就已经编写了进程收到信号之后,如何处理信号的源代码。
Q3:我们看到绿灯亮了时,可能由于有其它要紧的事情,所以可能不会立即通行,等到要紧的事情干完之后才会通行,对于进程而言,当进程收到信号时,进程可能也会有更要紧的任务要进行处理,所以进程收到信号之后可能也不会立即处理信号,而是等处理完优先级更高的任务之后再去处理信号,可是怎么保证处理完优先级更高的任务之后, 还能去处理信号呢?
进程收到信号之后,如果有着优先级更高的任务要去处理,会先将信号保存在自身的数据结构task_struct中,以便后续进行处理,具体保存在数据结构中的哪一块,我们下文会专门来讲。
进程信号的概念
信号:信号是一种通知的机制。
每个信号都有一个编号和一个宏名称。使用kill -l可以查看系统中的信号。
其中1号到31号新号为非实时新号,34号及以上为实时信号。我们只要学习1-31号这些非实时新号。本期我们只关注1-31号非实时信号的学习。
信号的产生
键盘产生信号
#include<stdio.h>int main()
{while(1){printf("hello yjd!\n");}return 0;
}
上述代码是一个死循环打印字符串的代码,运行之后,我们使用按下ctrl+c键,进程终止,现象如下。
进程为什么会终止呢?这是因为当我们按下ctrl+c键之后,操作系统向目标进程发送了2号信号即SIGINT,所以导致了进程终止。
进程异常产生信号
观察下列代码。
#include<stdio.h>
#include<unistd.h>int main()
{printf("进程异常,产生信号!\n");sleep(1);int a=10;int b=a/0;printf("a=%d,b=%d\n",a,b);return 0;
}
运行结果如下。
在出现了除零操作之后,操作系统向目标进程发送了SIGFPE(8号) 信号,导致进程退出。
观察下列代码。
#include<stdio.h>
#include<unistd.h>int main()
{printf("进程异常,产生信号!\n");sleep(1);int *p=NULL;*p=10;return 0;
}
运行结果如下。
在出现了指针越界访问时,操作系统发现了异常,向目标进程发送了STGSEGV(11号信号),导致进程退出。
怎样验证,当出现了除零错误时发送8号信号,出现了指针越界访问发送11号信号呢?这就不得不提到一个接口signal。
参数:第一个参数为信号编号,表示要捕捉哪个信号,第二个参数为捕捉到的信号的自定义动作函数的地址也即函数指针(一般实参直接传递函数名即可)。
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<stdlib.h>void handler(int signo)
{switch(signo){case 11:printf("signal:%d\n",signo);exit(1);}}int main()
{signal(11,handler);printf("进程异常,产生信号!\n");sleep(1);int *p=NULL;*p=10;return 0;
}
通过代码我们进行了11号信号的捕捉,所以说只要指针越界访问产生了11号信号,那么就会通过signal接口捕捉得到,并执行我们对应的语句,对应的语句为打印11号信号。
运行结果如下。
通过运行结果不难发现,确实产生了11号信号。
为什么出现除零错误和指针越界访问,操作系统会向目标进程发送异常呢?
图示如下。
当进程执行除零操作时,最终的运算会在硬件CPU上进行,当进程执行指针越界访问时会在硬件内存上运行,除零操作和指针越界访问都是在硬件层面上的异常,所以操作系统发现这一异常之后,就会去查找究竟是哪个进程导致了这些异常,找到之后就会向目标进程发送信号,终止该进程。
所以说,异常产生信号,其实就是进程执行代码时导致硬件层面发生了异常,操作系统发现这一异常之后,终止了目标进程。
在之前学习进程终止时我们学过了,父进程通过waitpid函数等待子进程,防止子进程成为僵尸进程。waitpid函数中有一个参数为status,status中存储了进程退出时的相关信息。status为一个32为的整数,我们只用到了低16位,低16位中的次低八位存储进程的退出码,低八位中的低七位存储进程接收到的信号信息,剩余一位我们当时称之为core dump标志位,当时我们没有仔细讲解,本期我们就要来仔细研究一下。代码如下。
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>int main()
{//子进程if(fork()==0){int a=0;int b=a/0;printf("b=%d\n",b);}//父进程printf("子进程退出,父进程等待子进程\n");int status=0;waitpid(-1,&status,0);printf("signal:%d\n",status&0x7F);printf("core dump:%d\n",(status>>7)&0x1);return 0;
}
运行结果如下。
我们知道了子进程退出时收到了8号信号,同时status的core dump标志位为0。
在Linux中,进程终止时,如果进程正常退出会有在status中会保存对应的退出码,但是如果进程异常退出, 那么此时退出码就没有任何意义,有意义的就是status中保存的信号以及core dump标志位,如果进程异常退出,core dump标志位会被设置为1,同时会生成一个对应的调试文件,供后续我们调试,发现进程异常退出的原因。
在服务器上,core dump是默认关闭的,我们需要开启。
ulimit -a查看所有的属性信息,ulimit -c 文件大小,在编译可执行程序之后生成对应的调试文件。重新编译,运行结果如下。
文件后缀为当前可执行程序的pid。
对生成的调试文件,我们可以进行调试。
最终终端会告诉我们哪一行出现了异常以及出现了什么异常。以上这种方式我们也称作事后调试。
通过系统调用产生信号
两个函数:
int kill(pid_t pid, int signo); //给指定的进程发送指定的信号
int raise(int signo); //给当前进程发送指定的新号
代码如下。
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>void handler(int signo)
{switch(signo){case 2:printf("当前进程收到2号信号\n");break;case 8:printf("当前进程收到8号信号\n");break;}}int main()
{int pid=getpid();signal(2,handler);signal(8,handler);kill(pid,2);raise(8);return 0;
}
当前进程向自身发送2号和8号新号并进行捕捉。
软件条件产生信号
在学习匿名管道时,我们当时学习过匿名管道通信的四种类型,就是读端关闭了读端的文件描述符并且退出,此时操作系统就会向写端发送13号SIGPIPE信号,导致写端也退出,这就是一种典型的软件信号。
还有alarm接口产生的软件信号。代码如下。
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>int main(){int count=1;alarm(1);while(1){count++;printf("count:%d\n",count);}return 0;
}
运行结果如下。
alarm中的参数为几秒钟后向目标进程发送SIGALARM信号,该信号的默认处理动作为终止进程。
以上便是四种产生信号的方式,虽然有四种方式,但是本质上都是操作系统向目标进程发送信号。
三个重要概念
1.递达:执行信号的处理动作称为信号递达。
2.未决:信号从产生到递达之间的状态称为未决。
3.阻塞:进程处于未决状态时,一旦被阻塞是不能被递达的。所以阻塞即为阻止信号递达。
信号处理动作的种类
忽略
进程收到信号之后,可能有优先级更高的任务要去进行处理,所以会忽略信号,将信号存储在进程的task_structz中,在linux中,一般会存储在一个32位的位图结构中,位图的每一位的编号表示一种信号,当前位的为0表示未收到当前信号,为1则为收到当前信号。
执行收到信号之后的默认处理动作
进程收到信号的默认处理动作一般为终止当前进程。比如上文提到的2号,8号,11号,13号信号,当进程收到这些信号之后,默认的处理动作就是终止进程,这些默认处理动作是编写linux操作系统的程序员在编写信号及进程的代码时就已经编写好的。
自定义处理动作
这就是上文提到的signal接口,这个接口用于捕捉对应的信号,将当前信号的默认处理动作转为我们自己定义的处理动作。但是有个例外,9号信号是不能被捕捉的。
代码如下。
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>void handler(int signo)
{switch(signo){case 9:printf("捕捉到了9号信号\n");break;}
}int main(){signal(9,handler);while(1){sleep(1);printf("hahahahahahahah\n");}return 0;
}
运行结果如下。
我们捕捉9号信号,想让进程执行我们自定义的动作,但是我们发现当我们发送9号信号之后,执行的仍然是9号信号的默认处理动作,杀死进程,所以说,9号新号是不可以被捕捉的。
信号阻塞
上文我们已经提到了阻塞其实就是阻止信号被递达,那么底层逻辑究竟是怎样的呢?
我们说过发送信号其实就是把信号数据写入进程的task_struct里,我们也说过了是把信号数据写入位图中,现在我们给出最准确的概念,其实在进程的task_struct中有三张表,分别对应,block表,pending表,handler表。前两个表为位图结构,handler表为函数指针数组。我们之前讲述的保存信号的位图其实就是pending表。图示如下。
block表中,值为1,表示阻塞当前信号,为0表示不阻塞,pending表中,值为1表示收到该信号,为0表示未收到该信号,handler表中,值为SIG_DFL表示执行信号的默认处理动作,SIG_IGN表示忽略当前信号,为函数地址时,为我们自定义的函数,里面去执行我们自定义的动作。只要阻塞表中的值为1就表示当前信号被阻塞,不能被递达,也就不能执行信号的处理动作。这便是阻塞的原理。
以上便是本期信号的所有内容。
本期内容到此结束^_^