为了虚拟化CPU,操作系统需要以某种方式共享物理CPU,在许多工作中同时运行。基本的想法很简单:运行一个进程一段时间,然后运行另一个进程,等等。通过这种方式共享CPU,实现了虚拟化。然而,在构建这样的虚拟化机制方面存在一些挑战,然而,在构建这样的虚拟化机制方面存在一些挑战。第二个是控制:如何有效地运行流程,同时保留对CPU的控制。控制对操作系统尤其重要,因为它负责资源;如果没有控制,进程可能会永远运行并接管机器,或者访问不允许访问的信息。因此,在维护控制的同时获得高性能是构建操作系统的主要挑战之一。
症结:如何有效地使用控件虚拟化CPU ?操作系统必须以一种有效的方式虚拟化CPU,同时保持对系统的控制。要做到这一点,就需要硬件和操作系统的支持。为了有效地完成工作,操作系统通常会使用一个明智的硬件支持。
6.1基本技术:有限制的直接执行。
为了让程序运行得和人们预期的一样快,毫不奇怪,OS开发人员提出了一种技术,我们称之为有限制的直接执行。这个想法的直接执行部分很简单:直接在CPU上运行程序。因此,当操作系统希望启动一个程序时,它在进程列表中为其创建一个进程条目,为它分配一些内存。将程序代码加载到内存中(从磁盘)。定位其入口点(即,main()例程或类似的东西。跳进他。并开始运行用户的代码。图6.1显示了这个基本的直接执行协议(没有任何限制)。使用普通的调用和返回来跳转到程序的main(),然后返回到内核。
听起来很简单,不是吗?但是这种方法在我们追求虚拟化CPU的过程中产生了一些问题。第一个问题很简单:如果我们运行一个程序,操作系统如何确保程序不执行我们不希望它做的事情,而仍然有效地运行它呢?第二步:当我们运行一个进程时,操作系统如何阻止它运行并切换到另一个进程,从而实现我们需要虚拟化CPU的时间共享。
在回答下面这些问题时,我们将更好地理解虚拟化CPU需要什么。在开发这些技术时,我们还将看到名称的有限部分来自何处;如果没有对运行程序的限制,操作系统将无法控制任何东西,因此对于一个有抱负的操作系统来说,这只是一个非常可悲的状态。
问题# 1:限制操作
直接执行具有较快的明显优势;程序在硬件CPU上本地运行,因此执行速度和预期一样快。但是在CPU上运行会带来一个问题:如果进程希望执行某种受限操作,比如向磁盘发出I/O请求,或者获得更多的系统资源,比如CPU或内存。
关键:如何执行受限操作。一个进程必须能够执行I/O和其他一些受限操作,但不需要对系统进行完全控制。操作系统和硬件如何协同工作?
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
您可能想知道为什么调用一个系统调用,比如open()或read(),看起来就像C中的一个典型的过程调用;也就是说,如果它看起来像一个过程调用,系统如何知道它是一个系统调用,并做所有正确的事情。原因很简单:它是一个过程调用,但隐藏在过程调用中是著名的陷阱指令。更具体地说,当您调用open()(例如)时,您正在执行一个对C库的过程调用。其中,无论是对open()或任何其他的系统调用,图书馆与内核使用一个商定的调用协定将在著名的参数位置(如在堆栈上,或在特定寄存器),将系统调用号放入一个众所周知的位置(再次压入堆栈或寄存器),然后执行上述指令。库中的代码将返回值,并将控制权返回给发出系统调用的程序。因此,C库中使系统调用的部分是手工编码的,因为它们需要仔细地遵循约定,以便正确处理参数和返回值,以及执行特定于硬件的trap指令。现在你知道为什么你自己不需要编写汇编代码来进入操作系统了;有人已经为你写过了。
一种方法就是让任何进程按照I/O和其他相关操作来做它想做的事情。:然而,这样做会阻碍许多需要的系统的建设。例如,如果我们希望构建一个文件系统,在授予对文件的访问权限之前检查权限,那么我们不能简单地让任何用户进程向磁盘发送I/Os。如果我们这样做了,一个进程可以简单地读取或写入整个磁盘,这样所有的保护都将丢失。因此,我们采用的方法是引入一种新的处理器模式,即用户模式;在用户模式下运行的代码会受到限制。例如,在用户模式下运行时,进程不能发出I/O请求;这样做会导致处理器引发异常;操作系统很可能会扼杀这个过程。与用户模式相反的是内核模式,操作系统(或内核)运行的是内核模式。在这种模式下,运行的代码可以执行它喜欢的操作,包括特权操作,比如发出I/O请求和执行所有类型的受限指令。然而,我们仍然面临着一个挑战:当用户希望执行某种特权操作时(比如从磁盘读取)应该如何处理。为了实现这一点,几乎所有现代硬件都提供了用户程序执行系统调用的能力。在像Atlas [K+61,L78]这样的古老机器上,系统调用允许内核仔细地向用户程序公开某些关键的功能部件。例如访问文件系统、创建和销毁进程。与其他进程,和分配更多内存。大多数操作系统都提供几百个调用(详细情况参见POSIX标准[P10]);早期的Unix系统公开了大约20个调用的更简洁的子集。要执行系统调用,程序必须执行特殊的陷阱指令。该指令同时跳转到内核,并将特权级别提升到内核模式。在内核中,系统现在可以执行所需的任何特权操作(如果允许),从而完成调用过程所需的工作。当完成时,操作系统调用一个特殊的“返回-陷阱”指令,正如您所期望的那样,它将返回到调用用户程序,同时将特权级别降低到用户模式。在执行一个陷阱时,硬件需要小心谨慎,因为它必须确保保存足够的调用者的寄存器,以便在操作系统发出返回陷阱指令时能够正确地返回。例如,在x86上,处理器会将程序计数器、标志和其他一些寄存器推到每个进程的内核堆栈上。将这些值从堆栈中取出并重新执行程序(参见Intel systems手册[I11])。其他硬件系统使用不同的约定,但是基本概念在不同的平台上是相似的。这个讨论遗漏了一个重要的细节:陷阱如何知道在操作系统中运行哪些代码。显然,调用进程不能指定要跳转到的地址(就像在进行过程调用时那样)。这样做将允许程序跳转到内核中,这显然是一个非常糟糕的想法。因此,内核必须小心地控制在一个陷阱上执行的代码。内核通过在引导时设置一个trap表来实现这一点。当机器启动时,它会以特权(内核)模式运行,因此可以根据需要自由配置机器硬件。操作系统首先要做的事情之一就是告诉硬件在某些异常事件发生时要运行什么代码。例如,当一个硬盘中断发生时,当一个键盘中断发生,或者程序发出一个系统调用时,应该运行什么代码。操作系统通知这些陷阱处理程序的位置的硬件。通常有一些特殊的结构。一旦硬件被告知,它就会记住这些处理器的位置,直到机器被重新启动,因此硬件知道该做什么(也就是)。
当系统调用和其他异常事件发生时,需要跳转到什么代码。要指定确切的系统调用,系统调用编号通常被分配给每个系统调用。因此,用户代码负责将所需的系统调用编号放置在寄存器中或堆栈的指定位置上;操作系统在处理陷阱处理程序中的系统调用时,检查这个数字,确保它是有效的,如果是,则执行相应的代码。这种间接的程度是一种保护的形式;用户代码不能指定要跳转到的确切地址,而是必须通过数字请求特定的服务。要指定确切的系统调用,系统调用编号通常被分配给每个系统调用。因此,用户代码负责将所需的系统调用编号放置在寄存器中或堆栈上的指定位置上。操作系统在处理陷阱处理程序中的系统调用时,检查这个数字,确保它是有效的,如果是,则执行相应的代码。这种间接的程度是一种保护的形式;用户代码不能指定要跳转到的确切地址,而是必须通过数字请求特定的服务。最后一个问题是:能够执行指令来告诉硬件哪里的trap表是一个非常强大的功能。因此,正如您可能已经猜到的,它也是一种特权操作。硬件不会让你这么做的。猜猜会发生什么(提示:adios,违规程序?思考要点:如果你可以安装自己的陷阱表,你会对一个系统做什么可怕的事情?你能接管这台机器吗?时间轴(随着时间的增加,在图6.2中)总结了协议。我们假定每个进程都有一个内核堆栈,其中寄存器(包括通用寄存器和程序计数器)在转换进出内核时从(由硬件)保存并恢复。在有限的直接执行(LDE)协议中有两个阶段。在第一个(在引导时),内核初始化了trap表,并且CPU记住了它的位置以便以后使用。内核通过特权指令进行操作(所有特权指令都以粗体显示)。在第二个(运行一个进程)中,内核设置了一些东西(例如,在进程列表上分配一个节点,分配内存),然后使用返回-trap指令来启动进程的执行;这将把CPU切换到用户模式,并开始运行进程。当进程希望发出一个系统调用时,它会将其捕获到操作系统中,操作系统会处理它,并再次通过从陷阱到进程的返回来返回控制。然后流程完成其工作,并从main返回。通常会返回到一些存根代码中,这些代码将正确地退出程序(比如,调用exit()系统调用,将其捕获到OS中)。在这一点上,操作系统会清理,我们就完成了。