前文提到过,操作系统通过虚拟化CPU技术,提供了多个CPU的假象。要实现CPU的虚拟化,操作系统就需要一些低级**【机制】和高级【策略】**。本文主要谈谈,进程运行的一些机制。
虚拟化
为了虚拟化CPU,操作系统需要以某种方式让许多任务共享物理CPU,让他们看起来是同时运行。
基本思想:运行一个进程一段时间,然后运行另外一个进程一段时间,如此轮换。通过这种时分共享CPU,就实现了虚拟化。
如图是一个最简单的时分共享示意图,不考虑调度策略。进程A、B、C在一个时间段内各自运行一小段时间。
理想和丰满,现实很骨感。然而在构建这样的虚拟化机制时,存在一些挑战。如何高效、可控的虚拟化CPU?
即在不增加系统开销情况下,有效的运行程序,同时保留对CPU的控制。
进程执行步骤
无限制直接运行
为了使程序尽可能快的运行,操作系统开发人员想出了一种技术:无限制的直接执行技术。直接在CPU上运行程序(没有任何限制)。
看起来很简单,但是这种方法产生了一些问题。
- 问题1:如果我们只运行一个程序,操作系统怎么能确保程序不做任何我们不希望它做的事?
- 问题2:当我们运行一个进程时,操作系统如何停下来并切换到另一个进程,从而实现时分共享?
受限的运行
直接执行的明显优势是快速。程序直接在硬件CPU上运行,因此执行速度与预期一样快。
但是,如果希望进程希望执行某种受限制的操作,该怎么办?
一个进程必须能够执行I/O 和其他一些受限制的操作,但又不能让进程完全控制系统。操作系统和硬件如何协作实现这一点?
答案是采用受保护的控制转移:【用户模式】和【内核模式】
硬件通过提供不同的执行模式来协作操作系统。在用户模式下,应用程序不能完全访问硬件资源,在用户模式下的代码会受限制。在内核模式下,操作系统可以访问机器的全部资源。
另外操作系统还提供了陷入(trap)内核和从内核返回(return from trap),以及一些指令,让操作系统告诉硬件陷阱表(trap table)在内存中的位置。
如何用户希望执行某种特权操作(发出I/O请求等)。又该如何呢?
几乎所有的现代硬件都提供了执行系统调用的能力。通过系统调用,可以允许应用程序访问某些关键功能。
要执行系统调用,程序必须执行特殊的陷阱(trap)指令。
- 该指令同时跳入内核并将特权级别提升到内核模式。
- 一旦进入内核,系统就可以执行任何需要的特权操作(如果允许),从而为调用进程执行所需要的工作。
- 完成后,操作系统调用一个特殊的从陷阱返回(return-from-trap)指令。返回到发起调用的应用程序中。同时将特权级别降低,回到用户模式。
我们在回过头再看这张经典的GNU/Linux 图,是不是理解更深,应用和内核的交互本质上是通过系统调用。有关系统调用过程分析可以见旧文。
为什么系统调用看系统像C语言的过程调用一样,因为有关汇编部分的指令操作在glibc中已经帮我们封装好了。事实上我们调用一个系统调用实际上是调用了syscal这条指令。
受限制直接运行策略如下,这里其实可以看出。main函数事实上不是程序的入口。
进程间如何切换
直接运行的另一个问题就是如何实现进程间的切换。在进程间切换,无非就是操作系统应该决定停止一个进程并开始另一个进程。这看起来很简单,但是事实上,当一个进程在CPU上运行,这就意味着操作系统没有运行,如果操作系统没有运行,那么显然是没有办法采取行动的。
那么操作系统是如何重获CPU的控制权的?
- 协作方式:等待系统调用。在一些古老的系统中。OS通过等待系统调用,或某种非法操作,从而重新获取CPU的控制权。(容易进入无限循环)
- 非协作方式:操作系统进行控制。利用【时钟中断】重新获取控制权。
时钟中断:时钟设备每隔几毫秒产生一个中断。当产生中断时,当前正在运行的程序停止,操作系统预先配置的中断处理程序(interrupt
handler)会运作。
通过时钟中断,操作系统可以重新获得CPU的控制权,从而停止当前进程,并启动另一个进程。
当然当操作系统重新获取CPU的控制权后,必须决定是继续运行当前进程还是切换到另一个进程。这个策略是由【调度】决定的。这又是另一个话题了。
如图展示了整个进程切换的时间线:
注意:在这种方式中,有两种类型的寄存器保存/恢复。
- 第一种是发生时钟中断的时候,在这种情况下,运行进程的用户寄存器由硬件隐士保存,使用该进程内核栈。
- 第二种是当操作系统决定从A切换B。在这种情况下,内核寄存器被软件(OS)明确的保存,这次被存储在该进程的进程结构的内存中。