前言
继续阅读《深入理解计算机系统》这本经典书籍
本节是第九章
虚拟内存
虚拟内存为每个进程提供一个大的、一致的、私有的地址空间
- 将主存看成一个存储在磁盘上的地址空间的高速缓存
- 为每个进程提供一致的地址空间
- 保护每个进程的地址空间不被其他进程破坏
1、物理和虚拟寻址
物理寻址是CPU访问内存最自然的方式
现在都是用虚拟寻址
内存管理单元(MMU)利用存放在主存中的查询表动态翻译虚拟地址
主要有以下几点考虑
- 可以更有效率的使用内存:使用 DRAM 当做部分的虚拟地址空间的缓存
- 简化内存管理:每个进程都有统一的线性地址空间
- 隔离地址控件:进程之间不会相互影响;用户程序不能访问内核信息和代码
2、地址空间
地址空间:非负整数地址的有序集合
线性地址空间:整数连续
虚拟地址空间:CPU从一个N=2^n个地址的地址空间中生成虚拟地址
物理地址空间:对应物理内存的M个字节
3、虚拟内存作为缓存的工具
虚拟内存就是存储在磁盘上的 N 个连续字节的数组
这个数组的部分内容,会缓存在 DRAM 中
在 DRAM 中的每个缓存块(cache block)就称为页(page)
类似缓存,我们需要尽可能从DRAM读取数据:
- 更大的页尺寸(page size):通常是 4KB,有的时候可以达到 4MB
- 全相联(Fully associative):每一个虚拟页(virual page)可以放在任意的物理页(physical page)中,没有限制。
- 映射函数非常复杂,所以没有办法用硬件实现,通常使用 Write-back 而非 Write-through 机制
- Write-through: 命中后更新缓存,同时写入到内存中
- Write-back: 直到这个缓存需要被置换出去,才写入到内存中(需要额外的 dirty bit 来表示缓存中的数据是否和内存中相同,因为可能在其他的时候内存中对应地址的数据已经更新,那么重复写入就会导致原有数据丢失)
做法:页表
- 实际上是一个数组
- 数组中的每个元素称为页表项(PTE, page table entry)
- 每个页表项负责把虚拟页映射到物理页上
那查询时
- 命中:访问到页表中蓝色条目的地址时,因为在 DRAM 中有对应的数据,可以直接访问。
- 不命中:访问到 page table 中灰色条目的时候,因为在 DRAM 中并没有对应的数据,所以需要执行一系列操作(从磁盘复制到 DRAM 中):
- 触发 Page fault,也就是一个异常
- Page fault handler 会选择 DRAM 中需要被置换的 page,并把数据从磁盘复制到 DRAM 中
- 重新执行访问指令,这时候就会是 page hit
- 复制过程中的等待时间称为 demand paging。
看起来效率似乎不高
但局部性原则使得程序趋向于在一个较小的活动页面集合上工作
4、虚拟内存作为内存管理的工具
如前所说
每个进程都有自己的虚拟地址空间
映射如下
一个好处是
如果两个进程间有共享的数据,那么直接指向同一个物理页即可
且有以下几点
- 简化链接
- 简化加载
- 简化共享
- 简化内存分配
5、虚拟内存作为内存保护的工具
页表中的每个条目的高位部分是表示权限的位
MMU 可以通过检查这些位来进行权限控制(读、写、执行)
6、地址翻译
一些符号
一个例子
- 通过虚拟地址找到页表(page table)中对应的条目
- 检查有效位(valid bit),是否需要触发页错误(page fault)
- 然后根据页表中的物理页编号(physical page number)找到内存中的对应地址
- 最后把虚拟页偏移(virtual page offset)和前面的实际地址拼起来,就是最终的物理地址了
如果是命中
如果是缺页
想要更快
直接在 MMU 进行一部分的工作
有了Translation Lookaside Buffer(TLB)
v*n + VPO 就是虚拟地址
分成三部分,分别用于匹配标签、确定集合
如果 TLB 中有对应的记录,那么直接返回对应页表项(PTE)即可
如果没有的话,就要从缓存/内存中获取,并更新 TLB 的对应集合
多级页表
因为往往虚拟地址的位数比物理内存的位数要大得多
所以保存页表项(PTE) 所需要的空间也是一个问题
故采用多级页表
7、动态内存分配
动态内存分配器会管理一个虚拟内存区域,称为堆(heap)
分配器以块为单位来维护堆,可以进行分配或释放
有两种类型的分配器:
- 显式分配器:应用分配并且回收空间(C 语言中的 malloc 和 free)
- 隐式分配器:应用只负责分配,但是不负责回收(Java 中的垃圾收集)
关于malloc和free的调用
分配器有以下限制
- 不能控制已分配块的数量和大小
- 必须立即响应 malloc 请求(不能缓存或者给请求重新排序)
- 必须在未分配的内存中分配
- 不同的块需要对齐(32 位中 8 byte,64 位中 16 byte)
- 只能操作和修改未分配的内存
- 不能移动已分配的块
在这些约束下,分配器试图实现吞吐率最大化和内存使用率最大化
造成堆利用率低的主要原因是碎片 of course
-
内部碎片:对于给定的块,如果需要存储的数据(payload)小于块大小,就会因为对齐和维护堆所需的数据结构的缘故而出现无法利用的空间
-
外部碎片:内存中有足够的空间,但是空间不连续,所以成为了碎片
8、垃圾收集
垃圾收集器
- 一种动态内存分配器
- 自动释放程序不再需要的已分配块
- 将内存视为一张有向可达图,释放不可达节点
9、内存相关错误
1、间接引用坏指针
一个例子如下
错误的写为
2、读未初始化的内存
3、允许栈缓冲区溢出
必须用fgets函数限制输入串的大小
4、假设指针和他们指向的对象大小相同
5、错位错误
6、引用指针而不是它所指的对象
7、误解指针运算
8、引用不存在的变量
9、引用空闲堆块中的数据
10、内存泄漏
结语
主要了解虚拟内存是怎么一回事儿
它的一些功能与作用
略过了动态内存分配的一些详细操作