为了虚拟化CPU,操作系统需要在似乎同时运行的许多作业之间以某种方式共享物理CPU。其基本思想很简单:先运行一个进程,然后再运行另一个进程,以此类推。通过这种方式分时共享time sharingCPU,可以实现虚拟化。
然而,在构建这样的虚拟化机制时存在一些挑战。第一个是性能:我们如何实现虚拟化而不给系统增加过多的开销?第二个是控制:我们如何有效地运行进程,同时保持对CPU的控制?控制对操作系统来说尤其重要,因为它负责资源;如果没有控制,进程就可能永远运行并接管机器,或者访问不允许访问的信息。因此,在保持控制的同时获得高性能是构建操作系统的核心挑战之一

关键的问题:如何通过控制有效地虚拟化 CPU 操作系统必须以高效的方式虚拟化CPU,同时保持对系统的控制。为此,需要硬件和操作系统支持。为了有效地完成工作,操作系统通常会适当地使用一些硬件支持。

6.1 基础技术:受限的直接执行 Basic Technique: Limited Direct Execution

为了让程序运行得和预期的一样快,操作系统开发人员想出了一种技术,我们称之为受限直接执行limited direct execution。这个想法的直接执行部分很简单:直接在CPU上运行程序。因此,当操作系统希望开始一个程序运行时,它会创建一个进程进入进程列表,分配一些内存,程序代码装载到内存中(从磁盘),定位其入口点(例如,main()例程或类似的东西),跳跃,并开始运行用户代码。
图6.1显示了这个基本的直接执行协议(目前还没有任何限制),使用一个正常的调用和返回来跳转到程序的main(),然后返回到内核。
image.png
听起来很简单,不是吗?但是这种方法在我们寻求虚拟化CPU的过程中带来了一些问题。第一个很简单:如果我们只是运行一个程序,操作系统如何确保程序不会做任何我们不希望它做的事情,同时仍然有效地运行它?第二个问题:当我们运行一个进程时,操作系统如何停止它的运行并切换到另一个进程,从而实现我们需要的时间共享来虚拟化CPU?
通过回答下面的这些问题,我们将更好地了解虚拟化CPU需要什么。在开发这些技术时,我们还将看到名称的“受限”部分是从哪里来的;如果没有对运行程序的限制,操作系统将无法控制任何东西,因此对于一个有抱负的操作系统来说,这将是一个非常可悲的状态。

6.2 问题1:受限操作 Problem #1: Restricted Operations

直接执行具有明显的快速优势;该程序在硬件 CPU 上本地运行,因此执行速度与预期一样快。但是在 CPU 上运行会带来一个问题:如果进程希望执行某种受限制的操作,例如向磁盘发出 I/O 请求,或者获得更多系统资源(例如 CPU 或内存)的访问权限,该怎么办?

关键的问题: 如何执行受限操作 进程必须能够执行I/O和其他一些受限制的操作,但不能让进程完全控制系统。操作系统和硬件是如何协同工作的呢?

Aside:为什么系统调用看起来像过程调用(函数) 您可能想知道为什么对系统调用(例如 open() 或 read())的调用看起来与 C 中的典型过程调用完全一样;也就是说,如果它看起来就像一个过程调用,那么系统如何知道它是一个系统调用,并执行所有正确的操作?原因很简单:它是一个过程调用,但隐藏在该过程调用中的是著名的陷阱指令(trap instruction)。更具体地说,当您调用 open()(例如)时,您正在执行对 C 库的过程调用。其中,无论是 open() 还是提供的任何其他系统调用,库都使用与内核商定的调用约定将 open() 的参数放在众所周知的位置(例如,在堆栈上,或在特定寄存器),将系统调用号也放入一个众所周知的位置(再次放入堆栈或寄存器),然后执行上述陷阱指令。陷阱解包后,库中的代码返回值并将控制权返回给发出系统调用的程序。因此,进行系统调用的 C 库部分是用汇编手工编码的,因为它们需要仔细遵循约定以正确处理参数和返回值,以及执行特定于硬件的陷阱指令。现在您知道为什么您个人不必编写汇编代码来捕获操作系统了;有人已经为您编写了该程序集。

