一,引言

本章节主要从信号的产生信号的保存信号的处理这几个方面深处解析。一个信号从发出到操作系统是一步步如何处理的这个过程。

二,信号的产生

信号产生的方式有非常多,在之前使用xshell中当进程出现卡死的状况时,经常使用(ctrl +c)来结束进程。这个过程就是给进程发送信号。首先为了可视化信号的这个过程,介绍信号的第一个函数,修改信号的默认处理动作:signal如下:

#include <signal.h>
// signum:要处理的信号(如SIGINT、SIGTERM)
// handler:信号处理函数/处理方式
void (*signal(int signum, void (*handler)(int)))(int);

这个函数会修改指定信号的默认执行方式,如上述第二个参数,通过函数指针的形式执行指定的函数,进而不执行默认的函数方法。如下:


当运行进程时,原本ctrl +c会结束进程。此时并没有结束,而是执行了修改后的函数方法。

1,前后台进程

在Linux操作系统中,进程是分前台进程和后台进程的区别的,由于一款操作系统会在运行是会产生多个进程,与之对应的键盘等等只有一个,为此Linux操作系统进行前后进程的区分。前台进程只有一个,可以接收键盘的输入等等;后台进程可以有多个但是不能接收键盘的输入。

在之前学的孤儿进程中,使用ctrl+z就不能杀掉该进程,由于该进程自动被提取的后台,无法接收的指定的信号。下面接收几个关于前后台进程查看切换的方法:

查看后台的所有任务:

jobs

fg+任务号:将指定进程提到前台

fg

ctrl+z:将进程切换到后台

2,信号产生的方式

首先就是上文中讲到的键盘输入的方式。其次系统输入,操作系统提供了系统接口,可以进行产生信号。函数原型如下:

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig)

向指定的pid发送信号。


#include <signal.h>
int raise(int sig);

自己给自己发送信号.


硬件异常产生信号,当进程进程除0,空指针访问等等时,直接程序崩溃。本质上也是操作系统给进程发送信号 使得程序结束。最后还可以由于软件条件来产生信号。在之前管道的学习中由于读端关闭,写端继续的情况下,操作系统就会给进程发送信号使得写端结束。这就是一种由于软件条件来引起的信号的产生方式。下面介绍一个闹钟函数--alarm

unsigned int alarm(unsigned int seconds);

seconds:当进程等待对应的秒数,会发送向对应的信号,默认是14号信号。可以在同一个进行进程重复设置,若上一个alarm函数没有执行就设置新的alarm函数。alarm函数会返回相对于的差值。


总结:信号的产生是有多种方式的大致包含以下五种情况:键盘,系统调用,系统命令,软件条件,硬件异常。

三,信号的保存

在操作系统给某个进程发送信号,实际上就是修改用于控制信号这个结构体对于的值。也就是说,进程内部是有专属于管理信号对于的结构体的。在进程的PCB中中存放这以下三个表:

大致就可以理解位这是三个数组,第一个block表述是否被阻塞。pending表示是否接收这个信号。handler就是指向对应的默认处理方法。因此产生信号之后,会修改对应的pending的比特位。进行进程也就接收到了这个信号。因此,操作系统中有一个类型专门负责这种信号的存储类型--sigset_t类型。对于这种类型,每一个bit位数代表一种信号是否存在。因此在使用这种变量时必须要先对该变量进行初始化操作,以及赋值等等。下面就介绍一些用于修改pending这个类型的方法:


#include <signal.h> 
int sigemptyset(sigset_t *set); 
int sigfillset(sigset_t *set); 
int sigaddset(sigset_t *set, int signo); 
int sigdelset(sigset_t *set, int signo); 
int sigismember(const sigset_t *set, int signo);

1,函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表⽰该信号集不包含 任何有效信号。

2,函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表⽰该信号集的有效信号 包括系统⽀持的所有信号。

3,在指定的信号集添加某种信号。

4,在指定的信号集中删除某种信号

5,相当于查找该信号集中是否存在指定有效的信号


在对于的block表中,最主要就是修改对于的信号是否处于阻塞情况,函数介绍如下:

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);  
返回值:若成功则为0,若出错则为-1

这个函数就相当于是一种替换的功能。

how:表述替换方式类似于权限中的umask,规则如下:

一般来说,都是选择最后一种。

set:表述替换的信号集

oset:表述原本没有被替换的信号集,如何替换错误等等,可以使用该变量将原本的信号集保存在这个变量中。


四,信号的处理

上述讲述了信号的产生,信号的保存,那么操作系统对于产生的信号是如何进行处理的呢,在什么时候进行处理的呢,这时候就要引出第一个概念:用户态和内核态,如下图:

其中上半部分叫做用户态,下半部分叫做内核态。信号就是在内核态返回用户态这个过程中进行查看handler表进行处理。在理解中断之前要讲解一个话题:硬件中断


1,硬件中断

其中这里的中断向量表本身就是操作系统的一部分,当主机刚刚启动就会被加载到内存中。硬件中断就是当外部设备准备就绪时,会向中断控制器发起中断,这时中断控制器就会通知CPU,这个外部设备已经准备好啦,这时CPU就会向中断控制器,获取中断号,进而执行对应的中断处理方法。最终结束。

2,时钟中断

上述的中断都是基于硬件进行完成,但是在没有其他硬件进行中断,操作系统也要进行处理事物,这时,为了解决这类问题,工程师直接将一个硬件--时钟嵌套进入CPU内部,以固定的频率进行执行中断。因此操作系统本质上就是一个死循环,不断的进行中断来检查有没有事物要进行处理。

3,软中断

上述本质上来说还是硬件,除了硬件之外还有软件,在操作系统中为了支持系统调用,CPU也设计了对应的汇编代码,使得CPU执行中断逻辑。

也就是说操作系统本身并不提供任何的系统调用接口,只提供系统调用号。以前所使用的系统调用接口全部都是经过封装过的。


五,总结

本质来说,操作系统首先会在用户层执行对应的代码,当涉及到信号时,会修改进程PCB对应的控制信号的结构体。而操作系统本身通过中断的形式会由用户态进入内核态,在检查内核态工作完成之后,会检查信号的数组,看有没有需要执行的信号,若有就会从内核态跳转进入用户态。执行对应函数。之后返回内核态,重复检查。若没有则返回用户态继续执行对应的代码。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