原文:The Abstraction: Address Spaces
在早期,构建计算机系统是很简单的。为什么?你可能会想。因为用户期望值不高。都是那些『该死』的用户,想要一个"易用"、"高性能"、"可靠"的系统,才引起了一系列头疼的问题。下一次你遇到这些计算机用户们,别忘记感谢一下他们制造出来的问题:)
早期的系统
从内存的角度来说,早期的机器并没有提供太多的抽象给用户。一般而言,机器的物理内存可以用下图表示:
整个 OS(图中的阴影部分,译者注) 不过就是物理内存中的一些routines(本质上来说,一个 library),在这个例子中,从物理地址的 address 0开始。同时,还有一个运行的程序(进程)位于物理地址的 address 64k 处,并且独自占据了整个剩下的内存。用户并没对操作系统寄予太大的期望,操作系统程序员的生活还是很轻松的,不是吗?
多道编程(Multiprogramming)和 分时系统(Time Sharing)
一段时间过后,因为计算机实在是太贵了,人们开始更有效地共享有限的计算资源。于是 多道编程
DV66:Programming Semantics for Multiprogrammed Computations 的时代开始了,在一个时刻,多个进程可以同时等待被执行,然后操作系统不断在这些进程中切换(比如当前进程进行 IO 的时候就可以切换到其他进程)。这样很有效地提高了 CPU 的利用效率。在那个计算机动辄几百万美元的时代,这些效率的提高是非常重要的。
然而,人是永远不会满足的,人名开始期待能够从操作系统那获取更多功能,于是分时系统的时代开始了。很多人尤其是程序员们自己意识到了 batch programming 的局限性,厌倦了很长(于是效率低下)的 debug 周期。因为很多用户在同时使用一台计算机,每个人都在等待(甚至可以说是期望)自己的程序返回结果,可交互性(interactivity)的概念开始变得越来越重要。
实现 time sharing 的一种方法是让一个进程运行一小段时间,这段时间内让它占据所有的内存,然后停止它,把其所有的状态保持到硬盘上(包括所有的物理内存!),接口导入另外一个进程,运行一段时间...... 这也算是一种简陋的实现方法。
显然,这种方法有很大的问题:太慢了,尤其是内存不断增大的时候。保存和欢迎寄存器级别的状态(比如 PC,general-purpose registers等等)是很快的,但是要把整个内存的全部内容保存到硬盘,这无疑不是一种高效的方法。所以,我们得想办法在切换的时候把进程的状态保持在内存里面。
在这个图中,有三个进程(A,B,C),每个占据 512KB内存的一小部分。现在对于 CPU,他可以选择任何一个抽象来运行(比如说 A),然后让另外两个(B 和 C)等待。
随着分时系统越来越流行,你肯定猜到了人们又开始提出新的需求了。尤其重要的一点是:让这么多程序同时保存在内存里,保护措施就很重要,你可不希望一个进程可以看到其他进程的内容,更别说可以随意修改其他进程的内容。
地址空间
用户是很难伺候的,操作系统的设计者得对物理内存做一个抽象,提供一个好用的接口给他们。我们把这种抽象就叫做地址空间。对于运行中的程序而言,它对物理内存是无感知的,它知道的只有地址空间(虚拟的)。理解基本的操作系统抽象,是理解内存是如何虚拟化的关键!
进程的地址空间包含了运行进程的所有状态。比如说,代码总得保存在某个地方吧?所以代码就在地址空间里面。进程运行的时候,会用 stack 来保存当前函数调用链的位置,分配空间给局部变量,并且返回值。还有一部分,叫做 heap,被用来保存动态分配、用户管理的内存,比如调用 malloc() 函数,或者 new 一个对象,都是从这里面得到需要的内存。当然,保存在地址空间的内容还有很多,目前而言我们只需要关注于这三点就行了。
上图中,有一个很小的地址空间(16kb)。程序的代码保存在地址空间的最上方(0-1kb)。代码是静态的,不会增加也不会减少,可以直接放在地址空间的最上部。还有两部分大小可变的区域,那就是 heap 和 stack。上图中,stack 和 heap 往两个不同的方向增长。如进行 malloc()
申请内存时,heap 往下增长;当进行一个 procedure call
时,stack 往上增长。当然这只是一个惯例,并没有物理上的限制要求一定要这样,你可以以其他方法重新组织地址空间。(事实上,当多个 threads 共存的时候,也不存在这么优雅的地址空间划分方法。)
我们谈地址空间的时候,我们谈的是操作系统提供给进程的抽象。上图的进程并不是在物理内存的0-16kb 位置,它可以被存放在物理内存的任意位置。回顾一下图13.2,你可以看到每个进程被加载到内存的不同位置。
当操作系统这么做的时候,我们说操作系统是在虚拟化内存,因为运行中的进程是以为自己占据了所有的内存的。然而现实很不一样。
比如说,图13.2的进程A想要加载地址0处的数据(这里指的是虚拟内存),操作系统,具备特定的硬件支持,会把这个这个地址0映射到物理内存中另外的位置。这个就是内存虚拟化的核心,几乎每一个现代计算机系统都在试用这种方法。
目标
虚拟内存系统的最主要目标是 透明化
。也就是底层的物理地址对运行的程序不可见。所以,程序根本就不会意识到内存是虚拟化的,而是认为自己有一个完整的私有物理地址。操作系统在幕后,承当着虚拟地址到物理地址的转换工作。这极大地降低了程序开发者地复杂度!
第二个目标是 高效率
。操作系统应该更可能让抽象更加高效,包括时间上的和空间上的。比如,操作系统会依赖于一些硬件,比如 TLB 缓存,可以缩短操作系统访问用户内存的时间。
最后一个目标是 保护
。操作系统需要确保进程间是隔离的,一个进程不能随意读取另一个进程的数据,尤其是不能随意在别人的地盘乱写数据,不然会带来很多很多不可预期的问题。
总结一下
虚拟内存本质上就是一层抽象。操作系统给程序提供了一个简单易用的接口,把虚拟内存映射成底层的物理内存,这中间的复杂度全部交给了操作系统,极大地降低了软件开发的难度,同时降低了软件出错的可能性。
再次深刻地体会到了那句名言:
计算机领域的一切难题都可以通过抽象解决。