【linux】信号的保存和递达处理(一)

简介: 上节我们了解到了预备(信号是什么,信号的基础知识)再到信号的产生(四种方式)。今天我们了解信号的保存。信号产生,进程不一定立马就去处理,而是等合适的时间去处理,那么在这段时间内,进程就需要保存信号,到了合适时间再去执行!

一、递达,阻塞,未决

       我们知道,信号是发送给进程的,而进程又是被操作系统创建pcb(信号的相关信息被保存到进程pcb中)而进行管理的,所以修改或者访问进程pcb都需要操作系统来进行,那么信号发送的本质就是:操作系统在向进程发送信号。


      信号产生,进程不一定立马就去处理,而是等合适的时间去处理,那么在这段时间内,进程就需要保存信号,到了合适时间再去执行!那么实际执行信号的处理动作称为信号递达;信号从产生到递达之间的状态,称为信号未决(Pending)。进程可以选择阻塞 (Block )某个信号。


       被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。我们之前知道,进程递达之后的动作有三种:默认动作、自定义动作、忽略动作(执行动作,只不过这个动作就是什么都不做)。注意:阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。


二、信号的保存

       我们知道信号是保存到进程pcb中的,信号产生、信号递达、信号阻塞、信号未决这些到底怎么实现的呢?我们来看:


2.1 信号在内核中的数据结构构成

 上图就是信号在内核中的数据结构构成,我们来慢慢了解。首先信号的相关信息都在进程pcb中存储,判断信号发送给进程后的状态都是位图来实现的。


       unsigned int pending = 0;这是信号未决的位图结构,一共有32个比特位,分别代表32个进程信号的编号,当然比特位的内容(0/1)也代表进程是否收到了对应的信号,收到信号但未递达,对应编号的比特位就会由0改为1。


       unsign int block =0 ;这是信号阻塞的位图结构,一共有32个比特位,分别代表32个进程信号的编号,当然比特位的内容(0/1)也代表进程是否阻塞了对应的信号,收到信号被阻塞,对应编号的比特位就会由0改为1。


       如果某个信号被阻塞,那么阻塞位图结构中对应的比特位(信号编号)就会置为1,那么在此信号阻塞未被解除之前,会一直处于信号未决(信号产生但未被处理)非阻塞被解除。


       handler_t handler[32] :信号递达后要处理动作,那么handler这个数组中一定存放着信号编号所对应的处理动作。handler_t 其实是函数指针类型,typedef void(*handler)(int signo); 参数是信号编号,返回值是void的函数指针。数组的下标就是对应的信号编号,数组下标中的内容就是对应信号的处理方法(函数指针)。


       当调用signal(signo,handler); ,就会把信号对应的处理方法设置为自定义方法,内核中就是将数组下标(信号编号)中的内容(处理方法)设置为自定义方法的函数指针。从而在递达后执行处理方法。


       所以我们知道,为什么进程可以识别信号呢?原来是因为程序员在设计进程的时候,已经为进程设计好了这三种结构,从而去识别信号!


2.3 用户态和内核态

       信号产生时,进程可能不会立马去处理,而是等待合适的时机,那么这个合适的时机是什么时候呢?是从内核态返回到用户态!哦吼,那什么是用户态和内核态呢?我们来看:


      我们编写的代码一般都是用户层级的代码,那当我们去调用接口去访问os自身的资源(getpid等等),去printf(访问硬件资源)的时候,这就需要我们切换身份为内核态去执行这些操作!访问不同的资源始终是进程,但是当他的身份不同的时候,那么可以访问的资源就是不同的!


       用户为了访问内核或者硬件资源,必须通过系统接口完成访问。那么系统调用肯定是比进程互相调用用户态层级的代码慢得多,因为他需要身份的切换等等,所以我们尽量避免频繁的调用系统接口。(这就是为什么vector中的扩容他需要一次性去扩充1.5/2倍的空间,因为这样就可以避免频繁的扩容,导致频繁的去调用系统接口,导致速度和效率大大下降)


       那么我们就会想,那到底是怎么操作这个身份的呢?如何就知道它是内核态或者用户态的呢?我们都知道进程在执行时,会将此进程的上下文投递到cpu的寄存器中,那么此时cpu中还有很多寄存器存放着不同的信息:


  cpu内部的寄存器分为:1.可见寄存器 2.不可见寄存器。其中,有存放着进程pcb的起始地址的寄存器(这样就可以访问进程的所有信息),有存放页表起始地址的寄存器,也有存放着当前进程的运行级别的寄存器(利用位图结构,来表示不同的级别),所以当进程去访问内核的资源的时候,os就会到cpu的CR3去看进程的运行级别,如果处于内核态,那可以访问,反之。


       我们了解了访问的条件,但是他到底是如何到os中访问资源呢?来看:


  每一个进程都有[3,4]G的内核空间,[1,3]G的用户空间,且都享有同一个内核级页表。


       之前我们知道,当动态库加载到物理内存时,是可以通过页表映射到进程空间的共享区,之后在执行代码若执行到共享区的代码时,就会在当前地址空间(起始地址+偏移量的方式)去跳转到共享区去执行代码,执行完毕后,再回到对应执行的代码。每一个进程他都有自己的一套内核结构(进程的独立性),且都有不同的用户级页表。


       但若去访问操作系统的资源,因为操作系统只有一个,当开机时,操作系统的资源会被加载到物理内存,进程访问时,通过同一个内核级页表。所以无论进程怎么切换,都不会更改3-4G的内核空间。


       那什么时候从用户态切换到内核态呢?系统调用的最开始。(根据 Int 80(汇编代码),会把寄存器中的进程运行级别状态修改。(系统调用最开始就设计了这样))


