信号

一、什么是信号

信号的本质是通知进程发生了某个事件,打断进程当前的操作,去处理该事件。信号是一个软件中断,作用是事件通知。
linux下有62中信号(查看命令·kill -l),如图:
在这里插入图片描述
其中1-31号信号是非可靠信号(有可能产生信号丢失),34-64号信号为可靠信号(不会产生信号丢失)。

二、信号的生命周期

2.1 信号的产生

2.1.1 硬件产生

本质:通过键盘去向进程发送一个信号。
例如:
ctrl+c(SIGINT–进程中断)
ctrl+z(SIGSTOP–停止进程)
ctrl+\(SIGQUIT–退出进程)

2.1.2 软件产生

本质:通过一些命令,系统调用接口或者库函数,向进程发送一个信号。
例如:
1.命令:kill -signum pid
kill+(-信号)+(进程pid)
2.接口:int kill(pid_t pid,int sig);
在这里插入图片描述
功能:向一个指定进程发送一个信号。
返回值:成功产生返回0,失败返回-1。
pid_t pid:进程pid。
int sig:信号类型。
举例:
在这里插入图片描述

3.接口:int raise(int sig);
在这里插入图片描述
功能:向当前进程发送一个信号。
返回值:成功产生返回0,失败返回非0。
int sig:信号类型。
在这里插入图片描述
以上两个接口,我在举例中没有取判断其返回值,为了更加完善,读者可以添加判断,是否成功产生信号。
4.接口:void abort(void);
在这里插入图片描述
功能:向当前进程发送一个SIGABRT信号
在这里插入图片描述
abort没有返回值,不用传参,直接向进程发送一个SIGABRT信号,方便快捷。
5.接口:unsigned int alarm(unisgned int seconds);
在这里插入图片描述
功能:设置一个定时器,seconds秒后向进程发送一个SIGALRM信号,如果多次调用该接口,会取消当前时间未到的定时器。
返回值:返回信号发送时定时器剩余的秒数,如果没有剩余,则返回0。
unsigned int seconds:秒数。
举例:
在这里插入图片描述

2.2 注册信号

pending位图:防止信号产生后丢失,并且去具象描述信号,所以每个进程PCB中,有一个位图(未决(没有被处理)信号集合),进程在运行中,通过这个位图可以明确自己收到了哪些信号,进程收到什么信号,就在位图的相应位置置1。
sigqueue双向链表:但是pending位图只能告诉进程收到了哪些信号,但是并不能去告诉进程收到了多少个,所以在进程PCB中还有一个sigqueue双向链表,每个节点保存信号的信息。从而可以通过节点是否相同,判断有多少相同的信号。
非可靠和可靠信号注册的区别
非可靠信号:如果一个信号已经注册,则不做二次注册(多个相同信号发送给同一个进程,但是进程只会注册第一次发送的信号,后面的相同信号被丢失)。(不可以在sigqueue添加相同节点信息,最多只有一个节点)
可靠信号:可以二次注册(可以在sigqueue添加相同节点信息,可以有多个相同节点)。

2.3 注销信号

信号的注销,本质就是消除信号在进程中的痕迹,也就是删除sigqueue双向链表中对应信号节点,当sigqueue双向链表中没有对应信号的节点,则对应信号在pending位图中置为0。

2.4 处理信号

2.4.1 信号的处理

首先我们需要知道,处理一个事件就是完成一个对应的功能,而在C语言中完成某个功能的最小模块就是函数,所以处理一个信号,就是去让信号调用对应的处理函数,去执行该函数。并且信号什么时候处理,是由进程察觉自己收到了什么信号,然后去处理,并不是由我们用户去决定。
信号的处理方式:
默认处理方式:系统定义的,若没有特殊指定则采用系统默认。
SIG_DFL:默认处理方式。
忽略处理方式:针对对应事件,不做任何事情。
SIG_IGN:忽略处理方式。
自定义处理方式:用户自己定义信号处理函数。
接口:sighandler_t signal(int signum, sighandler_t handler);
在这里插入图片描述
功能:修改信号的处理函数。
返回:返回信号原来的处理函数地址。
int signum:信号值。
sighandler_t handler:信号处理函数的地址(回调函数)–函数定义格式:typedef void (*sighandler_t)(int);。
举例:
忽略处理方式:
在这里插入图片描述
回调函数的应用:
在这里插入图片描述
这里需要注意的就是自己编写回调函数,一定要传递参数no,这个参数是系统自己传参时传递的信号值(不同信号有自己不同的函数处理方式)。
返回值的应用:
在这里插入图片描述
信号处理第一次收到信号去使用更改后的处理方式,处理完毕后,信号的处理方式又变回原来的处理方式,所以第二次收到信号则进程退出。

2.4.2 信号的捕捉