一种方法是让任何进程在I/O和其他相关操作方面做它想做的任何事情。然而,这样做将阻止构建许多种理想的系统。例如,如果我们希望构建一个文件系统,在授予对文件的访问权之前检查权限,我们不能简单地让任何用户处理向磁盘发出I/ o;如果我们这样做,进程可以简单地读取或写入整个磁盘,因此所有的保护都将丢失。
因此,我们采用的方法是引入一种新的处理器模式,称为用户模式user mode,也叫用户态);在用户模式下运行的代码的功能受到限制。例如,在用户模式下运行时,进程不能发出I/O请求;这样做会导致处理器引发异常;操作系统很可能会终止该进程。
与用户模式相对的是内核模式kernel mode,也叫内核态),操作系统(或内核)在内核模式中运行。在这种模式下,运行的代码可以做它想做的事情,包括特权操作,如发出I/O请求和执行所有类型的受限指令。
然而,我们仍然面临一个挑战:当用户进程希望执行某种特权操作(例如从磁盘读取)时,它应该做什么?为了实现这一点,几乎所有现代硬件都为用户程序提供了执行系统调用的能力。系统调用最早出现在Atlas [K+61,L78]等古老机器上,它允许内核小心地将某些关键功能暴露给用户程序,如访问文件系统、创建和销毁进程、与其他进程通信,以及分配更多内存。大多数操作系统提供几百个调用(详见POSIX标准[P10]);早期的Unix系统公开了一个更简洁的子集,大约有20个调用。

Tip: 使用保护控制转移(TIP: USE PROTECTED CONTROL TRANSFER) 硬件通过提供不同的执行模式来帮助操作系统。在用户模式下,应用程序不能完全访问硬件资源。在内核模式下,操作系统可以访问机器的全部资源。还提供了进入内核和从陷阱返回(return-from-trap)到用户模式程序的特殊指令,以及允许操作系统告诉硬件陷阱表( trap table)驻留在内存中的位置的指令。

要执行一个系统调用,程序必须执行一个特殊的trap指令该指令同时跳转到内核并将特权级别提升到内核模式;一旦进入内核,系统现在就可以执行任何需要的特权操作(如果允许的话),从而为调用进程执行所需的工作。当操作完成时,操作系统调用一个特殊的return-from-trap指令,如您所料,该指令返回到调用用户程序,同时将特权级别降低到用户模式
硬件在执行trap时需要稍微小心一点,因为它必须确保保存足够的调用者寄存器(caller’s registers),以便在操作系统发出return-from-trap指令时能够正确返回。例如,在x86上,处理器将把程序计数器、标志和一些其他寄存器推到每个进程的内核堆栈(kernel stack)上;return from-trap将从堆栈中取出这些值并继续执行用户模式程序(详情请参阅英特尔系统手册[I11])。其他硬件系统使用不同的约定,但基本概念在不同平台上是相似的。
讨论中遗漏了一个重要的细节:陷阱如何知道在操作系统中运行哪些代码?显然,调用进程不能指定要跳转到的地址(就像调用过程时那样);这样做将允许程序跳转到内核的任何地方,这显然是一个非常糟糕的主意(Very Bad Idea)。因此,内核必须小心地控制在陷阱上执行的代码。

想象一下,进入代码访问文件,但刚刚经过权限检查;事实上,这种能力很可能会让一个老谋深算的程序员让内核运行任意的代码序列[S07]。一般来说,尽量避免这种非常糟糕的想法。

内核通过在引导时设置一个陷阱表(trap table)来做到这一点。当机器启动时,它以特权(内核)模式启动,因此可以根据需要自由配置机器硬件。因此,操作系统所做的第一件事就是告诉硬件,当某些异常事件发生时,应该运行哪些代码。例如,当硬盘中断发生时,当键盘中断发生时,或当程序进行系统调用时,应该运行什么代码?操作系统将这些陷阱处理程序(trap handlers)的位置通知硬件,通常使用某种特殊指令。 一旦通知硬件,它就会记住这些处理程序的位置,直到机器下次重新启动,这样当系统调用和其他异常事件发生时,硬件就知道该做什么(即跳转到什么代码)。

