第九章 虚拟存储器
为了更加有效地管理存储器并且少出错,现代系统提供了一种对主存的抽象概念,叫做虚拟存储器。
虚拟存储器提供了三个重要的能力:
1)将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保护活动区域,并根据需要在磁盘和主存之间来回传送数据,通过这个方式,高效地使用了主存。
2)为每个进程提供了一致的地址空间,从而简化了存储器管理
3)保护了每个进程的地址空间不被其他进程破坏
9.1 物理和虚拟寻址
每个字节都有唯一的物理地址。
CPU访问存储器最自然的方法就是使用物理地址——物理寻址
虚拟寻址:CPU通过生成一个虚拟地址来访问主存,这个虚拟地址在被送到存储器之前先替换成适当的物理地址,这个过程叫做地址翻译
地址翻译需要CPU硬件和操作系统之间的紧密合作
存储器管理单元:利用存放在主存中的查询表来动态翻译虚拟地址
9.2 地址空间
地址空间是一个非负整数的有序集合
线性地址空间:地址空间中的整数是连续的
虚拟地址空间:在一个带虚拟存储器的系统里,CPU从一个有N=2的n次方个地址的地址空间中生成虚拟地址(n位地址空间)
物理地址空间:与系统中物理存储器的M个字节相对应
虚拟存储器的基本思想:
允许每个数据对象有多个独立的地址,其中每个地址都选自一个不同的地址空间
主存中的每个字节都有一个选自虚拟地址空间的虚拟地址和一个选自物理地址空间的物理地址
9.3 虚拟存储器作为缓存的工具
VM系统将虚拟存储器分割为虚拟页,大小为P=2的p次方
物理存储区被分割为物理页,大小也为P
虚拟页面的集合分为三个不相交的子集:未分配的、缓存的、未缓存的
9.3.1 DRAM缓存的组织结构
SRAM缓存:位于CPU和主存之间的L1、L2、L3高速缓存
DRAM缓存:虚拟存储器系统的缓存
9.3.2 页表
虚拟存储器系统必须有某种方法来判定一个虚拟页是否存放在DRAM中的某个地方
这个功能由许多软硬件联合提供,其中包括页表
页表将虚拟页映射到物理页
每次地址翻译都要读取页表
页表就是一个页表条目的数组
9.3.3 页命中
9.3.4 缺页
DRAM缓存不命中称为缺页
缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页
在磁盘和存储器之间传送页的活动叫做交换
页从磁盘换入DRAM和从DRAM换出磁盘
按需页面调度:一直等待,直到最后时刻,也就是当有不命中发生时,才换入页面
9.3.5 分配页面
9.3.6 局部性
局部性原则保证了在任意时刻,程序往往在一个较小的活动页面集合上工作——工作集
如果工作集的大小超过物理存储器的大小,就会发生颠簸
9.4 虚拟存储器作为存储器管理的工具
简化链接
简化加载
简化共享
简化存储器分配
9.5 虚拟存储器作为存储器保护的工具
每次CPU生成一个地址时,地址翻译硬件都会读一个PTE
9.6 地址翻译
参数见p542图9-11
地址翻译是一个N元素的虚拟地址空间中的元素和一个M元素的物理地址空间中的元素之间的映射
公式见书p543
页表基址寄存器指向当前页表
n位虚拟地址包含两个部分:一个p位的虚拟页面偏移,一个(n-p)位的虚拟页号
页面命中时,CPU执行的步骤:
1)处理器生成一个虚拟地址,并把它传送给MMU
2)MMU生成PTE地址,并从高速缓存/主存请求得到它
3)高速缓存/主存向MMU返回PTE
4)MMU构造物理地址,并把它传送给高速缓存/主存
5)高速缓存/主存返回所请求的数据字给处理器
处理缺页的步骤
1)到3)相同
4)传递CPU中的控制到操作系统内核中的缺页异常处理程序
5)缺页处理程序确定牺牲页,如果被修改,则换出到磁盘
6)缺页处理程序页面调入新的页面,并更新存储器中的PTE
7)缺页处理程序返回原来的进程,再次执行导致缺页的指令
9.6.1 结合高速缓存和虚拟存储器
详情见p545图9-14
主要思路是地址翻译发生在高速缓存查找之前
9.6.2 利用TLB加速地址翻译
1)CPU产生一个虚拟地址
2)3)MMU从TLB中取出相应的PTE
4)MMU将这个虚拟地址翻译成一个物理地址,并将它发送到高速缓存/主存
5)高速缓存/主存将所请求的数据字返回给CPU
当TLB不命中时,MMU必须从L1缓存中取出相应的PTE
9.6.3 多级页表
用来压缩页表的常用方法是使用层次结构的页表
从两个方面减少了存储器的要求:
1)如果一级页表中的一个PTE是空的,那么相应的二级页表根本不会存在
2)只有一级页表才需要总是在主存中
9.7 案例研究:Intel Core i7/Linux存储器系统
处理器包包括四个核、一个大的所有核共享的L3高速缓存,以及一个DDR3存储器控制器。
每个核包含一个层次结构的TLB、一个层次结构的数据和指令高速缓存,以及一组快速的点到点连接
9.7.1 Core i7地址翻译
Core i7采用四级页表层次结构
与已分配了的页相关的页表都留在存储器中
CR3控制寄存器指向第一级页表的起始位置
CR3的值是每一个进程上下文的一部分
PTE有三个权限位,控制对页的访问
R/W:确定页的内容是读写还是只读
I/S:确定是否能够在用户模式中访问该页
XD:禁止执行
MMU有另外两个位
A位:引用位,实现页替换算法
D位:脏位,告诉内核在拷贝替换页之前是否必须写回牺牲页
9.7.2 Linux虚拟存储器系统
图9-26展示了一个Linux进程的虚拟存储器
1.Linux虚拟存储器区域
一个具体区域的区域结构包含下面的字段:
vm_start:指向这个区域的起始处
vm_end:指向这个区域的结束处
vm_prot:描述这个区域内包含的所有页的读写许可权限
vm_flags:描述这个区域内的页面是与其他进程共享的,还是这个进程私有的
vm_next:指向链表中下一个区域结构
2.Linux缺页异常处理
1)段错误:访问一个不存在的页面
2)正常缺页
3)保护异常:例如,违反许可,写一个只读的页面
9.8 存储器映射
存储器映射:Linux通过将一个虚拟存储器区域与一个磁盘上的对象关联起来,以初始化这个虚拟存储器区域的内容
有两种对象:
1)Unix文件系统中的普通文件
2)匿名文件
9.8.1 共享对象
一个对象可以被映射到虚拟存储器的一个区域,要么作为共享对象,要么作为私有对象
一个映射到共享对象的虚拟存储器区域叫做共享区域,类似地,也有私有区域
9.8.2 fork函数
9.8.3 execve函数
替代的步骤:
1)删除已存在的用户区域
2)映射私有区域
3)映射共享区域
4)设置程序计数器
9.8.4 使用mmap函数的用户级存储器映射
参数prot包含描述新映射的虚拟存储器区域的访问权限位:
PROT_EXEC:这个区域内的页面由可以被CPU执行的指令组成
PROT_READ:这个区域内的页面可读
PROT_WRITE:这个区域内的页面可写
PROT_NONE:这个区域内的页面不能访问
9.9 动态存储器分配
动态存储器分配器维护着一个进程的虚拟存储器区域,称为堆
分配器将堆视为一组不同大小的块的集合来维护,每个块就是一个连续的虚拟存储器片
分配器有两种基本风格:显式分配器,隐式分配器(垃圾收集器)
9.9.1 malloc和free函数
C标准库提供了一个称为malloc程序包的显式分配器,程序通过调用malloc函数来从堆中分配块
malloc函数返回一个指针,指向大小为至少size字节的存储器块
动态存储器分配器,可以通过使用mmap和munmap函数,显式地分配和释放堆存储器
9.9.2 为什么要使用动态存储器分配
9.9.3 分配器的目标与要求
约束条件:
1)处理任意请求序列
2)立即响应请求
3)只使用堆
4)对齐块
5)不修改已分配的块
目标:
1)最大化吞吐率
2)最大化存储器吞吐率:最有用的标准是峰值吞吐率
9.9.4 碎片
造成堆利用率很低的主要原因是一种称为碎片的现象
有两种形式的碎片:内部碎片、外部碎片
9.9.5 实现问题
要考虑的问题:1)空闲块组织 2)放置 3)分割 4)合并
9.9.6 隐式空闲链表
隐式空闲链表:块由一个字的头部、有效载荷,以及一些可能的额外填充物组成。头部后面就是应用调用malloc时请求的有效载荷
9.9.7 放置已分配的块
分配器搜索空闲链表的方式:放置策略
常见策略:首次适配、下一次适配、最佳适配
首次适配:从头开始搜索空闲链表,选择一个合适的空闲块
下一次适配:从上一次查询结束的地方开始搜索
最佳适配:检查每个空闲块,选择适合所需请求大小的最小空闲块
9.9.8 分割空闲块
9.9.9 获取额外的堆存储器
9.9.10 合并空闲块
当分配器释放一个已分配块时,可能有其他空闲块与这个新释放的空闲块相邻
这种邻接的空闲块可以引起一种现象,叫做假碎片
任何实际的分配器都必须合并相邻的空闲块
分配器可以选择立即合并:每次一个块被释放时,就合并所有的相邻块
也可以选择推迟合并:等到某个稍晚的时候再合并空闲块
9.9.11 带边界标记的合并
边界标记:允许在常数时间内进行对前面块的合并
分配器释放当前块时所有可能存在的情况
1)前面的块和后面的块都是已分配的
2)前面的块是已分配的,后面的块是空闲的
3)前面的块是空闲的,而后面的块是已分配的
4)前面的和后面的块都是空闲的
9.9.13 显式空闲链表
空闲链表中块的排序策略:
1)用后进先出的顺序维护链表,将新释放的块放置在链表开始处
2)按照地址顺序来维护链表,其中链表中每个块的地址都小于它后继的地址
9.9.14 分离的空闲链表
分离存储:维护多个空闲链表,其中每个链表中的块有大致相等的大小,用来减少分配时间
所有可能的块大小分为一些等价类,也叫做大小类
两种基本方法:简单分离存储、分离适配
1.简单分离存储
使用简单分离存储,每个大小类的空闲链表包含大小相等的块,每个块的大小就是这个大小类中最大元素的大小
2.分离适配
分配器维护一个空闲链表的数组
每个空闲链表是和一个大小类相关联的
每个链表包含潜在的大小不同的块
3.伙伴系统
分离适配的一种特例,其中每个大小类都是2的幂
9.10 垃圾收集
垃圾收集器是一种动态存储分配器,它自动释放程序不再需要的已分配块,这些块称为垃圾
自动回收堆存储的过程叫做垃圾收集
9.10.1 垃圾收集器的基本知识
垃圾收集器将存储器视为一张有向可达图
该图的节点被分为一组根节点和一组堆节点
每个堆节点对应于堆中的一个已分配块
有向边p->q意味着块p中的某个位置指向块q中的某个位置
根节点对应于这样一种不在堆中的位置,它们中包含指向堆中的指针
9.10.2 Mark&Sweep垃圾收集器
由标记阶段和清除阶段组成
标记阶段标记出根节点的所有可达的和已分配的后继
清除阶段释放每个未被标记的已分配块
9.10.3 C程序的保守Mark&Sweep
已分配块集合维护成一棵平衡二叉树:左子树中的所有块都放在较小的地址处,右子树中的所有块都放在较大的地址处
isPtr(ptr p)函数用树来执行对已分配块的二分查找
平衡树方法保证会标记所有从根节点可达的节点
9.11 C程序中常见的与存储器有关的错误
1)间接引用坏指针
2)读未初始化的存储器
3)允许栈缓冲区溢出
4)假设指针和它们指向的对象是相同大小
5)造成错位错误
6)引用指针,而不是它所指向的对象
7)误解指针运算
8)引用不存在的变量
9)引用空闲堆块中的数据
10)引用存储器泄露
引用资料:《深入理解计算机系统》
遇到的问题:简单分配器的实现那里有点不懂
体会:这一章主要学了和虚拟存储器有关的知识。这学期的最后一个博客任务就这样结束了,成就感与失落感参半,成就感是因为一学期下来,把一本这么厚的书看完了,还写了这么多博客,失落感时因为,虽然花费了好多时间在这一科上,甚至可以说花费了所有科里最多的时间,看上去也学会了不少东西,但反思起来,还是觉得真正学会的并不多,大多数都模棱两可,或者本来学会了后来不知不觉就忘记了。还有一个遗憾是不知道为什么我的电脑怎么也装不上Linux虚拟机,所以听说接下来的考试都要进行代码的编译,不知道该怎么办才好。。。