虚拟内存

前面有提到过, 操作系统要完成多个进程中的隔离, 想要使得两个程序达成隔离, 从内存方面就必须达成隔离, 程序A不能访问到程序B所属的内存, 才是我们想要的隔离.

在我们学习编写程序时, 时常能够看到一些地址及其操作. 例如像0x12345678这样的地址. 在计算机中, 从程序员的角度来看, 我们能够通过某个地址访问到我们程序中的数据或代码. 按照正常的物理分配, 程序加载到内存中应该是这样的:

物理内存

无疑这样设置内存结构是不安全的, 但是真实的物理内存中就是这样的结构.

假设我是程序B, 我知道自己的地址空间位于0x80000x9000之间, 依次类推, 我就猜测0xA0000xB000也有一个程序, 这样完全起不到隔离的作用. 我可以将一个脏数据写到别的程序那去.

然后就可能会有想到这样的实现, 在程序访问内存时做有效性校验,判断程序读写的区域是否属于自身. 实现上这样确实会避免许多不安全操作, 但是在每次读写操作前校验安全性给CPU带来很大的性能开销, 另外也可能在内存中有前一个应用留下来的脏数据影响程序运行.

于是, 计算机科学家们提出了虚拟地址的概念. 不仅让每个程序拥有了一个完整的空间, 使得整个程序可以在自身的虚拟空间内随意操作, 更是完成了进程间的内存空间隔离.

操作系统将物理地址映射到虚拟地址上, 由进程操作虚拟地址, 操作系统就可以对对应的物理地址操作. 各个进程间是没办法相互访问到了, 因为大家都有0x0000, 而这些0x0000 实际上对应的是不同位置的物理内存, 于是现在内存就变成了这样:

虚拟地址映射

地址空间

于是在我们使用程序时, 就存在两种不同叫法的地址空间 (实际上是相同的位置).

  • 程序所读写的内存地址叫做虚拟内存地址.
  • 操作系统实际读写的内存地址叫做物理内存地址.

每个进程在初始化的时候, 操作系统会分配出一块地址空间给进程, 当进程访问虚拟地址时, CPU会通过MMU(内存管理模块) 将虚拟地址转换为物理地址, 再使用物理地址访问内存.

MMU地址转换

实际上操作系统对内存的管理是更加细粒化的, 我们不难发现, 假设我们电脑有4G的内存, 映射出来最底下的空间应该为0x00000000, 一共有32个二进制位代表的地址. 如果我们想要从$2^{32}$个地址中直接寻找一个地址, 无疑开销是非常大的.

因此产生了内存分段内存分页两种内存管理方式, 我认为其中的基本思想是分层映射.

内存分段

程序在编译时分为代码分段, 数据分段, 栈段和堆段. 不同的段需要有不同的读写执行属性,

页式硬件

页表

内存管理单元 : 虚拟地址转换物理地址

内存管理单元 -> 页表

CPU寄存器satp -> 页表 -> 内存

硬件采取多级树实现页表