《操作系统导论》第6章 | 受限直接执行

本章描述了一些实现CPU虚拟化的关键底层机制,并将其统称为受限直接执行(limited direct execution)。其基本思路很简单:让程序在CPU上运行,但首先确保设置好硬件,以便在没有操作系统帮助的情况下限制进程可以执行的操作。

直接执行

为了虚拟化CPU,操作系统需要以某种方式让许多任务共享物理CPU,让它们看起来像是同时运行。基本思想就是运行一个进程一段时间,然后运行另一个进程,如此轮换。通过以这种方式时分共享CPU,就实现了虚拟化。然而,在构建这样的虚拟化机制时存在一些挑战。第一个是性能:如何在不增加系统开销的情况下实现虚拟化?第二个是控制权:如何有效地运行进程,同时保留对CPU的控制?控制权对于操作系统尤为重要,因为操作系统负责资源管理。如果没有控制权,一个进程可以简单地无限制运行并接管机器,或访问没有权限的信息。因此,在保持控制权的同时获得高性能,这是构建操作系统的主要挑战之一。

为了使程序尽可能快地运行,操作系统开发人员想出了一种技术——受限制的直接执行。这个概念的“直接执行”部分很简单:只需直接在CPU上运行程序即可。因此,当OS希望启动程序运行时,它会在进程列表中为其创建一个进程条目,为其分配一些内存,将程序代码从磁盘加载到内存中,找到程序入口,跳转到那里,并开始运行用户的代码。下图展示了这种基本的直接执行协议(没有任何限制),使用正常的调用并返回跳转到程序的main(),并在稍后回到内核。

但是,这种方法在虚拟化CPU时会产生一些问题。如果我们只运行一个程序,操作系统怎么能确保程序不做任何我们不希望它做的事,同时仍然高效地运行它?当我们运行一个进程时,操作系统如何让它停下来并切换到另一个进程,从而实现虚拟化CPU所需的时分共享?

受限制的操作

一个进程必须能够执行I/O和其他一些受限制的操作,但又不能让进程完全控制系统。因此,我们采用的方法是引入一种新的处理器模式,称为用户模式(user mode)。在用户模式下运行的代码会受到限制。例如,在用户模式下运行时,进程不能执行某些受限制的指令,否则会导致处理器引发异常,操作系统可能会终止该进程。与用户模式相对应的是内核模式(kernel mode)。在此模式下,运行的代码可以做它喜欢的事,包括一些特权操作,操作系统(或内核)就以这种模式运行。

如果用户希望执行某种特权操作(例如从磁盘读取),那应该怎么做?为了实现这一点,几乎所有的现代硬件都提供了用户程序执行系统调用的能力。要执行系统调用,程序必须执行特殊的陷阱(trap)指令。该指令跳入内核并将特权级别提升到内核模式。一旦进入内核,系统就可以执行任何需要的特权操作(如果允许),从而为调用进程执行所需的工作。完成后,操作系统调用一个特殊的从陷阱返回(return-from-trap)指令,该指令返回到发起调用的用户程序中,同时将特权级别降低,回到用户模式。

硬件通过提供不同的执行模式来协助操作系统。在用户模式下,应用程序不能完全访问硬件资源。在内核模式下,操作系统可以访问机器的全部资源。还提供了陷入内核和从陷阱返回到用户模式程序的特别说明,以及一些指令,让操作系统告诉硬件陷阱表(trap table)在内存中的位置。执行陷阱时,硬件需要小心,因为它必须确保存储足够的调用者寄存器,以便在操作系统发出从陷阱返回指令时能够正确返回。例如,在x86上,处理器会将程序计数器、标志和其他一些寄存器推送到每个进程的内核栈上。从返回陷阱将从栈弹出这些值,并恢复执行用户模式程序。

操作系统在启动时会设置陷阱表,以便告诉硬件在发生某些异常事件时要运行哪些代码。操作系统通常通过某种特殊的指令,通知硬件这些陷阱处理程序的位置。一旦硬件被通知,它就会记住这些处理程序的位置,直到下一次重新启动机器,并且硬件知道在发生系统调用和其他异常事件时要跳转到哪段代码。

受限直接执行协议有两个阶段。第一个阶段(在系统引导时),内核初始化陷阱表,并且CPU记住它的位置以供随后使用。内核通过特权指令来执行此操作(所有特权指令均以粗体显示)。第二个阶段(运行进程时),在使用从陷阱返回指令开始执行进程之前,内核设置了一些内容,这会将CPU切换到用户模式并开始运行该进程。当进程希望发出系统调用时,它会重新陷入操作系统,然后再次通过从陷阱返回,将控制权还给进程。该进程然后完成它的工作,并从main()返回。这通常会返回到一些存根代码,它将正确退出该程序。随后,操作系统清理现场,任务完成。

