前言:本节内容讲述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陷入内核。
内核态: 允许访问操作系统的代码和数据。 用户态: 只能访问用户自己的代码和数据。捕捉过程
要理解信号的捕捉过程, 我们可以先看下面这张图。 接下来就是对这张图进行解释:
首先, 信号的处理必须是操作系统要先检测到信号。 那么这个检测的时机就是上面图中的红圈圈。 上面这张图是一个循环的, 但是进程的开始要从绿色的位置执行。 然后一开始在用户态, 在用户态执行main函数的时候, 遇到异常或者其他触发信号的调用, 就进入内核态。 也就是经过第一个蓝色圈圈, 到了下层。 然后操作系统就进行检测。 现在谈一谈这个检测:做检测, 首先要遍历pending列表。 看看有没有信号,再看看有没有block。 没有就正常执行, 有的话就不做处理, 继续看下一个。 直到遇到1, 并且block没有被阻塞就执行信号动作。那么此时如果这个信号是DFL,就是默认动作, 该怎么样就怎么样。 如果是IGN, 就是忽略, 什么都不做。 并且在执行这个信号动作之前将信号的pending由零变成1。 这两种都是直接在内核态执行的。 但是问题来了, 如果不是忽略或者默认, 而是自定义动作呢? ——要知道, 我们的自定义动作是用户代码,执行它就需要进入用户态。 那么我们为什么要使用用户态而不直接使用内核态呢? 因为如果我们调用的是非法代码。 盗取了我们计算机内的重要信息, 此时就会出现危险。 所以我们必须返回用户态。 然后从用户态处理完之后, 就会原路返回, 也就是利用sigreturn系统调用回归内核态, 再利用sys_sigreturn返回main。 那么进程我们知道是会被调度的, 只要他再跑, cpu就会调度它的进程。 只要需要调度, 时间片必然会消耗完毕。 消耗完毕就必然将时间片从cpu上剥离下来, 当下次调用进程的时候, 必然要把进程的各种队列放到寄存器里面跑, 而这个过程中, 见数据恢复到cpu中的过程一定在内核态, 而开始执行进程代码时一定是在用户态。 所以, 在进程调度的时候, 有无数次机会从进程用户态到内核态, 从内核态到用户态。——————以上就是本节全部内容哦, 如果对友友们有帮助的话可以关注博主, 方便学习更多知识哦!!!