<>信号的概念

生活中遇到红绿灯,我们知道红灯停绿灯行;跑到比赛的枪响了我们知道要开始跑了,这些在我们生活中存在的各种信号在操作系统当中也会有类似的机制,接下来我将分成
三个大块来介绍进程间的信号。
(编号为1-31的为不可靠信号,34-64的为可靠信号。连续不间断发送信号,信号会丢失的为不可靠信号,连续不间断发送信号,信号不会丢失的为可靠信号。)

<>第一块:信号产生前

机器是人生产的,所以就会让机器实现和人类相同的处理问题的机制,比如我们为什么遇到了红灯就知道要停下来,这是因为我们一开始是通过接受教育获得知识才懂得了红绿灯的规则,(OS)操作系统同样,一开始会由程序员在为OS编写好了遇到各种信号的处理时机与处理方法,是所以在我们使用当中遇到各种问题操作系统会接收到信号并反馈给我们。
因此,进程是具有识别信号处理信号的能力,这是远远早于信号产生的。
但如果信号产生了,进程就必须立刻处理吗?比如说早上我们的闹钟响了,我们一定要立刻起床吗?不一定对吗,进程也会有将信号储存起来的功能,到合适的时机再进行处理。

收到的信号保存在进程的控制块(PCB)当中。信号的本质也是数据,信号的发送>>>>往进程的PCB中写入信号的数据

<>信号产生的方式

一共有四种,我们分别简单介绍下:
1.键盘产生
2.进程异常,产生信号
3.通过系统调用,产生信号
4.软件条件,产生信号
本质上所有的信号最后都是经过OS向目标进程发出来的

<>1.键盘产生

最常见的就是我们写一个死循环的函数,那么当我们想结束循环,我们可以键盘输入ctrl+c,强行结束该进程。
这里我们键盘的输入本质上就是就是我们向OS发送了要强行结束的信号,通过OS控制进程结束了循环。

<>2.进程异常,产生信号

简单来说,当我们写了一行错误的代码,我们运行后OS会提示我们哪一行有问题,这就是因为我们的进程出现了异常被OS识别到,然后OS发信号干掉我们的进程。

<>3.通过系统调用,产生信号

常见的就是我们的写的代码在执行过程中调用了比如kill的命令,kill功能简单来说就是向OS发送信号处理想要的进程。

<>4.软件条件,产生信号

include <unistd.h>
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。

<>补充:如何获取子进程退出的信号

