前言:本节内容讲述linux信号的捕捉。 我们通过前面的学习, 已经学习了信号的概念, 信号的产生, 信号的保存。 只剩下信号的处理。 而信号的处理我们应该着重注意的是第三种处理方式——信号的捕捉。 也就是说, 这篇文章其实最核心的部分就是在解释信号的捕捉。 但是大的篇幅还是在前面的预备知识上面。现在, 开始我们的学习吧!
ps: 本节内容适合了解信号概念, 信号产生, 信号保存的友友们观看。
目录
信号是什么时候被处理的呢?
重谈地址空间
操作系统的执行逻辑
cpu与用户态和内核态
捕捉过程
信号是什么时候被处理的呢?
我们要处理一个信号, 首先是我们自己知道自己收到信号了。 那么知道自己收到信号了, 进程就必须得在合适的时候查一查我们对应的pending位图, block位图以及handler数组。 而这三个表都属于内核数据结构, 进程不会, 也不能直接访问他们。 而解决方法就是让进程处于一种内核状态。 当进程从内核态转回用户态的时候就会对信号做检测并处理。
关于内核态和用户态, 是我们从学习linux到现在, 第一次听到的名词。一般我们自己进程在进行的时候, cpu在进行调度的时候不仅仅运行我们自己写的代码。 比如我们的代码里还有系统调用或者库函数调用。 对应着就是我们自己写的代码, 库自己写的代码, 操作系统曾经写的代码。 ——这些cpu都要跑。 就比如我们进程等待wait函数, 我们创建子进程的fork, 我们打印数据printf。 这些操作我们都没有自己进行, 都是通过系统调用或者库函数直接进行调用。
那么, 我们的操作系统既然不相信我们的用户, 所以在很多场景下, 它是需要我们的用户做一下相关的身份切换的, 才允许我们执行对应的代码。 一般我们在执行库函数的时候, 执行我们自己的代码的时候, 一般都是在用户态直接执行的。 还有一种是进程在cpu调度之下, 要陷入到操作系统内部, 执行对应的任务——最典型的就是在调用系统调用的时候。——这里要知道的是, 当我们调用系统调用的时候, 操作系统会自动把我们的用户的身份进行转变, 转变为内核身份。 然后由操作系统帮我们把函数执行完毕。 当返回时, 再把我们的身份从内核身份转化为用户身份。 此时才能够允许我们执行系统调用。 ——这里我们只需要知道, 系统调用除了调用函数, 也是需要进行身份变换的。 这里的身份就两个, 一个是用户态, 一个是内核态。
也就是说, 调用系统调用, 操作系统是自动会做身份切换的。 (从用户到内核, 或者从内核到用户)
重谈地址空间
我们其实之前学到的进程相关的知识, 其实都是和我们上图中的用户空间相关联(0~3GB), 但是在3~4GB, 还有一个内核地址空间。这个内核地址空间其实映射的就是操作系统的代码和数据。 而且因为操作系统是最先被加载进进程的程序, 所以它的代码和数据应该在内存中的较为底部的位置。 同时映射的时候需要有一个内核级页表进行映射。
那么这里问题就来了, 如果我们的系统中有很多进程的话, 用户页表有几份呢 ? 内核级页表有几份呢 ?
对于用户级页表: 有几个进程就有几份用户级页表, 因为进程具有独立性。 对于内核级页表: 有一份。所以, 我们的每一个进程, 看到的都是3 ~ 4GB内核空间里面的内容, 看到的内核级页表里面的内容, 看到的物理内存里面的操作系统的代码和数据的内容都是一样的!!
要知道, 我们的各种系统调用的方法, 比如write, read, wait, fork, sigset_t等等都是在内核空间的。
所以, 以后我们进程在代码区进行系统调用的时候, 就直接找自己内核地址空间里面的系统方法, 然后返回到代码区。
就如同在自己的地址空间里面直接调用
——在进程的视角: 我们调用系统当中的方法, 就是在我们自己的地址空间中进行执行的。 ——在OS的视角: 在任何一个时刻, 都有进程在执行。 我们想执行操作系统的代码, 就可以随时执行。操作系统的执行逻辑
执行逻辑就是——基于时钟中断的一个死循环。
在我们计算机里面都有一个芯片单元, 每隔很短很短的时间, 向计算机发送时钟中断。 接收到所谓的时钟中断后, 就要执行中断所对应的方法, 也就是操作系统的代码。 也就是说我们的计算机内部有一个芯片单元, 它每隔很短的时间就给cpu发送一次时钟中断, 这个就叫做一次滴答。 然后我们操作系统, 就被动的被时钟中断推着向后执行。
然后操作系统将前期的工作做完之后, 往后的过程中就是执行一个死循环, 在死循环中检测有没有时钟到来, 如果有时钟到来, cpu就去执行时钟中断对应的方法。具体的操作系统如何做的, 看下图:
这张图里面有些字段已经被博主标记出来了, 但是有些字段涉及的内容太多, 无法全部截图, 所以这里直接叙述:
首先说这个for(; ;) pause(); 这个是一个死循环,说明操作系统执行到这里后就什么都没干, 一直执行一个pause()。 pause的意思是暂停, 为什么暂停? 因为操作系统往后的动作, 都靠着时钟中断来驱动, 也就是说, 有外设中断, 操作系统就去执行对应的中断。
也就是说, 操作系统最后会卡在for(; ;) pause(); 什么都不去做, 一直暂停在这里相应中断。 而我们的操作系统响应中断, 就一直在执行相应的调用。 所以, 我们的硬件一直在推着操作系统在走。 所以, 我们的操作系统才一直推着我们的进程在走。 所以我们的代码才得以推进。
ps:我们的计算机天然就有计算时间的能力, 就比如我们的台式电脑即便断电, 再次开机的时间也能够正确。 这是因为计算机内部可以续电, 可能是纽扣电池, 然后我们计算机能够通过一种硬件单元进行时间的计算。
然后要着重说一下的就是这个调度程序初始化:
这个timer_interrupt, 如果没有中断就绪, 就会执行这个。 这个东西是用来相应每隔很小时间发来的中断的, 比如执行或者调度等操作。
cpu与用户态和内核态
我们说过, 么一个当前正在调度的进程, 对应的这个进程有自己的页表。 并且, cpu中有一个CR3寄存器, 这个寄存器直接指向当前进程所对应的用户级页表。
同时有一个ECS寄存器。 我们怎么知道我们当前是用户态还是内核态呢? 进程如果在执行用户的代码的时候, ECS一定是指向用户态。 进程如果在执行系统的代码的时候, ECS一定是指向内核态。 关键在于, 在ECS寄存器里面, 最后有着两个比特位。 这两个比特位有四种表示方式:00、01、10、11. linux内核中, 对于cpu常见的有两种工作模式, 一种叫做内核态00, 一种叫做用户态01。 所以如果今天想访问内核态对应的代码, 必须想办法将ECS的低两位由三置为0. 那么谁能做到呢? ——cpu必须给我们提供一个方法, 能够修改自己的工作状态, 即int 80陷入内核。
内核态: 允许访问操作系统的代码和数据。 用户态: 只能访问用户自己的代码和数据。捕捉过程
要理解信号的捕捉过程, 我们可以先看下面这张图。 接下来就是对这张图进行解释:
——————以上就是本节全部内容哦, 如果对友友们有帮助的话可以关注博主, 方便学习更多知识哦!!!