在四章中已经知道,要实现 CPU 的虚拟化,操作系统就需要一些低级机制以及一些高级智能。
我们将低级机制称为机制(mechanism)。机制是一些低级方法或协议,实现了所需的功能。
这一章我们介绍一种机制:受限直接执行
构建虚拟化时存在一些挑战:
- 性能:如何在不增加系统开销的情况下实现虚拟化?
- 控制权:如何有效地运行进程,同时保留对 CPU 的控制?
控制权对于操作系统尤为重要,因为操作系统负责资源管理。如果没有控制权,一个进程可以简单地无限制运行并接管机器,或访问没有权限的信息
直接运行协议
这个概念的“直接执行”部分很简单:只需直接在 CPU上运行程序即可。
下表展示了这种直接运行协议:
当 OS 希望启动程序运行时,它会在进程列表中为其创建一个进程条目,为其分配一些内存,将程序代码(从磁盘)加载到内存中,找到入口点(main()函数或类似的),跳转到那里,并开始运行用户的代码。
这种方法会产生一些问题:
- 受限制的操作:操作系统怎么能确保程序不做任何我们不希望它做的事,同时仍然高效地运行它?
- 进程间切换: 操作系统如何让一个进程停下来并切换到另一个进程,从而实现虚拟化 CPU 所需的时分共享
问题 1:受限制的操作
采用受保护的控制权转移 硬件通过提供不同的执行模式来协助操作系统。在用户模式(user mode)下,应用程序不能完全访问硬件资源。在内核模式(kernel mode)下,操作系统可以访问机器的全部资源。还提供了陷入(trap)内核和从陷阱返回(return-from-trap)到用户模式程序的特别说明,以及一些指令,让操作系统告诉硬件陷阱表(trap table)在内存中的位置。
我们采用的方法是引入新的处理器模式:用户模式(user mode):在用户模式下运行的代码会受到限制。例如,在用户模式下运行时,进程不能发出 I/O 请求。这样做会导致处理器引发异常,操作系统可能会终止进程。
与用户模式不同的是内核模式(kernel mode):操作系统(或内核)就以这种模式运行。在此模式下,运行的代码可以做它喜欢的事,包括特权操作,如发出 I/O 请求和执行所有类型的受限指令。
几乎所有的现代硬件都提供了用户程序执行系统调用的能力,从而使用户执行某种特权操作(如从磁盘读取)。
系统调用:允许内核小心地向用户程序暴露某些关键功能,例如访问文件系统、创建和销毁进程、与其他进程通信,以及分配更多内存。
要执行系统调用,程序必须执行特殊的陷阱(trap)指令。该指令同时跳入内核并将特权级别提升到内核模式。一旦进入内核,系统就可以执行任何需要的特权操作(如果允许),从而为调用进程执行所需的工作。完成后,操作系统调用一个特殊的从陷阱返回(return-from-trap)指令,如你期望的那样,该指令返回到发起调用的用户程序中,同时将特权级别降低,回到用户模式。
执行陷阱时,硬件需要小心,因为它必须确保存储足够的调用者寄存器,以便在操作系统发出从陷阱返回指令时能够正确返回。例如,在 x86 上,处理器会将程序计数器、标志和其他一些寄存器推送到每个进程的内核栈(kernel stack)上。从返回陷阱将从栈弹出这些值,并恢复执行用户模式程序。
内核通过在启动时设置陷阱表(trap table)来实现如何知道在 OS 内运行哪些代码。
当机器启动时,系统在特权(内核)模式下执行,因此可以根据需要自由配置机器硬件。操作系统做的第一件事,就是告诉硬件在发生某些异常事件时要运行哪些代码。例如,当发生硬盘中断,发生键盘中断或程序进行系统调用时,应该运行哪些代码?操作系统通常通过某种特殊的指令,通知硬件这些陷阱处理程序的位置。一旦硬件被通知,它就会记住这些处理程序的位置,直到下一次重新启动机器,并且硬件知道在发生系统调用和其他异常事件时要做什么(即跳转到哪段代码)。
受限直接运行协议
LDE 协议有两个阶段:
第一个阶段(在系统引导时),内核初始化陷阱表,并且 CPU 记住它的位置以供随后使用。内核通过特权指令来执行此操作(所有特权指令均以粗体突出显示)。
第二个阶段(运行进程时),在使用从陷阱返回指令开始执行进程之前,内核设置了一些内容(例如,在进程列表中分配一个节点,分配内存)。这会将 CPU 切换到用户模式并开始运行该进程。当进程希望发出系统调用时,它会重新陷入操作系统,然后再次通过从陷阱返回,将控制权还给进程。该进程然后完成它的工作,并从 main()返回。这通常会返回到一些存根代码,它将正确退出该程序(例如,通过调用 exit()系统调用,这将陷入 OS 中)。此时,OS 清理干净,任务完成了。
问题 2:在进程之间切换
关键问题:如何重获 CPU 的控制权 操作系统如何重新获得 CPU 的控制权(regain control),以便它可以在进程之间切换?
协作方式:等待系统调用
过去某些系统采用协作方式。在这种风格下,操作系统相信系统的进程会合理运行。运行时间过长的进程被假定会定期放弃CPU,以便操作系统可以决定运行其他任务。
在协作调度系统中,OS 通过等待系统调用,或某种非法操作发生,从而重新获得 CPU 的控制权。
非协作方式:时钟中断
时钟设备可以编程为每隔几毫秒产生一次中断。产生中断时,当前正在运行的进程停止,操作系统中预先配置的中断处理程序(interrupt handler)会运行。此时,操作系统重新获得 CPU 的控制权,因此可以做它想做的事:停止当前进程,并启动另一个进程。
硬件在发生中断时有一定的责任,尤其是在中断发生时,要为正在运行的程序保存足够的状态,以便随后从陷阱返回指令能够正确恢复正在运行的程序。这一组操作与硬件在显式系统调用陷入内核时的行为非常相似,其中各种寄存器因此被保存(进入内核栈),因此从陷阱返回指令可以容易地恢复。
保存和恢复上下文
当操作系统通过上述两种方式获取控制权后,就可以决定是否切换进程,这个决定是由调度程序(scheduler)做出。
当操作系统决定切换进程时,需要首先进行上下文切换(context switch),就是为当前正在执行的进程保存一些寄存器的值(例如,到它的内核栈),并为即将执行的进程恢复一些寄存器的值(从它的内核栈)。这样一来,操作系统就可以确保最后执行从陷阱返回指令时,不是返回到之前运行的进程,而是继续执行另一个进程。
下图展示受限直接执行协议(时钟中断):
进程 A 正在运行,然后被中断时钟中断。硬件保存它的寄存器(在内核栈中),并进入内核(切换到内核模式)。在时钟中断处理程序中,操作系统决定从正在运行的进程 A 切换到进程 B。此时,它调用 switch()例程,该例程仔细保存当前寄存器的值(保存到 A 的进程结构),恢复寄存器进程 B(从它的进程结构),然后切换上下文(switch context),具体来说是通过改变栈指针来使用 B 的内核栈(而不是 A 的)。最后,操作系统从陷阱返回,恢复 B 的寄存器并开始运行它。
在此协议中,有两种类型的寄存器保存/恢复。第一种是发生时钟中断的时候。在这种情况下,运行进程的用户寄存器由硬件隐式保存,使用该进程的内核栈。第二种是当操作系统决定从 A 切换到 B。(期中考试考过)
这篇文章比较详细介绍了这两种寄存器保存/恢复