操作系统的一个任务是虚拟化CPU,让每个进程以为自己在独占CPU。
现代操作系统采用分时的方式来完成这个工作,一个进程获得CPU,运行一段时间,另一个进程再获得CPU去运行,这些进程不断的切换,从而达到让一个物理CPU虚拟给多个进程的目的
但这其中有很多问题
如何限制进程访问资源——用户模式/内核模式
在上面的模型下,进程直接在物理CPU上运行,那么如何确保它不会访问它不应该访问的资源,比如访问其它程序的内存地址,越过操作系统提供的文件系统直接访问磁盘中的数据(从而达到文件的越权访问)...
由于进程直接运行在物理CPU上,所以单靠操作系统是不足以限制进程的,因为进程运行在CPU上时,就意味着操作系统当前没有运行。
所以,CPU硬件需要提供某种机制来与操作系统协作,来限制进程的行为。
CPU提供用户模式和内核模式两种模式,所有的用户进程在用户模式下运行,它们不能随意发起IO,不能随意访问内存...操作系统或内核往往在内核模式下运行,它们能做任何的操作。
那当进程又切实的有对应需求咋办,比如它要访问一个文件,要申请更多的内存...这时,操作系统提供一系列类似函数库的系统调用,用户进程调用系统调用,控制权转移到操作系统内核并将权限提升到内核模式,操作系统来帮助用户进程完成想要的功能,然后再将控制权转移回用户进程,将权限降为用户模式。
下面两个图是流程的简单抽象
如何将控制权转移到内核?—— 陷阱指令
如果单单把系统调用理解为库函数的话,当进程调用系统调用时,那也是在用户空间执行一个普通函数,CPU也就没法区别对待这类系统调用和普通用户函数。
实际上,系统调用中都包含一个隐藏指令——trap
陷阱指令。
陷阱指令是一类指令的统称,它在用户模式下被调用,调用时需要给它提供一个类似ID的东西,这个ID对应了一个操作系统内核中的例程,CPU硬件可以通过这个ID找到应该执行内核中的哪一个例程,并且将用户模式提升为内核模式,去执行这个例程。
所以,可以看作系统调用是对应的内核函数的一个包装,它是一个接口,以用于陷入对应的内核函数。
如何确保陷阱指令的安全性?—— 陷阱表
从上面的描述中,我们知道,这个系统调用也是操作系统和硬件合作完成的,至少,硬件需要知道陷阱指令的ID所对应的系统例程,它才能去找到并执行对应的系统例程。
那我能不能告诉CPU一个自定义的ID,然后把它引导到我自己编写的例程上,这样就能以内核模式来运行我自己的例程了!
这就是在想桃子,内核在启动时会设置一个陷阱表,陷阱表中就包含了ID到例程的映射,而这个陷阱表的设置,只能在内核模式下执行。
在/usr/include/x86_64-linux-gnu/asm/unistd_64.h
下,可以看到系统调用以及对应的陷阱表ID。
拓展:中断、异常
中断和异常可以看作是强制性的执行流的转移,CPU从当前正在执行的程序转移到另一个特殊的例程或任务,这个例程或任务就叫中断或异常处理例程,当它执行完毕后,处理器可能会根据情况继续执行之前的程序或停止之前的程序。
就有点像刚刚说的陷阱嘛,实际上,陷阱是上面所说的内容的一个子集,Linux把执行流转移分为四种,并且统称为异常:
CPU在很多时候需要转移执行流程,比如:
- 中断:CPU曾向外部IO设备发送了一个请求,它干完活了,通过中断通知CPU处理它的工作成果,这时CPU放下手头的活,通过相应的中断处理例程去处理
- 陷阱:程序主动调用系统例程的一个方式
- 故障/终止:当前程序的执行发生了某些异常,CPU需要调用相应的异常处理例程来处理该异常
当然,如果按照其它的分类方式,我们还可以分什么外中断、内中断、可屏蔽中断、不可屏蔽中断等等,懂得比较少,我就不露怯了,这里只需要知道,不管是中断还是陷阱还是异常(因为这三个词总是会出现),它们都是为了让CPU将当前执行流程转移到其它的例程上的一个手段。
中断是异步的,因为IO设备随时可能发起中断,一般的实现是提供一些引脚做中断标志位,CPU每执行完一个指令就去判断下引脚电位,看是否有中断发生。而陷阱和故障都是同步的。
保存调用者寄存器
CPU中的寄存器是有限的,进程依赖寄存器做取指、计算等操作,当通过陷阱调用到系统例程时,我们必须把当前进程的寄存器给保存起来,然后才能让系统例程在CPU上运行。
在x86上,处理器会将程序计数器,标志和一些其它的寄存器推送到每个进程的内核栈上(一个进程具有一个用户栈和一个内核栈),然后执行对应的系统例程,当从陷阱返回时,处理器需要弹出这些值还原用户程序停止执行时的寄存器结构。
总结
- 通过操作系统和硬件结合,并且提供用户模式和内核模式的权限分离,可以限制用户程序对系统资源的访问。
- 用户对系统资源进行访问时,只能通过系统调用的方式,有了这一层,系统可以对用户程序对资源的访问做任何形式的限制。
- 系统调用通过
trap
陷入内核,然后以内核模式执行内核中的系统调用例程,以让进程对资源以受限于操作系统限制的方式进行访问。
如何切换进程?—— 时钟中断
解决了如何限制进程访问系统资源的问题之后,想这个问题。
如果让用户进程直接运行在真实的CPU上,而且你还想通过分时的方式让所有进程都能在一段时间内得到调用,谁来把用户进程从CPU上踢出去?操作系统肯定做不了这个事儿,因为当前CPU在执行用户进程,操作系统得不到执行。
时钟设备可以用于解决这个问题,它以固定的频率形成时钟脉冲,你可以理解为以固定的频率发生系统中断,就像前面提到的,中断就是强制性的流程转移,操作系统在启动时启动一个时钟,之后CPU就会以固定的频率转移到它预先设定好的中断处理例程上,也就是CPU总会回到操作系统手中。
这里有两个寄存器保存的步骤:
- 硬件将进程寄存器保存到进程内核栈
- 操作系统(软件)将进程寄存器保存到进程的进程结构中
操作系统的保存寄存器和载入另一个进程的寄存器的操作称为上下文切换。