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