1. 虚拟内存

130 阅读6分钟

物理内存

在早期的 CPU 指令集里,从内存中加载数据,向内存中写入数据都是直接操作物理内存。也就是说每一个数据存储在内存的什么位置,都由程序员自己负责。

但是直接访问物理内存,存在着一个很大的问题。

内存不不够用该怎么办?每个进程分配多少内存?如何保证指令中访存地址的正确性?

很明显这些问题由程序员负责是难以忍受的。这个时候我们要怎么办呢?

基于局部性原理,CPU 为程序员虚拟化了一层内存,我们只需要与虚拟内存打交道就可以了。

局部性原理

在绝大多数程序的运行过程中,当前指令大概率会引用最近访问过的数据。即程序的数据访问会表现出明显的倾向性。这种倾向性就是局部性原理。

可以从两个方面来理解局部性原理。第一个方面是时间局部性,即被访问过一次的内存位置很可能在不远的将来会被再次访问;另一方面是空间局部性,说的是如果一个内存位置被引用过,那么它邻近的位置在不远的将来也有很大概率会被访问。

基于这个原理,我们可以做出一个合理的推论:无论一个进程占用的内存资源有多大,在任一时刻,它需要的物理内存都是很少的。在这个推论的基础上,CPU 为每个进程只需要保存很少的内存就可以保证进程的正常执行。

为了让程序员编程方便,CPU 和OS还联手编织了一个假象:每个进程都独享 128T 的虚拟内存空间,并且地址空间都是相互隔离的。比如说进程 A 中有个变量 a,它的地址是 0x100,进程 B 中也有个变量 b,它的地址也是 0x100。但这并不会造成冲突。

可以对比下直接操作物理内存和操作虚拟内存,程序员要关心的事情都有哪些。

操作物理内存的情况下,需要知道每一个变量的位置都安排在了哪里,还要注意不能与其他进程地址冲突。项目中成百万的变量和函数,都要给它计算一个合理的位置,还不能与其他进程冲突,这是根本不可能完成的任务。

而操作虚拟内存的情况就简单多了。你可以独占 128T 内存,其他进程与我们没有关系。为变量和函数分配地址的活交给链接器去自动安排。这一切都是因为虚拟内存能够提供内存地址空间的隔离,极大地扩展了可用空间。

这是什么意思呢?虚拟内存不仅让每个进程都有独立的、私有的内存空间,而且这个地址空间比可用的物理内存要大得多。不过任何虚拟内存最终都要映射到物理内存,但虚拟内存的大小又远超真实的物理内存的大小。具体是怎么做到的呢?

虚拟内存与程序局部性原理

答案很简单,CPU 充分利用程序局部性原理,提出了虚拟内存和物理内存的映射 (Mapping) 机制。OS管理着这种映射关系,所以在写代码时就不用再操心物理内存的使用情况,你看到的内存就是虚拟内存。

映射关系以页为单位。

image.png

虽然虚拟内存提供了很大的空间,但进程启动之后,这些空间并不是全部都能使用的。开发者必须要使用 malloc 等分配内存的接口才能将内存从待分配状态变成已分配状态。

在得到一块虚拟内存后,这块内存是未映射状态,它并没有被映射到相应的物理内存,直到对该块内存进行读写时,os才为它分配物理内存。

在虚拟内存中连续的页面,在物理内存中不必是连续的。只要维护好从虚拟内存页到物理内存页的映射关系,就能正确地使用内存了。映射关系是os通过页表来自动维护的,不必操心。

计算机的虚拟内存大小是不一样的。虚拟地址空间往往与机器字宽有关系。例如 32 位机器上,指向内存的指针是 32 位的,所以它的虚拟地址空间是 2 的 32 次方,也就是 4G。在 64 位机器上,指向内存的指针就是 64 位的,但在 64 位系统里 只使用了低 48 位,所以它的虚拟地址空间是 2 的 48 次方,也就是 256T。

页表的结构

页表的本质是页表项 (Page Table Entry, PTE) 的数组,虚拟空间中的每一个页在页表中都有一个 PTE 与之对应,PTE 中会记录这个虚拟内存页所对应的实际物理页的起始地址。

image.png

每个页表项都是 4 字节。人们将 1024 个页表项组成一张页表。大小刚好是 4K,占据一个内存页,这样就更加方便管理。当前市场上主流的处理器也都选择将页大小定为 4K。 一个页表项对应着一个大小为 4K 的页,1024 个页表项所能支持的空间就是 4M。为了编码更多地址,必须使用更多的页表。而且,为了管理这些页表,引入了页表的数组:页目录表。

页目录表中的每一项叫做页目录项 (PDE),每个 PDE 都对应一个页表,它记录了页表开始处的物理地址,这就是多级页表结构。现代的 64 位处理器上,为了 编码更大的空间,还存在更多级的页表。

image.png

一个 CPU 怎么找到真实地址?

以32bit机器为例

image.png

CPU先从页目录基址寄存器(CR3)得到最高级页目录表的基地址。32位的虚拟地址拆成10、10、12位 3段,在目录表基址上加上高10位的值乘4(一个页目录项占4字节,1024个页目录项组成1页,刚好需要10bit进行编码),现在找到页表的位置,和上面一样用中10位定位到页表项地址。最后通过页表项存的地址+低12位的地址进行偏移(12位刚好可以对一页内的所有字节编码:4096字节)。

而64位使用48位虚拟地址,使用4级页表,和32位的3级页表相似