Tip: 在安全系统中小心用户的输入 尽管我们已经煞费苦心地在系统调用期间保护操作系统(通过添加硬件捕获机制,并确保所有对操作系统的调用都通过它转达),但实现安全操作系统(secure operating system)还有许多其他方面必须考虑。其中之一是在系统调用边界处理参数;操作系统必须检查用户传入的参数,并确保正确指定参数,否则拒绝调用。 例如,对于write()系统调用,用户指定缓冲区的地址作为写调用的源。如果用户(不管是意外的还是恶意的)传递了一个坏的地址(例如,一个在内核地址空间的部分),操作系统必须检测到这并拒绝调用。否则,用户可能会读取所有的内核内存;如果内核(虚拟)内存通常也包括系统的所有物理内存,那么这个小错误将使程序能够读取系统中任何其他进程的内存。 一般来说,一个安全的系统必须以高度怀疑的态度对待用户输入。如果不这样做,无疑会导致软件很容易被黑客攻击,会让人感到世界是一个不安全和可怕的地方,会让过于信任的操作系统开发人员失去工作保障。

  1. 为了指定准确的系统调用,通常为每个系统调用分配一个系统调用号(**system-call number**)。因此,用户代码负责将所需的系统调用号放置在寄存器或堆栈上的指定位置;当操作系统在陷阱处理程序**trap handlers**中处理系统调用时,会检查这个数字,确保它是有效的,如果是,则执行相应的代码。这种级别的间接服务是一种保护形式; 用户代码不能指定要跳转到的确切地址,而是必须通过号码请求特定的服务。<br />最后一点:能够执行指令告诉硬件陷阱表在哪里是一个非常强大的功能。因此,正如您可能已经猜到的,它也是一个特权操作(**privileged operation**)。如果您试图在用户模式下执行这条指令,硬件不允许,您可能会猜到会发生什么(提示:再见,违规程序)。思考的要点:如果你可以安装自己的陷阱表,你会对一个系统做什么可怕的事情?你能接管这台机器吗?<br />时间轴(随着时间向下增加,如图6.2所示)总结了协议。我们假设每个进程都有一个内核堆栈,当转换到内核和转换出内核时,寄存器(包括通用寄存器和程序计数器)被保存到内核堆栈中,或者从(由硬件)恢复到内核堆栈中。<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/12377925/1629708025412-70551453-1d54-48d4-ac41-025e7280d094.png#clientId=ua355bb72-979b-4&from=paste&height=834&id=u730047b0&margin=%5Bobject%20Object%5D&name=image.png&originHeight=834&originWidth=651&originalType=binary&ratio=1&size=99338&status=done&style=none&taskId=u4467387e-c36c-48cb-9db8-d60cff66184&width=651)<br />在受限直接执行(**limited direct execution,LDE**)协议中有两个阶段。第一阶段(在引导时),内核初始化trap表,CPU记住它的位置以便后续使用。内核通过一条特权指令(所有特权指令都用粗体突出显示)来实现这一点。<br />在第二个阶段(运行进程时),内核在使用return-from-trap指令开始执行进程之前设置一些事情(例如,在进程列表上分配一个节点,分配内存);这会将CPU切换到用户模式,并开始运行进程。当进程希望发出一个系统调用时,它将返回到操作系统,操作系统将处理它,并再次通过一个return-from-trap返回控制权给进程。然后进程完成它的工作,并从main()返回;这通常会返回到一些将正确退出程序的存根代码中(例如,通过调用exit()系统调用,这将trap到操作系统)。此时,操作系统清理完毕,我们就完成了。

6.3

问题2:进程间切换 Problem #2: Switching Between Processes 直接执行的下一个问题是实现进程之间的切换。在进程之间切换应该很简单,对吧?操作系统应该决定停止一个进程并启动另一个进程。有什么大不了的?但它实际上有一点棘手:具体来说,如果一个进程在CPU上运行,这就意味着操作系统没有运行。如果操作系统没有运行,它怎么能做任何事情呢?(提示:它不能)虽然这听起来几乎是哲学的,但它是一个真正的问题:显然,如果操作系统不运行在CPU上,就没有办法采取行动。这样我们就找到了问题的症结所在。

