虚存和地址转换
计算机存储体系结构
冯·诺依曼结构
层次化存储结构
- 内存DRAM:快、容量小、易丢失、昂贵
- 外存磁盘:慢、容量大、不易丢失、便宜
需求一:进程保护
- 一个进程出错不能影响到其他进程
- 需要对每个内存访问都进行检查,保证内存访问都合法
需求二:扩展内存和应用透明
- 一个进程必须能运行再不同的物理内存区域上
- 一个进程必须能运行在不同的物理内存大小上
问题:如何高效地使用内存空间?
- 目标1:同时运行多个进程
- 系统能够运行的进程越多越好
- 目标2:地址空间足够大
- 一个大进程,其大小超过物理内存
- 多个小进程,其大小总和超过物理内存
- 目标3:保护
- 一个用户进程不能读取和修改其他用户进程的内存
- 用户进程不能破坏内核使用的内存空间
解决方案:虚拟内存
- 基本内存抽象
- 地址空间:进程的内存视图 -> 虚拟内存
- 透明使用、高效访问、安全保护
- 虚拟内存 vs 虚拟CPU
- 虚拟CPU
- 进程不与CPU绑定,可以迁移到任一CPU
- 进程执行时,自以为独占了CPU
- 进程CPU状态与切换:通过上下文
- 虚拟内存
- 进程数据不与物理内存绑定,可以迁移到任意的内存/磁盘空间
- 进程执行时,自以为独占了内存
- 进程内存状态:通过swap到磁盘上进行保存
- 虚拟CPU
虚拟内存
- 独立的(进程)地址空间
- 给每个进程提供一个很大的、静态的"虚拟"内存空间
- 虚实地址转换
- 进程运行时,每次访存通过地址转换获得实际的物理内存地址
- 磁盘作为内存的延展(磁盘交换区)
- 按需加载:只装载部分地址空间到物理内存
地址空间
- 独立的进程地址空间 [0,MAX-1]
- 程序员能够看到的是虚地址
- 运行时装载部分地址空间
- 每次访存:虚地址->物理地址
- CPU 和 进程 看到的是虚地址
- 内存 和 I/O设备 看到的是物理地址
- 如果访问到未装载的地址空间
- 通过OS将它加载进内存
虚存的好处
- 灵活
- 进程在执行时才会被放进内存,另一部分在内存/磁盘中
- 简单
- 进程的内存访问变得简单
- 高效
- 20/80原则:20%的地址空间承担了80%的访问
- 将20%地址空间放进物理内存
- 安全
- 虚实地址转换时进行安全检查,防止非法访问
地址映射
目标
- 隐式:对每个内存访问,转换是隐式的,用户程序不感知
- 快速:有转换关系的时候,访存速度非常快
- 例外:没有转换关系时会触发一个例外
- 保护:能够隔离用户进程的错误
地址映射和粒度
- 需要某种"映射"机制
- 把虚地址空间(大)的内容 放进 物理内存空间(小)
- 映射必须有合适的粒度
- 粒度决定灵活性
- 大粒度映射可能造成内存浪费
- 细粒度映射则需要更多的映射信息
- 极端情况
- 字节粒度映射:映射表过大
- 进程粒度映射:内存浪费
基址 + 长度(Cray-1中采用的方法)
- 连续分配
- 为每个进程分配地址连续的内存空间
- 用一个二元组来限定其内存区域: <base,bound>
- 保护
- 一个进程只能访问[base,base+bound]区间的内存
- 上下文切换
- 保存/恢复 基址寄存器
- 好处
- 简单:映射时将虚地址和基地址相加
- 支持换出(swapping):多进程并发执行
- 坏处
- 外部碎片(进程间的碎片)
- 难以支持进程增大
- 难以共享内存
内存碎片:随着进程的换入与换出,内存会产生很多空洞(未使用的小区域)
分段
- 不连续分配
- 把程序逻辑上划分为若干段:代码、全局变量、栈
- 每个段分配连续内存,段间不必连续
- 每个进程有一张段表
- 每个段采用基址 + 长度
- 保护
- 每个段有不同的权限(Read,Write,Exec)
- 上下文切换
- 保存/恢复 段表和指向段表的内核指针
- 好处
- 相比于基址 + 长度,资源使用更高效
- 易共享
- 不足
- 管理的复杂度增加
- 仍然存在外部碎片(段间碎片)
虚实地址转换过程
Virtual Address = Seg# + Offset Seg# 通过 Segment Table 映射到 Physical Address
如果 offset > 段Size, 则触发 Memory Violation异常 如果访问地址所在的 Seg# 的 Valid 为0,则触发 Segment Fault异常
分页
- 分页机制
- 使用固定大小的映射单元
- 把虚存划分成固定大小的单元(称为页 Page)
- 把物理内存划分成同样大小的单元(称为页框 Page Frame)
- 按需加载(On-Demand Paging)
- 用页表来记录映射
- 虚页 -> 物理页
- 每个表项都有若干个控制位
- 按页进行保护(Read,Write,Exec)
- 上下文切换
- 与分段类似,保存/恢复页表及其地址
- 好处
- 分配简单
- 易共享
- 坏处
- 页表很大
- 进程地址空间有很多空洞:对应的页表项没用
页表项
- 表达一个映射关系(Page Table Entry,PTE)
- 虚页号 -> 物理页号
- 包含如下信息:
- 物理页框号
- 有效位:标志该页是否在内存中
- 保护位:标志该页的访问权限(Read,Write,Exec)
- 修改位:标志该页是否被修改过
- 访问位:标志该页是否被访问过
- 缓存位
- PTE的数量
- 假设一页是 4K(12位页内偏移Offset)
- 32位虚拟地址空间:2^32 = 4G = 4K * 1024
- 一个PTE的大小为32位 = 4B,故总共为 4MB
- 如果有10K个进程,那么可能内存连页表都放不下
- 64位虚拟地址空间:2^64 = 16EB = 4K * 1024 * 1024 * 1024
- 一个PTE的大小为64位 = 8B,故总共为 8TB
- 内存连一个页表的大小都放不下
分段 + 分页
- 先将进程划分为若干段
- 每个段采用分页
- 段表记录它的页表地址
- 不足:仍然存在分段的不足
多级页表
- 虚地址除去 offset 之外的部分划分为多个段
- 每段对应一级页表
- 多个页表
- 好处:节省空间
示例:二级页表
- 虚地址:32位,前10位为一级页表索引,中间10位为二级页表索引,后12位为页内偏移
- 每个页表4KB,共有1024个页表项,下级页表每一项映射1页(4KB),上级页表映射1024页(4MB)
- 对于大地址空间,大部分程序都只需要几个页表
反向页表
- 基本逻辑
- 64位地址空间,4K页,共有252个页;256MB物理内存,总共有216个页框
- 按物理页索引,记录每个物理页对应的进程ID及虚页
- 主要思想
- 每个物理页对应一个PTE
- 通过Hash查找进行地址转换(Hash(Vpage,pid) -> Ppage#)
- 好处
- 页表大小与地址空间大小无关,只与物理内存大小有关
- 对于大地址空间,页表较小
- 坏处
- 查找困难(Hash冲突)
- 管理哈希链需要大量开销
MMU 和 TLB
地址转换实现
- 地址类别
- CPU发出的是虚地址
- 内存和IO设备接收的是物理地址
- MMU(Memory Management Unit,内存管理单元)职责
- 负责虚地址到物理地址转换的硬件单元,通常在片内实现
- 虚存地址转换到物理地址(每条Load 和 Store指令都需要地址转换)
- 内存保护,检查地址是否有效
- 特殊指令操作对应寄存器,记录Base/Bound,或者页目录
- 操作系统职责
- 内存管理:新进程分配空间,结束的进程回收空间
- 进程切换时上下文管理(RISC-V satp寄存器等)
- 异常处理:内存越界访问、无效地址等
加速地址转换
- 程序只知道虚地址
- 每个程序或进程的地址空间是[0,max-1]
- 每个虚地址必须要进行转换
- 可能需要逐级查找多级页表
- 页表保存在内存中,一个内存访问可能变成多个内存访问
- 解决办法
- 用速度更快的部件来缓存使用最频繁的那部分页表项
TLB(Translation Look-aside Buffer,地址转换旁路缓冲)
TLB是一种页表的缓冲机制
- 共有的(必须的)位
- VP#(虚页号):与虚地址进行匹配
- PP#(物理页号):转换后的实际地址
- Valid位:标志此表项是否有效
- 访问控制位:运行内核/用户访问,以及允许何种访问(Read,Write)
- 可选的(有用的)位
- 进程标签(pid)
- 访问标签位(R位)
- 修改标志位(M位)
- 缓存标志位
TLB具体流程(硬件和软件不同)
- CPU先把一个虚地址VA给MMU进行转换
- MMU先查TLB: VA = VP# || offset
- 将该虚页号同时与TLB中所有的表项进行比较(硬件支持)
- TLB hit:TLB里找到含 VP# 的表项
- 如果有效(TLB的Valid位=1),取表项中的物理页号
- 如果无效(TLB的Valid位=0),则等同于TLB miss
- 如果TLB miss(不命中):TLB中没有含有 VP# 的表项
- MMU硬件在页表中进行查找,得到PTE / 进入内核异常处理程序,软件在页表中进行查找,得到PTE
- 将找到的PTE加载进TLB
- 如果没有空闲表项,则替换一个TLB表项
- 获取TLB表项中的物理页号 / 重新执行TLB不命中的指令
硬件控制 vs 软件控制
- 硬件控制
- 高效
- 不灵活
- 软件控制
- 简化 MMU 的逻辑,是的 CPU芯片上有更多面积可以用于缓存
- 软件控制更加灵活
- 可以使用反向页表进行映射,处理大的虚地址空间
TLB 设计问题
- 替换哪个TLB表项
- 伪随机 或 LRU算法
- 上下文切换时需要做什么
- 有进程标签:修改TLB寄存器和进程寄存器的内容
- 无进程标签:将整个TLB的内容置于无效
- 修改一个页表项时需要做什么
- 修改内存中的PTE
- 将对应的TLB表项置于无效(刷新TLB)
- TLB的大小
- 很小的TLB(64项),能够有很好的TLB命中率
- 不能太大(不超过256项),CPU的面积有限