先解释下一个困扰了我很久的问题:虚拟地址(vitural address)和逻辑地址(logical address)的区别。
大部分操作系统的书籍要么写的是虚拟地址,要么写的是逻辑地址,看的我一脸懵逼。
在《深入理解 Linux 内核》这本书中终于找到了确切的答案,这里我就不写出来了,扣概念的话这俩确实是有些区别的,不过对于我们日常使用以及理解操作系统来说的话,暂且可以把虚拟地址和逻辑地址理解为同一个意思。
你看到的所有地址都不是真的
下面这段 C 代码摘录自《操作系统导论 - [美] 雷姆兹·H.阿帕希杜塞尔》,依次打印出 main 函数的地址,由 malloc(类似于 Java 中的 new 操作)返回的堆空间分配的值,以及栈上一个整数的地址:
得到以下输出:
我们需要知道的是,所有这些打印出来的地址都是虚拟的,在物理内存中这些地址并不真实存在,它们最终都将由操作系统和 CPU 硬件翻译成真正的物理地址,然后才能从真实的物理位置获取该地址的值。
OK,上述就当作一个引子,让各位对物理地址和虚拟地址有个直观的理解,下面正文开始。
物理寻址 Physical Addressing
物理地址的概念很好理解,你可以把它称为真正的地址。《深入理解计算机系统 - 第 3 版》中给出的物理地址(physical address)的定义如下:
计算机系统的主存被组织成一个由 M 个连续的字节大小的单元组成的数组。每字节都有一个唯一的物理地址。
比如说,第一个字节的物理地址是 0,接下来的字节地址是 1,再下一个是 2,以此类推,给定这种简单的结构,CPU 访问内存的最自然的方式就是使用这样的物理地址。我们把这种方式称为物理寻址(physical addressing)。
举个例子,比如说当程序执行了一条加载指令,指令内容是从物理地址 4 中读取 4 字节字传送到某个寄存器中。
物理寻址过程如下:当 CPU 执行到这条指令时,会生成物理地址 4,然后通过内存主线,把它传递给内存,内存取出从物理地址 4 处开始的 4 字节字,并将它返回给 CPU,CPU 会将它存放到指定的寄存器中。看下图:
其实不难发现,物理寻址这种方式,每一个程序都直接访问物理内存,其实是存在重大缺陷的:
1)首先,用户程序可以寻址内存的任意一个字节,它们就可以很容易地破坏操作系统,从而使系统慢慢地停止运行。
2)再次,这种寻址方式使得操作系统中同时运行两个或以上的程序几乎是不可能的。
举个例子,我们打开了三个相同的程序(计算器),都执行到某一步。比方说,用户在这三个程序的界面上分别输入了 10、100、1000,其对应的指令就是把用户输入的数字保存在内存中的某个地址中。如果这个位置只能保存一个数,那应该保存哪个呢?这不就冲突了吗?
再举个例子,摘自《现代操作系统 - 第 3 版》:
一个程序给物理内存地址 1000 赋值也就是存入了一些数据后,另一个程序也同样给这个地址赋值,那么第二个程序的赋值会覆盖掉第一个程序所赋的值,这会造成两个程序同时崩溃。
当然了,我们也说了是几乎不可能,不是完全不可能,还是有一些方法可以在物理寻址这种方式下实现多个程序并发运行的。
最简单的方法就是:首先,将空闲的进程存储在磁盘上,这样当它们不运行时就不会占用内存,然后,让一个程序(或者说进程)单独占用全部内存运行一小段时间,当发生上下文切换的时候,就停止这个进程,并将它所有的状态信息保存在磁盘上,再加载其他进程的状态信息,然后运行一段时间...... 只要在某一个时间内存中只有一个程序,那么就不会发生上述所说的地址冲突。这就实现了一种比较粗糙的并发。
为什么说他是粗糙的呢,因为这种方法有一个问题:将全部的内存信息保存到磁盘太慢了!特别是当内存增长的时候。
因此,我们考虑把进程对应的内存一直留在物理内存中,在发生上下文切换的时候就切换到特定的区域。
如下图所示,有 3 个进程(A、B、C),每个进程拥有从 512KB 物理内存中切出来给它们的一小部分内存,可以理解为这 3 个进程共享物理内存:
显然,这种方式是存在一定安全隐患的。毕竟如果各个进程之间可以随意读取、写入内容的话那就乱套了。
那么如何对每个进程使用的地址进行保护(protection)呢?继续使用物理内存模型肯定是不行了,因此操作系统创造了一个新的内存抽象,引入了一个新的内存模型,那就是虚拟地址空间,很多书中都会直接称呼为 “地址空间(Address Space)”。
虚拟寻址 Virtual Addressing
我先通俗地解释下虚拟地址空间和虚拟地址的概念,直接上书中的定义读起来有点生涩。
就是说每个进程的栈啊、堆啊、代码段啊等等它们的实际物理内存地址对于这个进程来说是不可见的,谁也不能直接访问这个物理地址。
那我们怎么去访问这个进程呢?
操作系统会给每个进程分配一个虚拟地址空间(vitural address),每个进程包含的栈、堆、代码段这些都会从这个地址空间中被分配一个地址,这个地址就被称为虚拟地址。底层指令写入的地址也是虚拟地址。
每个进程都拥有一个自己的地址空间,并且独立于其他进程的地址空间。也就是说一个进程中的虚拟地址 28 所对应的物理地址与另一个进程中的虚拟地址 28 所对应的物理地址是不同的,这样就不会发生冲突了。
可以这么理解,物理地址就是一个仓库,虚拟地址就是一个门牌,比方说一共有三十个门牌,那么所有的进程都能看见这三十个门牌,但是他们看见的某个相同门牌,指向的并不是同一个仓库。
OK,下面再来看《现代操作系统 - 第 3 版》书中对于地址空间的解释,应该很容易理解了:
地址空间是一个进程可用于寻址内存的一套地址集合。每个进程都有一个自己的地址空间,并且这个地址空间独立于其他进程的地址空间(除了在一些特殊情况下进程需要共享它们的地址空间外)。 地址空间的概念非常通用,并且在很多场合中出现。比如电话号码,在美国和很多其他国家,一个本地电话号码通常是一个 7 位的数字。因此,电话号码的地址空间是从 0 000 000 到 9 999 999。 地址空间也可以是非数字的,以 “.com” 结尾的网络域名的集合也是地址空间。这个地址空间是由所有包含 2~63 个字符并且后面跟着 “.com” 的字符串组成的,组成这些字符串的字符可以是字母、数字和连字符。 到现在你应该已经明白地址空间的概念了,它是很简单的。
有了虚拟地址空间后,CPU 就可以通过生成一个虚拟地址来访问主存,这个虚拟地址在被送到内存之前会先被转换成合适的物理地址,这个虚拟地址到物理地址的转换过程称为 地址翻译/地址转换(address translation)。
地址翻译需要 CPU 硬件和操作系统的密切合作:CPU 上的内存管理单元(Memory Management Unit,MMU)就是专门用来进行虚拟地址到物理地址的转换的,不过 MMU 需要借助存放在内存中的查询表,而这张表的内容正是由操作系统进行管理的。
那么,上述这一套 CPU 生成虚拟地址并进行地址翻译的流程就是虚拟寻址(virtual addressing)。举个例子,看下图:
References
- 《操作系统导论 - [美] 雷姆兹·H.阿帕希杜塞尔》
- 《现代操作系统 - 第 3 版》
- 《深入理解计算机系统 - 第 3 版》