问题的关键:怎样重新控制CPU 操作系统如何重新获得(regain control)对CPU的控制,以便它可以在进程之间切换?

合作方式:等待系统调用 A Cooperative Approach: Wait For System Calls

过去一些系统采用的一种方法(例如,Macintosh操作系统[M11]的早期版本,或者施乐Alto系统[A79])被称为合作方法(cooperative approach)。在这种风格中,操作系统信任系统进程的合理行为。运行时间过长的进程被认为周期性地放弃CPU,以便操作系统可以决定运行其他任务。
因此,您可能会问,一个友好的进程如何在这个乌托邦世界中放弃CPU ?事实证明,大多数进程都非常频繁地通过系统调用将CPU的控制权传递给OS,例如,打开一个文件并随后读取它,或向另一台机器发送一条消息,或创建一个新进程。这样的系统通常包括一个显式的yield系统调用,除了将控制转移到操作系统以便它可以运行其他进程之外,它什么也不做。
当应用程序做了一些非法的事情时,它们也会将控制权转移给操作系统。例如,如果一个应用程序被0除,或者试图访问它不应该访问的内存,它将向操作系统生成一个trap。然后,操作系统将再次控制CPU(并可能终止违规进程)。
因此,在协同调度系统中,操作系统通过等待系统调用或某种非法操作的发生来重新获得对CPU的控制。你可能也在想:这种被动的方式不是不太理想吗?例如,如果一个进程(无论是恶意的进程,还是充满了错误的进程)在无限循环中结束,并且从未进行系统调用,会发生什么情况?那么操作系统能做什么呢?

非合作方式:操作系统控制 A Non-Cooperative Approach: The OS Takes Control

如果没有硬件的额外帮助,当进程拒绝进行系统调用(或错误)并因此将控制权返回给操作系统时,操作系统根本无法做很多事情。事实上,在协作方法中,当进程陷入无限循环时,您唯一的求助方法是求助于计算机系统中所有问题的古老解决方案:重新启动机器。因此,我们再次到达了获得CPU控制的一般任务的子问题。

问题的关键:如何在没有合作的情况下获得控制权 即使进程不合作,操作系统如何获得对CPU的控制?操作系统如何确保不被非法进程接管?

  1. 答案很简单,许多人在多年前就发现了:定时器中断(**timer interrupt**)[M+63]。一个计时器装置可以被编程为每几毫秒触发一个中断;当中断被引发时,当前正在运行的进程将被停止,并在操作系统中运行一个预先配置的中断处理程序(**interrupt handler**)。此时,操作系统已经恢复了对CPU的控制,因此可以做它想做的事情:停止当前进程,并启动另一个进程。<br />正如我们之前讨论的系统调用,当定时器中断发生时,操作系统必须通知硬件要运行哪些代码;因此,在引导的时候(**boot time**),操作系统正是这样做的。第二,也是在引导序列期间,操作系统必须启动定时器,这当然是一个特权操作。一旦定时器开始,操作系统就会感到安全,控制最终会返回给它,因此操作系统可以自由地运行用户程序。还可以关闭定时器(也是一种特权操作),我们将在稍后更详细地理解并发性时讨论这个问题。

Tip:处理应用程序错误行为 操作系统经常不得不处理行为不当的进程,这些进程要么是由于设计(恶意),要么是由于事故(bug),试图做一些它们不应该做的事情。在现代系统中,操作系统试图处理这种不当行为的方式是简单地终止冒犯者。你一振就出局!也许很残忍,但当你试图非法访问内存或执行非法指令时,操作系统还应该做什么呢?

  1. 请注意,当中断发生时,硬件有一定的责任,特别是保存中断发生时正在运行的程序的足够的状态,以便后续的return-from-