#include <sys/wait.h> #include <stdio.h> #include <stdlib.h> #include<unistd.h>
#include <string.h> #include <errno.h> int main( void ) { pid_t pid; if ( (pid=
fork()) == -1 ) perror("fork"),exit(1); if ( pid == 0 ){ sleep(20); exit(10); }
else { int st; int ret = wait(&st); if ( ret > 0 && ( st & 0X7F ) == 0 ){ //
正常退出 printf("child exit code:%d\n", (st>>8)&0XFF); } else if( ret > 0 ) { //
异常退出 printf("sig code : %d\n", st&0X7F ); }

<>举例证明键盘产生信号

<>signal函数

signal函数的功能是设置对某一种信号的对应动作,signum为要处理的信号,
handler是自定义的函数指针,在该函数中我们可以设置想要对某种信号的处理方法。接下来我将利用这个函数来证明我们键盘是如何发信号的:


首先介绍下ctrl+c对应的就是执行2号信号(可看上面的表)运行后循环打印,我们键盘敲ctrl+c正常如果我们没有改写信号处理的话会强制循环的进程结束,但我们通过signal改写了对2号信号的处理方法改成了打印该信号,所以我们可以看到get
a signal :2。
ctrl+\ 为3号信号,同理。

结束该循环可以用如下的方法,使用kill -9 +该进程的pid,杀掉该进程。
注意:**9号信号是无法自定义的,9号信号只有默认的动作,**因为如果所有信号如果都能被自定义那OS就没法管理系统了

<>第二块:信号产生中

<>(重要)如何理解OS给进程发信号->OS发送信号数据给task_struct

先给出概念

下图介绍了OS是如何储存信号的

下图介绍了OS是如何发送信号的:
OS发送信号本质:修改目标进程的pending位图
任何信号被block后 并不影响收到信号(pending)只会影响递达信号,默认block为0
handler:函数指针数组,【31】,每个信号的编号就是数组下标

<>举例证明OS是如何发信号的

<>sigset_t(更改的是BLOCK位图)

从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。下一节将详细介绍信号集的各种操作。
阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。

sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_
t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的。

一句话总结:sigset是用来表示信号是否阻塞和是否未决的,它只能通过特定的函数修改。

函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号。
函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置为1,表示 该信号集的有效信号包括系统支持的所有信号。
注意:在使用sigset_ t类型的变量之前,一定要调
用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种
信号,若包含则返回1,不包含则返回0,出错返回-1。

<>sigprocmask(修改block位图结构)

调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则 更改进程的信
号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后
根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。

由上图可见创建两个sigset_t类型变量, 通过sigemptyset函数将两个变量都初始化,,使其中所有信号的对应bit清零,表示该信号集不包含
任何有效信号。通过sigaddset函数将iset中的2信号置为1,大概的意思为下图:

顺便验证下9号信号无法被屏蔽:

<>举例证明信号未决存在pending位图中

<>sigpending函数

提出假设:如果我们的进程与先屏蔽掉2号信号(上面刚做的举例)
不断的获取当前pending位图,并打印显示(00000000000000000000000000000000)
然后手动发送2号信号,因为2号信号不会被递达,所以当获取当前pending位图,打印显示应该为:
(010000000000000000000000)
现在开始验证

由上图可知:由于2号信号被屏蔽了,所以只能保存在pending中,无法被抵达,因此打印会一直有1!证明假设成立!
现在我们恢复2号信号

2号信号默认动作是终止进程,因此看不到由1变0,因此我们再利用signal函数自定义2号信号的处理

这时就能看到2号信号被恢复了

通过上述举例我们证明了OS发送信号是通过修改pending位图以及未决的信号会存在于位图中

<>第三块:信号发送后

信号的产生是异步的,当前进程可能正在做着更重要的事情,因此信号会在“合适”的时候处理,即信号延时处理(取决于OS和进程)
那么什么时候合适呢?
我们先给出结论然后解释:进程从内核态切换到用户态的时候,进行信号检测与信号的处理

<>(重要)用户态与内核态

上图是较为感性的理解,接下来我们较为理性的认识一下:

这里我们总结一下上图所包含的信息:
1.CPU内有寄存器保存了当前进程的状态当寄存器为0则在内核态,为3则在用户态。
2.用户态使用的是用户级页表,只能访问用户的数据和代码,而且每个进程有独立的用户级列表,
将进程的地址空间(虚拟内存)映射到物理内存中。
3.内核态使用的是内核级页表,只能访问内核的数据和代码,所有进程共用一个内核级页表。
4.进程之间无论如何切换,我们都能找到同一个OS,因为我们每个进程都有3~4G的地址空间,用的是同一张内核级页表,因此我们访问的都是同一个OS。
5.OS可以在进程的上下文中直接运行的,因为我们的OS代码已经被映射到地址空间3~4G中。
6.系统调用就是进程的身份切换到内核态,根据内核页表找到系统函数,执行。

所以我们代码中调用系统函数过程大致为:我们的用户态存着内核函数的接口(起始地址),当我们调用系统函数时,OS将我们进程的身份从用户态切换到内核态,通过起始地址访问内核级页表映射到物理内存,找到系统函数的代码,这样在我的进程的上下文当中就可以使用系统函数了,使用完系统函数,OS会将进程再切换到用户级执行后续的代码。

<>信号的处理

通过大量篇幅介绍完内核态与用户态,我们就可以来解释信号什么时候处理了

上图说明了信号被处理的过程:
1.调用系统函数从用户态切换到内核态。
2.执行系统调用函数。
3.返回前对该进程做信号检测。

4.先判断pending位图是否接受到信号,如果接受到信号再判断是否被阻塞,如果被阻塞则不管该信号继续向下查找是否接收到信号,当遇到未被阻塞的信号进行处理,一共有三种处理方式(默认,
忽略,
自定义),默认就是执行默认方法;忽略就是什么都不做,把该信号pending位图从1置为0;自定义捕捉就要根据hanlder函数的地址回到用户级执行handler代码。
5.信号处理完毕后如果在用户态切换到内核态根据sys_sigreturn函数返回到用户代码,执行下一行。

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为信号捕捉。
用一个图简要说明信号捕捉的过程
如果信号处理方式是默认或忽略(不是信号捕捉)的话则会走一圈左边圆圈的流程,不会执行handler和sys返回函数

<>总结

进程的信号总共会经历三个过程:
1.信号的产生前(信号如何产生)
产生有很多种,但归根结底都是由OS向进程发送的
2.信号的发送
信号会保存在PCB当中描述信号的三个表中,block位图表示信号是否阻塞,pending位图表示是否接受到信号,handler表示信号的处理方式
3.信号的处理
参考上图

技术
下载桌面版
GitHub
Gitee
SourceForge
百度网盘(提取码:draw)
云服务器优惠
华为云优惠券
腾讯云优惠券
阿里云优惠券
Vultr优惠券
站点信息
问题反馈
邮箱:[email protected]
吐槽一下
QQ群:766591547
关注微信