文章目录
- 1. 为什么需要引入虚拟内存
- 2. 如何实现虚拟地址到物理地址的映射
- 2.1 内存分段
- 2.1.1 分段机制
- 2.1.2 内存分段的不足之处
- 2.2 内存分页
- 2.2.1 分页机制
- 2.2.2 单级页表
- 2.2.3 多级页表
- 2.2.2 如何解决了外部碎片和内存交换效率低的问题
- 2.3 段页式
1. 为什么需要引入虚拟内存
- 物理内存无法最大化被利用
每个进程所需要的内存空间不是固定的,会动态变化,所以导致分配的多余的空间被浪费
操作系统内部对虚拟的内存地址和真实的物理内存地址建立映射,程序基于虚拟地址实现。
另外还有读时共享,写时复制的机制提高内存的利用
- 程序逻辑内存空间独立使用
当N个程序同时使用一块物理地址空间时,可能会有读写冲突。
- 内存不够时可以虚拟化磁盘空间。
2. 如何实现虚拟地址到物理地址的映射
进程持有的虚拟地址会通过 CPU 芯片中的内存管理单元(MMU)的映射关系,来转换变成物理地址,然后再通过物理地址访问内存。
2.1 内存分段
2.1.1 分段机制
程序是由若干个逻辑分段组成的,如可由代码分段、数据分段、栈段、堆段组成。
分段机制下虚拟地址和物理地址的映射:
分段机制下的虚拟地址由两部分组成,段选择因子和段内偏移量。
段表中存有段的基地址和段界限等信息。
如果要访问段 3 中偏移量 500 的虚拟地址,我们可以计算出物理地址为,段 3 基地址 7000 + 偏移量 500 = 7500。
2.1.2 内存分段的不足之处
- 会产生外部碎片
- 内存交换效率低
为了解决外部碎片的问题,需要将内存中的数据写到硬盘,再读回来。而硬盘的访问速度要比内存慢太多了。
2.2 内存分页
2.2.1 分页机制
分页是把整个虚拟和物理内存空间切成一段段固定尺寸的大小。这样一个连续并且尺寸固定的内存空间,我们叫页(Page)。在 Linux 下,每一页的大小为 4KB。
页表是存储在内存里的,内存管理单元 (MMU)负责将虚拟内存地址转换成物理地址。
而当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常,进入系统内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行。
2.2.2 单级页表
在分页机制下,虚拟地址分为两部分,页号和页内偏移。页号作为页表的索引,页表包含物理页每页所在物理内存的基地址。
不足之处:在 32 位的环境下,虚拟地址空间共有 4GB,假设一个页的大小是 4KB(2^12),那么就需要大约 100 万 (2^20) 个页,每个「页表项」需要 4 个字节大小来存储,那么整个 4GB 空间的映射就需要有 4MB 的内存来存储页表。每个进程都有自己的页表,进程多起来之后是挺占内存的。
2.2.3 多级页表
一级页表存储的是一级也好到二级页表的关联信息,二级页表存储的是二级页号到物理页号的关联信息。
分了二级表,映射 4GB 地址空间就需要 4KB(一级页表)+ 4MB(二级页表)的内存。
如果使用了二级分页,一级页表就可以覆盖整个 4GB 虚拟地址空间,但如果某个一级页表的页表项没有被用到,也就不需要创建这个页表项对应的二级页表了,即可以在需要时才创建二级页表。
我们从页表的性质来看,保存在内存中的页表承担的职责是将虚拟地址翻译成物理地址。假如虚拟地址在页表中找不到对应的页表项,计算机系统就不能工作了。所以页表一定要覆盖全部虚拟地址空间,不分级的页表就需要有 100 多万个页表项来映射,而二级分页则只需要 1024 个页表项。
对于 64 位的系统,两级分页肯定不够了,就变成了四级目录,分别是:
- 全局页目录项 PGD(Page Global Directory);
- 上层页目录项 PUD(Page Upper Directory);
- 中间页目录项 PMD(Page Middle Directory);
- 页表项 PTE(Page Table Entry);
多级页表虽然解决了空间上的问题,但是虚拟地址到物理地址的转换就多了几道转换的工序,这显然就降低了这俩地址转换的速度,也就是带来了时间上的开销。
CPU 芯片中,加入了一个专门存放程序最常访问的页表项的 Cache,这个 Cache 就是 TLB(Translation Lookaside Buffer)。
有了 TLB 后,那么 CPU 在寻址时,会先查 TLB,如果没找到,才会继续查常规的页表。TLB 的命中率其实是很高的,因为程序最常访问的页就那么几个。
2.2.2 如何解决了外部碎片和内存交换效率低的问题
采用了分页,页与页之间是紧密排列的,所以不会有外部碎片。但是因为内存分页机制分配内存的最小单位是一页,即使程序不足一页大小,我们最少只能分配一个页,所以页内会出现内部碎片。
如果内存空间不够,操作系统会把其他正在运行的进程中的「最近没被使用」的内存页面给释放掉,也就是暂时写在硬盘上,称为换出(Swap Out)。一旦需要的时候,再加载进来,称为换入(Swap In)。所以,一次性写入磁盘的也只有少数的一个页或者几个页,不会花太多时间,内存交换的效率就相对比较高。
分页的方式使得我们在加载程序的时候,不再需要一次性都把程序加载到物理内存中。我们完全可以在进行虚拟内存和物理内存的页之间的映射之后,并不真的把页加载到物理内存里,而是只有在程序运行中,需要用到对应虚拟内存页里面的指令和数据时,再加载到物理内存里面去。
2.3 段页式
段页式内存管理实现的方式:
先将程序划分为多个有逻辑意义的段,也就是前面提到的分段机制;
接着再把每个段划分为多个页,也就是对分段划分出来的连续空间,再划分固定大小的页;
这样,地址结构就由段号、段内页号和页内位移三部分组成。
用于段页式地址变换的数据结构是每一个程序一张段表,每个段又建立一张页表,段表中的地址是页表的起始地址,而页表中的地址则为某页的物理页号。
段页式地址变换中要得到物理地址须经过三次内存访问:
- 第一次访问段表,得到页表起始地址;
- 第二次访问页表,得到物理页号;
- 第三次将物理页号与页内位移组合,得到物理地址。
参考资料:
小林coding 《图解系统》
刘丹冰 《深入理解Go语言》