trap指令能够正确地恢复正在运行的程序。这组动作非常类似于硬件在显式的系统调用trap进入内核期间的行为,不同的寄存器因此被保存(例如,在内核堆栈上),因此很容易通过return-from-trap指令恢复。

保存和恢复上下文 Saving and Restoring Context

现在,操作系统已经重新获得了控制权,无论是通过系统调用,还是更有力地通过定时器中断,必须做出决定:是继续运行当前运行的进程,还是切换到另一个进程。这个决定是由操作系统中称为调度器(scheduler)的部分做出的;我们将在接下来的几章中详细讨论调度策略。
如果决定切换,操作系统就会执行一段低级代码,我们称之为上下文切换(context switch)。上下文切换在概念上很简单:操作系统所要做的就是为当前正在执行的进程保存一些寄存器值(例如,保存到其内核堆栈上),并为即将执行的进程恢复一些寄存器值(从其内核堆栈中)。通过这样做,操作系统可以确保在最终执行return-from-trap指令时,系统不会返回正在运行的进程,而是继续执行另一个进程。
为了保存当前运行进程的上下文,操作系统将执行一些低级汇编代码来保存当前运行进程的通用寄存器PC和内核堆栈指针,然后恢复上述寄存器,PC,并切换到即将执行的进程的内核堆栈。通过切换堆栈,内核在一个进程(被中断的进程)的上下文中进入对切换代码的调用,并在另一个进程(即将执行的进程)的上下文中返回。当操作系统最终执行return-from-trap时,即将执行的进程成为当前运行的进程。这样上下文切换就完成了。

Tip:使用定时器中断重新获得控制 添加定时器中断使操作系统能够在CPU上再次运行,即使进程以一种不合作的方式运行。因此,这个硬件特性对于帮助操作系统维护对机器的控制至关重要。

Tip:重启是有用的 前面,我们注意到,在合作抢占下解决无限循环(和类似行为)的唯一解决方案是重新启动机器。虽然你可能会嘲笑这种黑客行为,但研究人员已经表明,重新启动(或者一般来说,重新启动某个软件)在构建健壮的系统中是一个非常有用的工具[C+04]。 具体来说,重新引导是有用的,因为它将软件移回一个已知的、可能经过更多测试的状态。重新启动还可以回收陈旧或泄漏的资源(例如内存),否则这些资源可能很难处理。最后,重新启动很容易实现自动化。由于所有这些原因,在大规模集群Internet服务中,系统管理软件定期重新启动机器集以重新启动它们,从而获得上述优势的情况并不少见。 因此,下次重新启动时,您不只是在执行一些丑陋的黑客行为。相反,您正在使用一种经过时间考验的方法来改进计算机系统的行为。做得好!

  1. 整个过程的时间线如图 6.3 所示。**在这个例子中,进程 A 正在运行,然后被定时器中断中断。硬件保存其寄存器(到其内核堆栈上)并进入内核(切换到内核模式)。在定时器中断处理程序中,操作系统决定从正在运行的进程 A 切换到进程 B。此时,它调用 switch() 例程,该例程小心地保存当前寄存器值(到 A 的进程结构中),恢复进程的寄存器进程 B(从它的进程结构入口),然后切换上下文,特别是通过更改堆栈指针以使用 B 的内核堆栈(而不是 A )。最后,操作系统returns-from-trap,它恢复 B 的寄存器并开始运行它。**<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/12377925/1629711989530-dbfa07e7-2a73-4a17-a11e-251d80fbd652.png#clientId=ua355bb72-979b-4&from=paste&height=876&id=u0a09f074&margin=%5Bobject%20Object%5D&name=image.png&originHeight=876&originWidth=839&originalType=binary&ratio=1&size=114121&status=done&style=none&taskId=u88a78fa1-7722-4588-a334-4b43f19f3f1&width=839)<br />注意,在此协议期间有**两种**类型的寄存器保存/恢复。**第一个是定时器中断发生的时间**;在这种情况下,**硬件使用进程的内核堆栈隐式地保存正在运行进程的用户寄存器**。**第二个是当操作系统决定从A切换到B**;在这种情况下,**内核寄存器是由软件(即操作系统)显式保存的,但这一次是在进程的进程结构的内存中。后一个操作将系统从就像刚刚从A trapped 进入内核的状态移动到就像刚刚从B trapped 进入内核的状态。**<br />为了让您更好地了解这种切换是如何执行的,图6.4显示了xv6的上下文切换代码。看看您是否能够理解它(您必须了解一些x86和一些xv6才能理解它)。新旧语境结构分别存在于新旧进程的进程结构中。<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/12377925/1629713131500-d414eaa1-fb18-4160-8fc4-ed665a27d8aa.png#clientId=ua355bb72-979b-4&from=paste&height=812&id=u369d0860&margin=%5Bobject%20Object%5D&name=image.png&originHeight=812&originWidth=874&originalType=binary&ratio=1&size=133763&status=done&style=none&taskId=u11dd81af-0544-4fd8-9e29-6439c1bb7bb&width=874)

