对操作系统底层认识还是一片空白怎么办?

昨天看到一条评论,读者自己说明了自己对操作系统底层认识一片空白的解决方法,小编觉着应该在这里说一下。读者解决问题的方法是看了《操作系统导论》,而且他准备再看第二遍。哈哈。

那么《操作系统导论》到底是一本什么要的书呢?

对操作系统底层认识还是一片空白怎么办?_第1张图片

 

《操作系统导论》围绕3个主题元素展开讲解:虚拟化(virtualization)、并发(concurrency)和持久性(persistence)。对于这些概念的讨论,最终延伸到讨论操作系统所做的大多数重要事情。

每个主要概念在若干章节中加以阐释,其中大部分章节都提出了一个特定的问题,然后展示了解决它的方法。这些章节很简短,尝试(尽可能地)引用作为这些想法真正来源的源材料。我们写这本书的目的之一就是厘清操作系统的发展脉络,因为我们认为这有助于学生更清楚地理解过去是什么、现在是什么、将来会是什么。在这种情况下,了解香肠的制作方法几乎与了解香肠的优点一样重要。

我们在整本书中采用了几种结构,值得在这里介绍一下。

无论何时,在试图解决问题时,我们首先要说明最重要的问题是什么。我们在书中明确提出关键问题(crux of the problem),并希望通过本书其余部分提出的技术、算法和思想来解决。

在许多地方,我们将通过显示一段时间内的行为来解释系统的工作原理。这些时间线(timeline)是理解的本质。如果你知道会发生什么,例如,当进程出现页故障时,你就可以真正了解虚拟内存的运行方式。如果你理解日志文件系统将块写入磁盘时发生的情况,就已经迈出了掌握存储系统的第一步。

整本书中有许多“补充”和“提示”,为主线讲解增添了一些趣味性。“补充”倾向于讨论与主要文本相关的内容(但可能不是必要的);“提示”往往是一般经验,可以应用于所构建的系统。

在整本书中,我们使用最古老的教学方法之一——对话(dialogue)。这些对话用于介绍主要的主题概念,并不时地复习这些内容。这也让我们得以用更幽默的方式写作。好吧,你觉得它们是有用还是幽默,完全是另一回事。

在每一个主要部分的开头,我们将首先呈现操作系统提供的抽象(abstraction),然后在后续章节中介绍提供抽象所需的机制、策略和其他支持。抽象是计算机科学各个方面的基础,因此它在操作系统中也是必不可少的。

在所有的章节中,我们尝试使用可能的真实代码(real code),而非伪代码(pseudocode)。因此书中几乎所有的示例,你应该能够自己输入并运行它们。在真实系统上运行真实代码是了解操作系统的最佳方式,因此建议你尽可能这样做。

在本书的各个部分,我们提供了一些作业(homework),确保你进一步理解书中的内容。其中许多作业都是对操作系统的一些模拟(simulation)程序。你应该下载作业,并运行它们,以此来测验自己。作业模拟程序具有以下特征:通过给它们提供不同的随机种子,你可以产生几乎无限的问题,也可以让模拟程序为你解决问题。因此,你可以一次又一次地自测,直至很好地理解了这些知识。

本书最重要的附录是一组项目(project),可供你通过设计、测试和实现自己的代码,来了解真实系统的工作原理。所有项目(以及上面提到的代码示例)都是使用C编程语言(C programming language)[KR88]编写的。C是一种简单而强大的语言,是大多数操作系统的基础,因此值得添加到你的工具库中。附录中含有两种类型的项目(请参阅在线附录中的想法)。第一类是系统编程(system programming)项目。这些项目非常适合那些不熟悉C和UNIX,并希望学习如何进行底层C编程的人。第二类基于在麻省理工学院开发的实际操作系统内核,称为xv6 [CK+08]。这些项目非常适合已经有一些C的经验并希望深入研究操作系统的学生。在威斯康星大学,我们以 3 种不同的方式开课:系统编程、xv6编程,或两者兼而有之。

如果你是读这本书的学生,那么我们很荣幸能够提供一些材料来帮助你学习操作系统的知识。我们至今还能够回想起我们使用过的一些教科书(例如,Hennessy和Patterson的著作[HP90],这是一本关于计算机架构的经典著作),并希望这本书能够成为你美好的回忆之一。


干货分享

本段内容截选自《操作系统导论》第15章。

第15章 机制:地址转换

在实现CPU虚拟化时,我们遵循的一般准则被称为受限直接访问(Limited Direct Execution,LDE)。LDE背后的想法很简单:让程序运行的大部分指令直接访问硬件,只在一些关键点(如进程发起系统调用或发生时钟中断)由操作系统介入来确保“在正确时间,正确的地点,做正确的事”。为了实现高效的虚拟化,操作系统应该尽量让程序自己运行,同时通过在关键点的及时介入(interposing),来保持对硬件的控制。高效和控制是现代操作系统的两个主要目标。

