一、信号是什么?
1.1.Linux信号的介绍
1.本质是一种通知机制,用户or 操作系统通过发送一定的信号,通知进程,某些事件已经发生,你可以在后续进行处理。
2.结合进程,信号结论。
3.进程要处理信号,必须具备信号 ,识别的能力,(看到+处理动作)
4.凭什么进程能够识别信号呢? 其实这是设计操作系统那批程序员已经设计好了的。
5.信号会临时记录下对应的信号,方便后续进行处理。
6.一般而言,信号的产生相对于进程而言是异步的。
1.2.信号概述
Linux中每个信号都有对应的信号和编号,名字都是以SIG开头,例如:SIGIO,SIGCHLD,头文件是<signal.h>
可以使用kill -l命令来查看具体的信号
可以看到少了 32 33 两个命令
1.2.1.信号的产生
1.通过终端按键产生信号 例如ctrl+c 等一些热键会产生对应的信号
信号通常会有三个处理信号的动作
a 默认 (这个是进程自带的,程序员写好的逻辑)
b.忽略(也是信号处理的一种方式)
c.自定义动作(捕捉信号)。
2.如何理解组合键变成信号呢?
先说所键盘的的工作方式:中断方式进行的。
当然能够识别ctrl+c
首先操作系统会先提前准备一份组合键的表,然后当用户按下一些热键时,与组合键进行对比,发现一致就产生对应的信号。
5.如何理解信号是怎样被进程保存呢?
其实Linux中拥有许多的信号,必须进行组织起来,在Linux中,采用位图结构来进行管理(位图,unisgned int). 这个位图在PCB中被保存。
6.如何理解信号发送的本质
操作系统 创建了一个位图保存在task_struct中
其实本质就是操作系统向 目标进程写信号 ,操作系统修改Pcb中指定的位图结构,完成发送信号的过程。
1.2.2.core_dump标记位的介绍
core_dump又被称为 核心转储 :是操作系统在进程收到某些信号而终止运行的时间,将此时进程地址空间的内容以及相关进程状态的其他信息写出一个磁盘文件,通常用于调试。
二、阻塞信号
1.信号一些概念的介绍
递达: 操作系统收到了信号并且实际开始做了事情称为递达(Delivery)
未决:信号从产生到递达之间的状态,称为信号未决。
被阻塞的信号,产生时,将保持未决状态,当进程解除对此信号的阻塞,才会执行递达动作
阻塞和忽略是不一样的,忽略是一种处理动作,而信号只要被阻塞就不会被递达
1.2. 信号保存图示意表
我们的内核之中有两张结构一样的位图,来表示当前信号是否被阻塞,和是否处于未决状态,还有一个函数指针表示处理的动作
信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达,信号标志才会被清除
1.3.普通信号易丢失
由于我们普通信号是由位图记录的,只能记录一次,不能记录个数,因此信号可能会丢失,
而实时信号是由链表保存的,实用性比较强,信号来了就会立即处理,并且链表会被管理起来,因此不易丢失。
1.4.sigset_t(信号集的介绍)
每个信号的阻塞或位决都是由一个比特位来表示的,不是0就是1,因此未决和阻塞标志可以使用同一个类型sigset_t 来进行存储,表示信号有效还是无效。
在阻塞状态中,有效 无效 表示是否被阻塞,阻塞信号集(block表)也被叫做当前信号的信号屏蔽字
在位决状态中 ,有效 无效表示信号是否处于未决状态。
1.5.sigprocmask函数与sigpending函数介绍
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
功能:设置阻塞或者解除信号集,用来屏蔽信号或解除屏蔽,其本质是读取或修改进程的PCB中的信号屏蔽字,需要注意的是,屏蔽信号只是将信号处理延后执行(延后至解除屏蔽),而忽略表示将信号丢弃处理。
函数参数:
how:假设当前的信号屏蔽字为mask
SIG_BLOCK: 设置阻塞,set表示需要屏蔽的信号,相当于 mask = mask | set 。
SIG_UNBLOCK: 解除阻塞,set表示需要解除屏蔽的信号,相当于 mask = mask & ~set 。
SIG_SETMASK:替换信号集,set表示用于替代原始屏蔽集的新屏蔽集,相当于 mask = set,直
接把传入的set设置为当前阻塞信号集。调用sigprocmask解除了对当前若干个信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。
set:传入参数,是一个位图(32位),set中哪位置1,就表示当前进程屏蔽哪个信号。
oldset:传出参数,保存旧的信号屏蔽集,可用于恢复上次设置。
函数返回值
sigprocmask() returns 0 on success and -1 on error.
sigpending函数
#include <signal.h>
int sigpending(sigset_t *set);
函数功能:获取当前进程的未决信号集
参数:
set:传出参数,传出当前未决信号集。
函数返回值
sigpending() return 0 on success and -1 error
1.6代码示例(打印未决信号集)
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<stdio.h>
#include<stdlib.h>
using namespace std;
int main()
{
//设置阻塞信号
sigset_t mvector;
sigemptyset(&mvector);//先清理
//添加一个信号屏蔽
sigaddset(&mvector,SIGINT);//屏蔽2信号
sigaddset(&mvector,SIGQUIT);
//在阻塞信号集中添加了两个信号 1 ctrl+c 2 ctrl+/
sigprocmask(SIG_BLOCK,&mvector,NULL);//设置阻塞信号集 上面只是在阻塞信号集
//中增加了一个信号,并没有设置,所以不会生效,
//通过sigprocmask设置后才能生效————这一步
//就相当于把当前进程阻塞信号集的SIG_BLOCK
//信号给屏蔽了*/
while(true)
{
//循环打印未决信号集
//使用sigpend函数来获取当前的
sigpending(&mvector);
int signo=1;
for(signo=1;signo<32;++signo)
{
//判断32个信号是否在为决信号中;
if(sigismember(&mvector,signo)==1)//sigismeber 函数是用来判断是否为同一个函数
{
cout<<"1"<<endl;
}
else
{
cout<<"0"<<endl;
}
}
}
return 0;
}
编译并运行,可以看到最开始没有信号产生,未决信号集中对应位为0,32位全为0。当按键产生信号的时候,未决信号集中对应的2、3位将置1,未决信号集变为0110000000000000000000000000000。此时按键ctrl+c或者ctrl+\产生信号后,不会再终止进程,只会将未决信号集对应位置1,因为信号已经被屏蔽了。但是,9号信号不能被阻塞,我们可以通过 kill -9 杀死该进程。
注意:操作系统为了防止恶意代码比如说屏蔽了所有信号制作了一个无法被杀死的信号,所以操作系统 设置了9号 信号不能够被捕捉和屏蔽。
三.信号的捕捉具体介绍
3.1.信号的捕捉
操作系统向进程发出信号,进程不是立即执行信号的,而是在合适的时候,这个合适的时候是指在被递达的时候。
一个信号在信号抵达,是在内核态切换为用户态时,进行信号相关检测。
3.2 用户态,内核态
我们的进程是在不断在用户态和内核态进行切换的,因为内核权限高,用户态的权限低,当我们需要执行操作系统代码的时候,用户态是无法执行的,需要切换成内核态。
内核形态和用户形态是有物理地址支持的,操作系统只有一个,因此只有进程的存在,操作系统就会存在,每个进程的虚拟空间的内核空间对应的映射关系都是同样的,都是同一分内核代码
用户态:
当进程执行我们自己写的代码的时候,比如在栈上定义一个变量,写一个while循环,这时候进程就处于用户态。
内核态:
调用系统接口的时候,当前进程的时间片到了,开辟一个空间分配内存都会切换到内核态
为什么需要这样来回切换呢?
因为操作系统的代码用户没有权限去执行的,比如我们调用printf,在底层调用的就是wirte函数,在用户层面只能执行用户级页表的映射,而操作系统的内核页表映射的区域,用户态无法访问。
那么操作系统是如何做到来回切换状态的呢?
当cpu执行用户的代码时候,顾名思义就是用户级页表,来处理用户态。
当执行内核代码时候,用的就是内核级页表,处于内核态。
在cpu寄存器中,记录着当前进程是用户态还是内核态,内核态有权限访问用户态的代码,但是一般不去做。
进程的内核页表都是一样的,都映射到内存中同一位置,内核态本质是操作系统,因此只要进程在,操作系统就在,当处于内核态时,进程就是一个外壳,本质是操作系统
3.3.信号捕捉具体图解
这张图参考了一下大佬的
3.4可重入函数介绍
两个执行流进入用一个函数称位可重入函数
即同一函数即使被多个执行流同时调用,结果仍然正确。
不可重入函数:一个函数会因为访问全局变量而照成错乱
main函数调用insert函数向链表中插入node1时,插入操作分为两步,可是刚做完第一步的时候因为硬件中断使进程切换到内核,内核进行信号处理时发现处理信号的动作是用户自定义所以再次回到用户态,执行sighandler函数再次插入了一个结点,这时又发生用户态转换为内核态,内核态再次转换为用户态,继续执行main程序流。结果是,链表中先后被插入了两个结点,但最终只有一个结点被真正的插入到结点中。
像上面的例子,insert函数流被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,导致结果错误,像这样的函数称为不可重入函数。
反之,如果一个函数只访问自己的局部变量或参数,则称为可重入函数。
常见不可重入函数:
1.调用malloc/free的函数,因为malloc也是用全局链表来管理堆的
2.调用了I/O函数库,标准I/O库中很多实现都以不可重入的方式使用全局数据结构
3.STL库,STL考虑的是效率问题,安全问题需要操作者自己考虑(比如迭代器失效问题,可以侧面反映)
3.5.volatile介绍
volatile修饰的变量,是不可被覆盖的,读取变量必须读取变量真实的存储位置(不可读取缓存、寄存器等位置的值)