6.4 担心并发吗?

Worried About Concurrency? 你们中的一些人,作为细心和有思想的读者,现在可能在想:嗯……在系统调用期间,定时器中断发生时,会发生什么?或者当您处理一个中断时,发生另一个中断时,会发生什么?这在内核中不会很难处理吗?好问题,我们真的对你有一些希望!
答案是肯定的,如果在中断或陷阱处理期间发生另一个中断,操作系统确实需要关心会发生什么。事实上,这正是本书第二部分关于并发性(concurrency)的主题;我们将推迟到那时再详细讨论。
为了激发您的兴趣,我们将简要介绍操作系统如何处理这些棘手的情况。操作系统可能做的一件简单的事情是在中断处理期间禁用中断;这样做可以确保当一个中断被处理时,不会有其他的中断被发送到CPU。当然,操作系统在这样做时必须小心;禁用中断太长时间可能会导致中断丢失,这(在技术上)是糟糕的。
操作系统还开发了许多复杂的锁定方案来保护对内部数据结构的并发访问。这使得内核中可以同时进行多个活动,在多处理器上尤其有用。但是,正如我们将在本书关于并发性的下一篇文章中看到的,这种锁定可能很复杂,并导致各种有趣的、难以找到的错误。

6.5

总结 Summary 我们已经描述了实现CPU虚拟化的一些关键的底层机制,这些技术统称为受限直接执行limited direct execution。基本思想很简单:只需在 CPU 上运行您想要运行的程序,但首先确保设置硬件以限制该进程在没有操作系统帮助的情况下可以执行的操作。

Aside:上下文切换需要多长时间 您可能会很自然地问:像上下文切换这样的事情需要多长时间?或者甚至是一个系统调用?对于那些好奇的人,有一个叫做lmbench [MS96]的工具可以精确地度量这些东西,以及一些其他相关的性能度量。 随着时间的推移,结果有了很大的改善,大致跟踪处理器性能。例如,1996 年在 200 MHz P6 CPU 上运行 Linux 1.3.37,系统调用大约需要 4 微秒,上下文切换大约需要 6 微秒 [MS96]。现代系统的性能几乎要好一个数量级,在具有 2 或 3 GHz 处理器的系统上获得亚微秒的结果。 应该注意的是,并非所有操作系统操作都跟踪CPU性能。正如Ousterhout观察到的,许多操作系统都是内存密集型的,随着时间的推移,内存带宽并没有像处理器速度那样显著地提高[O90]。因此,根据您的工作负载,购买最新和最好的处理器可能不会像您希望的那样提高操作系统的速度。

在现实生活中也可以采用这种方法。例如,你们中有孩子的人,或者至少听说过孩子的人,可能对防止婴儿(baby proofing)进入房间的概念很熟悉:锁上装有危险物品的柜子,遮住电源插座。当房间如此准备好,你可以让你的宝宝自由地漫游,安全的知识,房间最危险的方面已经被限制。
以类似的方式,OS婴儿通过首先(在引导期间)设置陷阱处理程序(trap handlers)和启动中断定时器来防护CPU,然后只在受限模式下运行进程。通过这样做,操作系统可以确信进程可以高效运行,只需要操作系统干预来执行特权操作,或者当它们垄断CPU太长时间,因此需要切换掉。
这样,我们就有了虚拟化CPU的基本机制。但是一个主要的问题没有得到回答:在给定的时间我们应该运行哪个进程?这就是调度者必须回答的问题,因此我们的下一个研究主题。