2.3 信号的捕捉流程

       我么们了解了内核态和用户态以后,就可以了解到,原来信号产生,不会立即被进程所处理动作,而是等到合适的时机去处理,这个合适的时机就是内核态切到用户态的时候。那我们一定之前就进入了内核态,我们来看:



  当进程需要访问内核资源的时,就会通过系统调用来切换身份,由用户态切换到内核态,之后进行系统调用(cpu中改变身份,通过内核级页表去访问内核资源),到这里本应该就是切换到用户态返回的,但是来都来了,而且切换到内核态确实不容易。所以就会通过进程中的pending,block,headler进行信号的检测过程(先在pending中查看信号是否存在,再到block中查看是否被阻塞,如果阻塞则该信号处于未决,继续查看pending中的下一个信号,如果没有被阻塞,那就信号递达,通过handler去处理动作(默认、自定义、忽略)。当然在信号递达前,会将pending中该信号对应的比特位由1变为0,再去执行。


       忽略其实最容易执行,只需要将pending中1改为0以后,啥都不做;而自定义就需要再将身份切换为用户态,然后去执行handler中的方法。那为什么不直接在内核态中去执行用户态中的方法呢?是因为操作系统不信任任何人,如果用户态的代码是问题代码,那么就会导致操作系统出现严重问题,所以会先切换用户态,再去执行handler中对应的方法(用户态执行一些代码会受到限制)。递达后为什么不直接回到进程中呢?是因为我们没办法直接回到当前进程执行的位置,这个过程需要操作系统的操作。所以只能再回到内核态,再由内核态切到用户态回到进程执行的位置。


       我们直接抽象看本质:


目录
相关文章
|
4月前
|
Linux 调度
Linux0.11 信号(十二)(下)
Linux0.11 信号(十二)
36 1
|
4月前
|
存储 Linux 调度
|
4月前
|
存储 Unix Linux
Linux0.11 信号(十二)(上)
Linux0.11 信号(十二)
40 0
|
4月前
|
Linux
|
5月前
|
安全 小程序 Linux
Linux中信号是什么?Ctrl + c后到底为什么会中断程序?
信号在进程的学习中是一个非常好用的存在,它是软件层次上对中断机制的一种模拟,是异步通信方式,同时也可以用来检测用户空间到底发生了什么情况,然后系统知道后就可以做出相应的对策。
158 6
|
5月前
|
缓存 网络协议 算法
【Linux系统编程】深入剖析:四大IO模型机制与应用(阻塞、非阻塞、多路复用、信号驱动IO 全解读)
在Linux环境下,主要存在四种IO模型,它们分别是阻塞IO(Blocking IO)、非阻塞IO(Non-blocking IO)、IO多路复用(I/O Multiplexing)和异步IO(Asynchronous IO)。下面我将逐一介绍这些模型的定义:
268 2
|
5月前
|
存储 NoSQL Unix
【Linux】进程信号(下)
【Linux】进程信号(下)
44 0
|
5月前
|
安全 Linux Shell
【Linux】进程信号(上)
【Linux】进程信号(上)
53 0
|
6月前
|
Linux Shell
蓝易云 - 【Linux-Day8- 进程替换和信号】
这两个概念在Linux系统编程和shell脚本编写中都非常重要,理解它们可以帮助你更好地理解和控制Linux系统的行为。
43 9
|
6月前
|
Linux
【Linux】进程信号_1
【Linux】进程信号_1
30 0