首先我们需要知道,程序地址空间分为用户空间和内核空间,所以程序的运行态也分为用户态和内核态,程序在运行过程中,如果运行的是库函数或者我们用户自己编写的代码,则在用户态运行,可以对用户空间进行随意访问,如果运行的内核中的代码则在内核态运行。
程序从用户态切换到内核态:系统调用,中断,异常(异常会引起中断)。
信号的捕捉流程:
一个进程主控流程因为系统调用,会被切换到内核态,完成系统调用后,在返回用户态之前,会调用do_signal接口去检测当前收到的信号,如果有信号则取处理信号,如果信号处理函数是用户自定义的,则需要返回用户态,进行函数处理,处理完成后,又会返回内核态,如果信号处理方式是系统自定义的,则只需要在内核态中处理函数即可,处理完毕后,如果当前没有信号了,则返回用户态,继续主控流程。
画图说明:
在这里插入图片描述
这里需要注意的是,信号的处理是在内核态即将返回用户态时进行的,并且根据处理函数为用户自定义还是系统定义判断,是否返回用户态进行函数处理,最终所有信号处理完毕,进程才会从内核态继续返回用户态主控流程运行

2.5 阻塞信号

进程收到某阻塞信号时,先不去处理,等阻塞解除,才会处理该信号。
阻塞信号位图:为了让进程知道哪些信号被阻塞,在进程PCB中,有一个阻塞信号集合位图,被添加到该位图的信号就表示阻塞该信号,反之解除阻塞则将位图对应位置置为0。
接口:int sigprocmask(int how,const sigset_t * set,sigset_t * oldset);
在这里插入图片描述
int how:对阻塞集合进行的操作(SIG_BLOCK/SIG_UNBLOCK/SIG_SETMASK).
sigset_t:信号位图的结构体(本质是一个数组)。
const sigset_t * set:
SIG_BLOCK:将set集合中的信号添加到阻塞集合中(阻塞set信号)。
SIG_UNBLOCK:将set集合中的信号从阻塞集合中移除(解除set信号)。
SIG_SETMASK:直接将阻塞集合的值修改为set。
sigset_t * oldset
保存原有的阻塞集合信号的位图(用于还原)。
举例之前我们简要介绍几个信号阻塞的其他相关接口:
在这里插入图片描述
举例:
在这里插入图片描述
注意,9号信号和19号信号,这两个信号不会被阻塞,不会被自定义,不会被忽略。

三、信号的实际应用

3.1 17号信号(SIGCHLD)

子进程退出,就会给父进程发送SIGCHLD信号,但是该信号的默认处理方式为忽略,什么也不做,所以如果父进程不是一直等待子进程退出处理,就会造成子进程成为僵尸进程。所以这是可以在自定义函数处理方式,在函数中添加wait/waitpid接口。这里需要注意的是,17号进程是一个非可靠信号,所以进程中只能去让一个子进程正常退出,有可能造成事件丢失。

3.2 13号信号(SIGPIPE)

所有管道的读端关闭,则继续向管道写入数据,则会造成触发异常,这个异常信号就是SIGPIPE,这个信号的默认处理方式为退出进程。

四、函数的可重入与不可重入

函数的重入:在不同的执行流(不同的执行流程,例如main函数主控流程或者信号事件执行流程)中进入同一个函数进行函数处理。
函数的重入: 函数一旦重入,并不会造成逻辑混乱或者异常情况。
函数的不可重入:函数一旦重入,会造成数据二义和逻辑混乱。
可重入和不可重入的判断基准:一个函数是否对一个全局变量进行了不受保护的非原子操作。(是则不可重入,反之可重入)
举例说明:
在这里插入图片描述
当发送2号信号(ctrl+c)时,造成函数重入,在main函数主控流程a++运行完成睡眠过程中,2号信号打断当前操作,进入信号的函数处理,这是a++再次运行,b++运行一次,所以第一次a=3,b=2,而当信号处理函数运行完毕后,进入主控流程,主控流程继续运行b++,从而第二次a=3,b=3。

五、volatile关键字

首先我们需要知道,在cpu中有很多寄存器,当一个代码优化程度过高,编译器就会直接设定将这个变量的值直接加载到寄存器中,每次cpu访问这个变量时,就会直接从寄存器中获取,不会从变量获取(当变量值改变,就会造成结果错误等)。
volatile关键字功能:volatile关键字用于修饰变量,防止编译器对变量过度优化,保持变量的内存可见性(cpu每次操作变量都要从内存中重新获取变量的值)。
举例:
在这里插入图片描述

如上图,当我们直接运行时,因为该代码没有过度优化,所以每次使用a都是从变量中获取值,所以程序就退出了,而当我们使用gcc - O2命令(O代表代码优化,2代表优化等级),a的值变为了0,但是程序依然没有退出,还是死循环(这就是因为while循环每次使用的都是寄存器中的值)。
当我们使用volatile关键字:
在这里插入图片描述
无论是否优化代码,运行程序中使用变量a时,始终从获取的都是变量a的值,所以传入2号信号后,a变为0,while循环结束,程序退出。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值