【Linux】进程信号的产生
个人主页~
进程信号的产生
- 一、进程信号的概念
- 二、信号的产生
- 1、常用的信号解析
- 2、从硬件到软件了解信号的产生
- 3、产生方式
- (一)键盘组合键
- 后台程序
- (二)命令
- (三)系统调用
- (四)程序异常
- (1)除0异常
- (2)野指针异常
- (3)总结
- (五)软件模拟---alarm闹钟
一、进程信号的概念
进程具备能够识别并处理信号的能力,这数据进程内置功能的一部分
当进程真正收到信号的时候,具备在合适的时候处理这个信号的能力
进程在信号产生到信号处理的这段时间有临时保存哪些信号已经发生了的能力
信号的处理方式有三种:默认动作、忽略、自定义动作
二、信号的产生
1、常用的信号解析
我们常用的信号就是ctrl+c
了,可以及时的把我们的前台进程(在Linux中,一个终端一般只有一个bash,每一次登陆只允许一个进程是前台进程,可以允许多个进程是后台进程)杀死,首先我们得到的结论是:ctrl+c
的信号是被我们的前台进程接收到了
实际上,ctrl+c
被进程解释成了2号信号,也就是SIGINT
中断信号
这张表中,前31个信号是普通信号,后31个信号是实时信号,其中没有32、33号信号,共62种信号
我们可以通过一些小办法来验证一下ctrl+c
是2号信号:
我们前面说过信号的处理方式有三种:默认动作、忽略、自定义动作,我们按下ctrl+c
前台程序中断这就属于默认动作,我们可以使用一个系统调用接口来捕捉信号,设置成我们自定义的动作,这个系统调用接口就是signal
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
返回值:成功返回之前该信号的处理函数指针,出错返回SIG_ERR
signum
:要处理的信号编号,参照上面的表
handler
:函数指针,指向处理信号的函数,这是自定义动作,其中默认动作:SIG_DFL
,忽略:SIG_IGN
信号被捕捉后“就不会让它逃跑了”,也就是说,信号捕捉后执行完sigint_handler
函数就继续运行下面的while循环打印,此时只能通过kill -9
杀死进程,当然也可以ctrl+z
停止进程
其中在所有信号中,9号信号SIGKILL
和19号信号SIGSTOP
是不可被捕捉的,他俩被捕捉了就没人能治的了进程了
2、从硬件到软件了解信号的产生
在现代操作系统中,为了保障系统的安全和稳定,采用了用户态和内核态的分层设计,进程通常运行在用户态,权限受限,不能直接操作硬件资源,而操作系统内核运行在内核态,拥有最高权限,负责管理和协调所有硬件设备,因此,当键盘产生输入信号时,首先接收到并处理该信号的是操作系统内核
在早期的计算机系统中,轮询是一种检测外设状态的方法,操作系统会按照一定的时间间隔,主动去检查键盘设备文件的状态,看是否有新的数据产生,如果有数据,就将其从键盘外设读取到内核的文件页缓冲区,以便后续处理
现代计算机系统通常会连接多个不同类型的外设,如鼠标、键盘、打印机、网卡等,如果对每个外设都采用轮询方式进行检查,CPU 会花费大量的时间在不断查询外设状态上,而无法高效地执行其他重要任务,这会严重降低系统的整体性能,所以,现代操作系统一般不采用这种方式来处理外设数据
在计算机的体系结构中,CPU 和外设之间的数据传输往往需要借助 I/O 接口电路和相应的控制器,CPU 一般不会直接与外设进行数据通信,但是,当外设产生新的数据时,外设可以通过中断控制器向 CPU 发送一个硬件中断信号,通知 CPU 有新的数据需要处理
为了区分不同外设发出的中断信号,每个中断都被分配了一个唯一的中断号,当 CPU 接收到中断信号后,会根据中断号来确定是哪个外设产生了中断,操作系统维护着一个中断向量表,该表存储了每个中断号对应的中断处理程序的入口地址,CPU 以中断号为下标,从中断向量表中获取相应的中断处理程序入口地址,然后跳转到该地址执行中断处理程序,从而完成对外设数据的处理
当用户按下键盘上的按键时,操作系统会对输入的内容进行解析,输入内容可以分为普通数据(如用户输入的字母、数字等)和控制信息(如组合键、功能键等),以ctrl+c
组合键为例,操作系统会识别ctrl+c
组合键,并将其转换为SIGINT
信号(信号编号为 2),然后将该信号发送给当前正在运行的前台进程,进程接收到该信号后,通常会执行相应的操作(如终止进程)
我们所学习的信号,就是用软件的方式,对进程模拟的硬件中断
3、产生方式
首先我们要知道的是:不管信号的产生方式有多少中,信号是由操作系统发送给进程的,因为操作系统是进程的管理者
我们信号学习的第一个产生的方式就是键盘组合键,ctrl+c
中断、ctrl+z
暂停等等
我们学习过程中知道的第二个产生的方式就是kill
命令,通过配合不同的选项可以达到不同的效果
第三个产生的方式就是系统调用,类似于int kill(pid_t pid, int sig);
在进程中杀死另一个进程
第四个产生的方式就是程序异常,比如说除数为0的情况,或者野指针的情况
第五个产生的方式就是通过软件模拟形成闹钟
(一)键盘组合键
我们前面已经解释过键盘组合键产生信号的原因了,这里我们来介绍一下后台程序
后台程序
我们在上面解释过了程序组合键ctrl+c
可以中断前台进程,但后台进程是奈何不了的,我们正常执行的进程是前台进程,在我们正常执行程序的命令后面加一个&
,可以将程序转到后台执行
首先我们看到第一行的打印[1] 13128
,其中方括号内的数字叫做作业编号,它是由shell分配给每个后台作业的一个唯一标识符,它是线性递增的,如果我有三个进程在后台运行,作业编号分别为123,我杀死作业编号为2的进程后,再运行一个新的后台进程,它的作业编号是2,后面那个数字就是进程PID
我们知道我们在打开终端后,默认在前台运行的就是bash
进程,在正常执行进程的时候它就跑到后台了,我们做了一个小实验,在后台程序test
执行过程中,我输入了ls
命令,不难看出他也打印出来了我们对应的内容,此时前台就是bash
进程,此时再按ctrl+c
就不会被捕捉了,因为键盘的输入只会被前台进程读取
(二)命令
命令就不多说了,就是kill
命令后面加上信号编码,表示是什么信号
(三)系统调用
kill
的作用就是指定信号发送给指定进程
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
返回值:调用成功返回0,失败返回-1
pid
:指定要接受信号的进程PID
sig
:要发送的信号编号
raise
:的作用就是进程自己给自己发信号
#include <signal.h>
int raise(int sig);
返回值:成功返回0,失败返回非0
sig
:要发送的信号编号
abort
的作用是异常终止当前的进程
#include <stdlib.h>
void abort(void);
(四)程序异常
(1)除0异常
我们可以看到除0前的打印执行了,除0后的打印没有执行,这是为啥呢?
程序中的计算任务由 CPU 执行,CPU 设有特定寄存器用于存储操作数,同时还有一个状态寄存器,它包含多个比特位级别的标记位,其中一个标记位专门用于指示当前运算是否发生异常,当 CPU 执行除 0 操作时,会触发异常,此时 CPU 会修改状态寄存器中相应的异常标记位,寄存器中的内容属于当前进程的上下文数据,在进行进程切换时,这些数据会被替换为即将执行的其他进程的上下文数据;而当该进程再次获得 CPU 执行权时,其上下文数据又会被恢复,通常情况下,非严重异常仅会对当前进程产生影响
CPU 作为硬件资源,操作系统作为硬件的管理者,需时刻关注硬件状态,当 CPU 中的异常标志位被设置时,会触发中断机制,此时,CPU 会暂停当前程序的执行,保存现场信息(如程序计数器、寄存器值等),然后根据中断向量表转去执行对应的中断处理程序,操作系统在中断处理程序中感知到异常后,会依据异常的具体类型,向引发异常的进程发送相应的信号,比如 SIGFPE
,以此通知进程处理异常情况
(2)野指针异常
空指针解引用和越界访问这一类的问题都是虚拟内存通过页表映射到物理内存的时候出错,导致硬件报错,进而被操作系统识别
(3)总结
程序对于异常信号,默认动作是让程序立即终止,但是我们可以在程序中对异常信号进行捕捉,如果捕捉方法里没有做退出的特殊处理,那这导致的后果就是程序不会立即终止,而是一直被调用运行,硬件错误就一直存在,操作系统就会一直给进程发送异常信号,因为它的状态寄存器中相应的异常标记位在这个进程离开CPU之前是不会改变的
(五)软件模拟—alarm闹钟
alarm
用于设置一个定时器,在指定的秒数后向进程发送SIGALRM
信号,如果在seconds
秒后没有其他操作来处理SIGALRM
信号,进程通常会终止并产生一个默认的行为
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
返回值:如果之前没有设置过定时器,返回 0,如果之前已经设置过定时器,返回距离下一次定时器到期剩余的秒数
seconds
:秒数
5秒后捕捉到闹钟信号,执行信号处理函数
如果我们想定时执行某个任务task
,实现如下的结构
在闹钟响后再定个五秒的闹钟,像不像早上赖床的你,这里的任务就是睡觉,main
函数中的alarm
就是你昨晚上定的闹钟,信号处理函数中的alarm
就是早上闹钟响后你按下的10分钟后再响
函数体搞明白了,我们再解释一下它的返回值
我们在进程main
函数发送alarm
之前发送14号信号,可以看到这里的剩余时间就是设定的时间减去main
函数执行的时间,此时跳到信号处理函数执行,重新设置alarm
,5秒后再响铃剩余时间就是0了
事实上,alarm
有管理它的结构体,你可以简单理解为,在调用的时候返回值是存在于结构体某个成员变量中的(返回值事实上是经过转换后得出的结果),然后在下次调用alarm
的时候返回的是上次alarm
的剩余时间,之后更新结构体中的信息,以设置新的定时器
今日分享就到这里了~