Aside:关键CPU虚拟化术语(机制)

  • CPU至少应该支持两种执行模式:受限用户模式(user mode)和特权(非受限)内核模式(kernel mode)。
  • 典型的用户应用程序在用户模式下运行,并使用系统调用(system call)进入内核以请求操作系统服务。
  • trap指令小心地保存寄存器状态,将硬件状态更改为内核模式,并将操作系统跳转到一个预先指定的目的地:trap表(trap table)。
  • 当操作系统完成对系统调用的服务时,它通过另一条特殊的return-from-trap指令返回到用户程序,该指令减少了特权,并将控制权返回给进入操作系统的trap之后的指令。
  • 操作系统必须在引导时设置trap表,并确保它们不会被用户程序轻易修改。所有这些都是受限直接执行(limited direct execution)协议的一部分,该协议有效地运行程序,但不会失去操作系统的控制。
  • 一旦程序运行,操作系统必须使用硬件机制来确保用户程序不会永远运行,即定时器中断(timer interrupt)。这种方法是一种非协作(non-cooperative)的CPU调度方法。
  • 有时,在定时器中断或系统调用期间,操作系统可能希望从运行当前进程切换到运行另一个进程,这是一种低级别的技术,称为上下文切换(context switch)。

References

[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. http://en.wikipedia.org/wiki/
Mac OS 9 . You can probably even find an OS 9 emulator out there if you want to; check it out, it’s a
fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.

Homework (Measurement)

Aside:测量作业 MEASUREMENT HOMEWORKS 测量作业是一些小练习,你编写代码在真实的机器上运行,以测量操作系统或硬件性能的某些方面。这样的作业背后的想法是给你一点实际操作系统的实践经验。

在本作业中,您将度量系统调用和上下文切换的成本。测量系统调用的成本相对容易。例如,您可以重复调用一个简单的系统调用(例如,执行一个0字节的读取),并计算它所花费的时间;将时间除以迭代次数,就可以估算出系统调用的成本。
您必须考虑的一件事是计时器的精度和准确性。您可以使用的典型计时器是 gettimeofday();阅读手册页man page了解详细信息。您将看到 gettimeofday() 返回自 1970 年以来的时间(以微秒为单位);然而,这并不意味着计时器精确到微秒。测量对 gettimeofday() 的背靠背调用以了解计时器的精确程度;这将告诉您必须运行多少次空系统调用测试才能获得良好的测量结果。如果 gettimeofday() 对您来说不够精确,您可以考虑使用 x86 机器上可用的 rdtsc 指令。
度量上下文切换的成本有点棘手。lmbench基准测试通过在单个CPU上运行两个进程,并在它们之间设置两个UNIX管道来实现这一点;管道只是UNIX系统中进程之间通信的多种方式之一。然后,第一个进程向第一个管道发出写操作,并等待第二个管道的读操作;当看到第一个进程在等待从第二个管道读取数据时,操作系统将第一个进程置于阻塞状态,并切换到另一个进程,后者从第一个管道读取数据,然后写入第二个管道。当第二个进程再次尝试从第一个管道读取数据时,它会阻塞,因此来回的通信循环会继续下去。通过反复测量这样的通信成本,lmbench可以很好地估计上下文切换的成本。您可以尝试在这里重新创建类似的内容,使用管道或其他通信机制,如UNIX套接字。
在有多个CPU的系统中,测量上下文切换成本的一个困难是:在这样的系统上,您需要做的是确保上下文切换进程位于同一个处理器上。幸运的是,大多数操作系统都有将进程绑定到特定处理器的调用;例如,在Linux上,sched_setaffinity()调用就是您正在寻找的。通过确保两个进程在同一个处理器上,可以确定OS在同一CPU上停止一个进程并恢复另一个进程的成本。