进程间切换

接下来我们面临的一个关键问题是,操作系统如何重新获得CPU的控制权,以便它可以在进程之间切换?

协作方式:等待系统调用

过去某些系统采用协作方式。在这种风格下,操作系统相信系统的进程会合理运行。运行时间过长的进程被假定会定期放弃CPU,以便操作系统可以决定运行其他任务。然而,在协作方式中,当进程陷入无限循环时,唯一的办法就是重新启动计算机。

非协作方式:操作系统进行控制

如果进程不协作,操作系统可以通过时钟中断获得CPU的控制权。时钟设备可以编程为每隔几毫秒产生一次中断。产生中断时,当前正在运行的进程停止,操作系统中预先配置的中断处理程序会运行。此时,操作系统重新获得CPU的控制权,因此可以停止当前进程,并启动另一个进程。需要注意的是,操作系统在启动时必须通知硬件哪些代码在发生时钟中断时运行。在启动过程中,操作系统也必须启动时钟(特权操作)。一旦时钟开始运行,操作系统就感到安全了,因为控制权最终会归还给它,因此操作系统可以自由运行用户程序。时钟也可以关闭(也是特权操作)。硬件在发生中断时有一定的责任,尤其是在中断发生时,要为正在运行的程序保存足够的状态,以便随后从陷阱返回指令能够正确恢复正在运行的程序。这一组操作与硬件在显式系统调用陷入内核时的行为非常相似,其中各种寄存器因此被保存(进入内核栈),因此从陷阱返回指令可以容易地恢复。

保存和恢复上下文

操作系统已经重新获得CPU控制权后,必须决定是继续运行当前正在运行的进程,还是切换到另一个进程。这个决定是由调度程序做出的,它是操作系统的一部分。如果决定进行切换,OS就会执行一些底层代码,即所谓的上下文切换(context switch)。简单来讲,操作系统在上下文切换时要做的就是为当前正在执行的进程保存一些寄存器的值,并为即将执行的进程恢复一些寄存器的值。这样一来,操作系统就可以确保最后执行从陷阱返回指令时,不是返回到之前运行的进程,而是继续执行另一个进程。为了保存当前正在运行的进程的上下文,操作系统会执行一些底层汇编代码,来保存通用寄存器、程序计数器,以及当前正在运行的进程的内核栈指针,然后恢复寄存器、程序计数器,并切换内核栈,供即将运行的进程使用。通过切换栈,内核在进入切换代码调用时,是一个进程(被中断的进程)的上下文,在返回时,是另一进程(即将执行的进程)的上下文。当操作系统最终执行从陷阱返回指令时,即将执行的进程变成了当前运行的进程。

在此协议中,有两种类型的寄存器保存/恢复。第一种是发生时钟中断的时候,此时运行进程的用户寄存器由硬件隐式保存,使用该进程的内核栈。第二种是当操作系统决定从A切换到B,内核寄存器将被操作系统明确地保存,但这次被存储在该进程的进程结构的内存中。后一个操作让系统从好像刚刚由A陷入内核,变成好像刚刚由B陷入内核。为了更好地了解如何实现这种切换,下面给出了xv6的上下文切换代码。context结构oldnew分别在老的和新的进程的进程结构中。

# void swtch(struct context **old, struct context *new);
#
# Save current register context in old
# and then load register context from new.
.globl swtch
swtch:
  # Save old registers
  movl 4(%esp), %eax # put old ptr into eax
  popl 0(%eax)        # save the old IP
 movl %esp, 4(%eax) # and stack
 movl %ebx, 8(%eax) # and other registers
 movl %ecx, 12(%eax)
 movl %edx, 16(%eax)
 movl %esi, 20(%eax)
 movl %edi, 24(%eax)
 movl %ebp, 28(%eax)

 # Load new registers
 movl 4(%esp), %eax # put new ptr into eax
 movl 28(%eax), %ebp # restore other registers
 movl 24(%eax), %edi
 movl 20(%eax), %esi
 movl 16(%eax), %edx
 movl 12(%eax), %ecx
 movl 8(%eax), %ebx
 movl 4(%eax), %esp  # stack is switched here
 pushl 0(%eax)       # return addr put in place
 ret                 # finally return into new ctxt
原文地址:https://www.cnblogs.com/littleorange/p/12736927.html