在实现虚拟内存时,我们将追求类似的战略,在实现高效和控制的同时,提供期望的虚拟化。高效决定了我们要利用硬件的支持,这在开始的时候非常初级(如使用一些寄存器),但会变得相当复杂(比如我们会讲到的TLB、页表等)。控制意味着操作系统要确保应用程序只能访问它自己的内存空间。因此,要保护应用程序不会相互影响,也不会影响操作系统,我们需要硬件的帮助。最后,我们对虚拟内存还有一点要求,即灵活性。具体来说,我们希望程序能以任何方式访问它自己的地址空间,从而让系统更容易编程。所以,关键问题在于:

关键问题:如何高效、灵活地虚拟化内存 

如何实现高效的内存虚拟化?如何提供应用程序所需的灵活性?如何保持控制应用程序可访问的内存位置,从而确保应用程序的内存访问受到合理的限制?如何高效地实现这一切?

我们利用了一种通用技术,有时被称为基于硬件的地址转换(hardware-based address translation),简称为地址转换(address translation)。它可以看成是受限直接执行这种一般方法的补充。利用地址转换,硬件对每次内存访问进行处理(即指令获取、数据读取或写入),将指令中的虚拟(virtual)地址转换为数据实际存储的物理(physical)地址。因此,在每次内存引用时,硬件都会进行地址转换,将应用程序的内存引用重定位到内存中实际的位置。

当然,仅仅依靠硬件不足以实现虚拟内存,因为它只是提供了底层机制来提高效率。操作系统必须在关键的位置介入,设置好硬件,以便完成正确的地址转换。因此它必须管理内存(manage memory),记录被占用和空闲的内存位置,并明智而谨慎地介入,保持对内存使用的控制。

同样,所有这些工作都是为了创造一种美丽的假象:每个程序都拥有私有的内存,那里存放着它自己的代码和数据。虚拟现实的背后是丑陋的物理事实:许多程序其实是在同一时间共享着内存,就像CPU(或多个CPU)在不同的程序间切换运行。通过虚拟化,操作系统(在硬件的帮助下)将丑陋的机器现实转化成一种有用的、强大的、易于使用的抽象。

15.1 假设

我们对内存虚拟化的第一次尝试非常简单,甚至有点可笑。如果你觉得可笑就笑吧,很快就轮到操作系统嘲笑你了。当你试图理解TLB的换入换出、多级页表,和其他技术一样有奇迹之处的时候。不喜欢操作系统嘲笑你?很不幸,但这就是操作系统的运行方式。

具体来说,我们先假设用户的地址空间必须连续地放在物理内存中。同时,为了简单,我们假设地址空间不是很大,具体来说,小于物理内存的大小。最后,假设每个地址空间的大小完全一样。别担心这些假设听起来不切实际,我们会逐步地放宽这些假设,从而得到现实的内存虚拟化。

15.2 一个例子

