目录
1.CPU的四种状态与操作系统的两种状态(用户态和内核态)
- 首先我们要知道CPU有四种状态,分别为编号为0(特权最大)到3(特权最小),以及3个受保护的主要资源:内存、I/O端口和执行某些机器指令的能力。
- 操作系统它基于CPU之上,只用到了CPU的两种状态,一个内核态,一个用户态,内核态运行在CPU的第 0 等级,用户态运行在CPU的第 3 等级。
2.操作系统的用户态和内核态之间的切换
- 首先内核态与用户态是操作系统的两种运行级别,跟intel cpu没有必然的联系, intel cpu提供Ring0-Ring3三种级别的运行模式,Ring0级别最高,Ring3最低。
- 其次Linux使用了Ring3级别运行用户态,Ring0作为 内核态,没有使用Ring1和Ring2。Ring3状态不能访问
Ring0的地址空间
(这里存放在整个内核的代码和所有的内核模块,以及内核所维护的数据
)。 - 然后用户运行一个程序,该程序所创建的进程开始是运行在用户态的,如果要执行文件操作,网络数据发送等操作,必须通过write,send等系统调用(比如netty和redis中对于多路复用select和poll的改进epoll也需要切换到内核态),这些系统调用会调用内核中的代码来完成操作,这时,必须切换到Ring0,然后进入
内核地址空间
去执行这些代码完成操作,完成后,切换回Ring3,回到用户态。这样,用户态的程序就不能随意操作内核地址空间,具有一定的安全保护作用。 - 最后至于说保护模式,是说通过内存页表操作等机制,保证进程间的地址空间不会互相冲突,一个进程的操作不会修改另一个进程的地址空间中的数据。
3.操作系统的用户态切换到内核态的四种情况
-
系统调用
这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作,比如x86里的call gate也可以用来做系统调用,也能做到权限控制和内核代码保护,跟中断的效果完全一样, -
硬中断
- 当外围设备完成用户请求的操作后,会向 CPU 发出相应的中断信号,这时 CPU 会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。该中断可被屏蔽。
- 从本质上讲,中断(硬)是一种电信号,当设备有某种事情发生的时候,他就会产生中断,通过总线把电信号发送给中断控制器。如果中断的线是激活的(
与逻辑门的另外一边设置的是高电平,如果是低电平就进行了屏蔽
),中断控制器就把电信号发送给处理器的某个特定引脚。处理器于是立即停止自己正在做的事,跳到中断处理程序的入口点,进行中断处理。
-
软中断(softIRQ)
- 软中断(softIRQ)的一种典型应用就是所谓的"下半部"(bottom half),它的得名来自于将硬件中断处理分离成"上半部"和"下半部"两个阶段的机制:上半部在屏蔽中断的上下文中运行,用于完成关键性的处理动作;而下半部则相对来说并不是非常紧急的,通常还是比较耗时的,因此由系统自行安排运行时机,不在中断服务上下文中执行。
- 在系统调用的时候,也可以用软中断的方式实现,比如 fork() 实际上就是执行了一个创建新进程的系统调用。而系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现,例如 Linux 的 int 80h 中断。该中断不可被屏蔽。
-
异常
当 CPU 在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。- 首先明确下什么是缺页异常,CPU通过地址总线可以访问连接在地址总线上的所有外设,包括物理内存、IO设备等等,但从CPU发出的访问地址并非是这些外设在地址总线上的物理地址,而是一个虚拟地址,由MMU将虚拟地址转换成物理地址再从地址总线上发出,MMU上的这种虚拟地址和物理地址的转换关系是需要创建的,并且MMU还可以设置这个物理页是否可以进行写操作,当没有创建一个虚拟地址到物理地址的映射,或者创建了这样的映射,但那个物理页不可写的时候,MMU将会通知CPU产生了一个缺页异常。
- 下面总结下缺页异常的几种情况:
1、当MMU中确实没有创建虚拟页物理页映射关系,并且在该虚拟地址之后再没有当前进程的线性区vma的时候,可以肯定这是一个编码错误,这将杀掉该进程;
2、当MMU中确实没有创建虚拟页物理页映射关系,并且在该虚拟地址之后存在当前进程的线性区vma的时候,这很可能是缺页异常,并且可能是栈溢出导致的缺页异常;
3、当使用malloc/mmap等希望访问物理空间的库函数/系统调用后,由于linux并未真正给新创建的vma映射物理页,此时若先进行写操作,将如上面的2的情况产生缺页异常,若先进行读操作虽也会产生缺页异常,将被映射给默认的零页(zero_pfn),等再进行写操作时,仍会产生缺页异常,这次必须分配物理页了,进入写时复制的流程;
4、当使用fork等系统调用创建子进程时,子进程不论有无自己的vma,“它的”vma都有对于物理页的映射,但它们共同映射的这些物理页属性为只读,即linux并未给子进程真正分配物理页,当父子进程任何一方要写相应物理页时,导致缺页异常的写时复制。
4.用户代码和操作系统代码是如何在cup上面运行的
4.1操作系统运行用户程序?
- 计算机的代码,都是机器码,都由CPU一条一条地执行的,无论是应用程序的代码,还是操作系统的代码,都是机器码,CPU都用一样的方法来执行,这个问题和内核态还是用户态无关。所以,没有“操作系统运行用户程序”的说法。所有的软件指令,都被CPU用一样的方法执行。
4.2为什么同是代码,操作系统程序会比一般程序拥有更高的权限?
- 权限是CPU制造的,CPU只要能保证权限的穿越是单向的,就可以赋予操作系统(的代码)特殊的权限。比如ARM64 CPU启动的时候,(在特定的设计下),工作在权限EL1,这时开始执行的代码就是操作系统的代码了,操作系统执行够了以后,主动把CPU权限降低到EL0(EL1的时候你有权降级,但反过来你就没有权利),这之后执行的代码就在所谓的“用户态"了,由于CPU工作在EL0状态,这些代码的权限就很低,这时如果你执行一个权限比较高的指令(比如访问SCTLR_EL1.A寄存器,又比如x86的int,ARM的SC),CPU就会报错(报错的结果是把权限切换回EL1,并且直接调用操作系统设置好的代码,这样控制权仍是操作系统的),把用户程序的控制权强行取走,赋予给操作系统,这就是为什么操作系统(表现出来)比用户程序拥有更高的权限。
- 所谓操作系统,用户程序,系统服务,都是我们基于CPU权限人为制造的概念,并非必须存在的客观实体。
4.3代码是如何成为操作系统的?
- CPU开始加电了,完成内部必要的处理后,就可以从指定的内存地址开始执行代码,这个内存地址称为Reset向量,在一些CPU上是固定的,在一些CPU上是可以根据特定的条件变化的(比如把CPU连入电路的时候,给某个引脚加高电平等),但无论如何,反正最终CPU会在某个固定的位置开始执行程序。什么代码放在这里,什么代码就具有最高的执行权限。
- 在DOS+x86的时代,硬件设计者会在这个内存位置上固定焊一个ROM,然后在计算机出厂的时候,固定在这个ROM上烧一段程序,这个程序就称为BIOS,所以,CPU加电后,首先进入的是BIOS程序,然后有BIOS程序根据你是否有磁盘,从磁盘的指定位置上读入代码来执行。
- 所以你要成为操作系统,你就要把你的代码放到磁盘的指定位置,这样这个代码就会具备“操作系统”的控制权。
4.4现代的计算机CPU是如何加载操作系统代码的?
- 现代更复杂的计算机,硬件加电的时候,根本还没有给主CPU加电,而是某个MCU(小CPU)首先获得系统的控制权,这个MCU就可以直接设置固定的内存内的内容,让CPU到时执行这段代码,这样,其实真正具有系统的把控权的是这个MCU,它的代码才是系统的“固件”。
- 如果是多核CPU,CPU加电的时候仅启动一个核,完成前期的大部分初始化(比如安全操作系统和虚拟机调度器的初始化),然后进入EL1,完成主CPU的初始化,在这个主CPU初始化的时候,准备其他CPU的Reset向量的内容,完成准备后,控制硬件给其他CPU加电,这样其他CPU也在操作系统的控制下投入运行。整个系统就都在控制之下了。