为了更好地理解实现地址转换需要什么,以及为什么需要,我们先来看一个简单的例子。设想一个进程的地址空间如图15.1所示。这里我们要检查一小段代码,它从内存中加载一个值,对它加3,然后将它存回内存。你可以设想,这段代码的C语言形式可能像这样:

 
  1. void func() {
  2. int x;
  3. x = x + 3; // this is the line of code we are interested in

编译器将这行代码转化为汇编语句,可能像下面这样(x86汇编)。我们可以用Linux的objdump或者Mac的otool将它反汇编:

 
  1. 128: movl 0x0(%ebx), %eax ;load 0+ebx into eax
  2. 132: addl $0x03, %eax ;add 3 to eax register
  3. 135: movl %eax, 0x0(%ebx) ;store eax back to mem

这段代码相对简单,它假定x的地址已经存入寄存器ebx,之后通过movl指令将这个地址的值加载到通用寄存器eax(长字移动)。下一条指令对eax的内容加3。最后一条指令将eax中的值写回到内存的同一位置。

提示:介入(Interposition)很强大 

介入是一种很常见又很有用的技术,计算机系统中使用介入常常能带来很好的效果。在虚拟内存中,硬件可以介入到每次内存访问中,将进程提供的虚拟地址转换为数据实际存储的物理地址。但是,一般化的介入技术有更广阔的应用空间,实际上几乎所有良好定义的接口都应该提供功能介入机制,以便增加功能或者在其他方面提升系统。这种方式最基本的优点是透明(transparency),介入完成时通常不需要改动接口的客户端,因此客户端不需要任何改动。

在图15.1中,可以看到代码和数据都位于进程的地址空间,3条指令序列位于地址128(靠近头部的代码段),变量x的值位于地址15KB(在靠近底部的栈中)。如图15.1所示,x的初始值是3000。

对操作系统底层认识还是一片空白怎么办?_第2张图片

 

图15.1 进程及其地址空间

如果这3条指令执行,从进程的角度来看,发生了以下几次内存访问:

  • 从地址128获取指令;
  • 执行指令(从地址15KB加载数据);
  • 从地址132获取命令;
  • 执行命令(没有内存访问);
  • 从地址135获取指令;
  • 执行指令(新值存入地址15KB)。

从程序的角度来看,它的地址空间(address space)从0开始到16KB结束。它包含的所有内存引用都应该在这个范围内。然而,对虚拟内存来说,操作系统希望将这个进程地址空间放在物理内存的其他位置,并不一定从地址0开始。因此我们遇到了如下问题:怎样在内存中重定位这个进程,同时对该进程透明(transparent)?怎么样提供一种虚拟地址空间从0开始的假象,而实际上地址空间位于另外某个物理地址?

图15.2展示了一个例子,说明这个进程的地址空间被放入物理内存后可能的样子。从图15.2中可以看到,操作系统将第一块物理内存留给了自己,并将上述例子中的进程地址空间重定位到从32KB开始的物理内存地址。剩下的两块内存空闲(16~32KB和48~64KB)。

对操作系统底层认识还是一片空白怎么办?_第3张图片

 

图15.2 物理内存和单个重定位的进程

15.3 动态(基于硬件)重定位

为了更好地理解基于硬件的地址转换,我们先来讨论它的第一次应用。在20世纪50年代后期,它在首次出现的时分机器中引入,那时只是一个简单的思想,称为基址加界限机制(base and bound),有时又称为动态重定位(dynamic relocation),我们将互换使用这两个术语[SS74]。

具体来说,每个CPU需要两个硬件寄存器:基址(base)寄存器和界限(bound)寄存器,有时称为限制(limit)寄存器。这组基址和界限寄存器,让我们能够将地址空间放在物理内存的任何位置,同时又能确保进程只能访问自己的地址空间。

采用这种方式,在编写和编译程序时假设地址空间从零开始。但是,当程序真正执行时,操作系统会决定其在物理内存中的实际加载地址,并将起始地址记录在基址寄存器中。在上面的例子中,操作系统决定加载在物理地址32KB的进程,因此将基址寄存器设置为这个值。

当进程运行时,有趣的事情发生了。现在,该进程产生的所有内存引用,都会被处理器通过以下方式转换为物理地址:

  1. physical address = virtual address + base

补充:基于软件的重定位 

在早期,在硬件支持重定位之前,一些系统曾经采用纯软件的重定位方式。基本技术被称为静态重定位(static relocation),其中一个名为加载程序(loader)的软件接手将要运行的可执行程序,将它的地址重写到物理内存中期望的偏移位置。

例如,程序中有一条指令是从地址1000加载到寄存器(即movl 1000,%eax),当整个程序的地址空间被加载到从3000(不是程序认为的0)开始的物理地址中,加载程序会重写指令中的地址(即movl 4000, %eax),从而完成简单的静态重定位。

然而,静态重定位有许多问题,首先也是最重要的是不提供访问保护,进程中的错误地址可能导致对其他进程或操作系统内存的非法访问,一般来说,需要硬件支持来实现真正的访问保护[WL+93]。静态重定位的另一个缺点是一旦完成,稍后很难将内存空间重定位到其他位置 [M65]。

进程中使用的内存引用都是虚拟地址(virtual address),硬件接下来将虚拟地址加上基址寄存器中的内容,得到物理地址(physical address),再发给内存系统。

为了更好地理解,让我们追踪一条指令执行的情况。具体来看前面序列中的一条指令:

  1. 128: movl 0x0(%ebx), %eax

程序计数器(PC)首先被设置为128。当硬件需要获取这条指令时,它先将这个值加上基址寄存器中的32KB(32768),得到实际的物理地址32896,然后硬件从这个物理地址获取指令。接下来,处理器开始执行该指令。这时,进程发起从虚拟地址15KB的加载,处理器同样将虚拟地址加上基址寄存器内容(32KB),得到最终的物理地址47KB,从而获得需要的数据。

将虚拟地址转换为物理地址,这正是所谓的地址转换(address translation)技术。也就是说,硬件取得进程认为它要访问的地址,将它转换成数据实际位于的物理地址。由于这种重定位是在运行时发生的,而且我们甚至可以在进程开始运行后改变其地址空间,这种技术一般被称为动态重定位(dynamic relocation)[M65]。

提示:基于硬件的动态重定位 

在动态重定位的过程中,只有很少的硬件参与,但获得了很好的效果。一个基址寄存器将虚拟地址转换为物理地址,一个界限寄存器确保这个地址在进程地址空间的范围内。它们一起提供了既简单又高效的虚拟内存机制。

现在你可能会问,界限(限制)寄存器去哪了?不是基址加界限机制吗?正如你猜测的那样,界限寄存器提供了访问保护。在上面的例子中,界限寄存器被置为16KB。如果进程需要访问超过这个界限或者为负数的虚拟地址,CPU将触发异常,进程最终可能被终止。界限寄存器的用处在于,它确保了进程产生的所有地址都在进程的地址“界限”中。

这种基址寄存器配合界限寄存器的硬件结构是芯片中的(每个CPU一对)。有时我们将CPU的这个负责地址转换的部分统称为内存管理单元(Memory Management Unit,MMU)。随着我们开发更复杂的内存管理技术,MMU也将有更复杂的电路和功能。

关于界限寄存器再补充一点,它通常有两种使用方式。在一种方式中(像上面那样),它记录地址空间的大小,硬件在将虚拟地址与基址寄存器内容求和前,就检查这个界限。另一种方式是界限寄存器中记录地址空间结束的物理地址,硬件在转化虚拟地址到物理地址之后才去检查这个界限。这两种方式在逻辑上是等价的。简单起见,我们这里假设采用第一种方式。

转换示例

为了更好地理解基址加界限的地址转换的详细过程,我们来看一个例子。设想一个进程拥有4KB大小地址空间(是的,小得不切实际),它被加载到从16KB开始的物理内存中。一些地址转换结果见表15.1。

表15.1 地址转换结果

虚拟地址   物理地址
0 16KB
1KB 17KB
3000 19384
4400 错误(越界)

从例子中可以看到,通过基址加虚拟地址(可以看作是地址空间的偏移量)的方式,很容易得到物理地址。虚拟地址“过大”或者为负数时,会导致异常。

补充:数据结构——空闲列表 

操作系统必须记录哪些空闲内存没有使用,以便能够为进程分配内存。很多不同的数据结构可以用于这项任务,其中最简单的(也是我们假定在这里采用的)是空闲列表(free list)。它就是一个列表,记录当前没有使用的物理内存的范围。

看到这里有必要让大家浏览一下主体目录。

  • 第1章 关于本书的对话
  • 第2章 操作系统介绍
  • 第1部分 虚拟化
  • 第3章 关于虚拟化的对话
  • 第4章 抽象:进程
  • 第5章 插叙:进程API
  • 第6章 机制:受限直接执行
  • 第7章 进程调度:介绍
  • 第8章 调度:多级反馈队列
  • 第9章 调度:比例份额
  • 第10章 多处理器调度(高级)
  • 第11章 关于CPU虚拟化的总结对话
  • 第12章 关于内存虚拟化的对话
  • 第13章 抽象:地址空间
  • 第14章 插叙:内存操作API
  • 第15章 机制:地址转换
  • 第16章 分段
  • 第17章 空闲空间管理
  • 第18章 分页:介绍
  • 第19章 分页:快速地址转换(TLB)
  • 第20章 分页:较小的表
  • 第21章 超越物理内存:机制
  • 第22章 超越物理内存:策略
  • 第23章 VAX/VMS虚拟内存系统
  • 第24章 内存虚拟化总结对话
  • 第2部分 并发
  • 第25章 关于并发的对话
  • 第26章 并发:介绍
  • 第27章 插叙:线程API
  • 第28章 锁
  • 第29章 基于锁的并发数据结构
  • 第30章 条件变量
  • 第31章 信号量
  • 第32章 常见并发问题
  • 第33章 基于事件的并发(进阶)
  • 第34章 并发的总结对话
  • 第3部分 持久性
  • 第35章 关于持久性的对话
  • 第36章 I/O设备
  • 第37章 磁盘驱动器
  • 第38章 廉价冗余磁盘阵列(RAID)
  • 第39章 插叙:文件和目录
  • 第40章 文件系统实现
  • 第41章 局部性和快速文件系统
  • 第42章 崩溃一致性:FSCK和日志
  • 第43章 日志结构文件系统
  • 第44章 数据完整性和保护
  • 第45章 关于持久的总结对话
  • 第46章 关于分布式的对话
  • 第47章 分布式系统
  • 第48章 Sun的网络文件系统(NFS)
  • 第49章 Andrew文件系统(AFS)
  • 第50章 关于分布式的总结对话
  • 附录A 关于虚拟机监视器的对话
  • 附录B 虚拟机监视器
  • 附录C 关于监视器的对话
  • 附录D 关于实验室的对话
  • 附录E 实验室:指南
  • 附录F 实验室:系统项目
  • 附录G 实验室:xv6项目

你可能感兴趣的:(对操作系统底层认识还是一片空白怎么办?)