虚拟存储器
- 虚拟存储器是硬件异常、硬件地址翻译、主存、磁盘文件和内核软件的完美交互。
- 虚拟存储器的特点:
- 中心的
- 强大的
- 危险的
物理和虚拟寻址
- 计算机系统的主存被组织成一个由M个连续的字节大小的单元组成的数据
- 每个字节都有一个唯一的物理地址
- 第一个字节的地址为0,接下来的抵制依次+1
- 这种方式称为物理寻址
- 虚拟寻址时,CPU通过生成一个虚拟地址来访问主存,该地址被送到存储器之前先转换成适当的物理地址。该任务叫做地址翻译。
- 地址需要CPU硬件和操作系统之间紧密合作
- 存储器管理单元(在CPU上)利用存放在主存中的查询表来动态翻译虚拟地址,该表的内容是由操作系统管理的。
地址空间
- 空间地址是一个非负整数地址的有序集合:{0,1,2,…}
- 如果地址空间中的整数是连续的,则成为线性地址空间。
- CPU从一个有N=2^n个地址的抵制空间中生成虚拟抵制,这个抵制空间叫做虚拟地址空间。
- 一个包含N=2^n个地址的虚拟地址空间叫做一个n位地址空间。
- 除了一个虚拟地址空间,还有一个物理地址空间,它与系统中物理存储器的M个字节相对应:{0,1,2,…,M-1}
- M不一定是2的幂。
虚拟存储器作为虚拟的工具
- 唯一的虚拟抵制是作为到数组的索引的。
- 磁盘上数据的内容被缓存在主存中。
- VM系统通过将虚拟存储器分割为称为虚拟页(VP)。
- 每个虚拟页的大小为P=2^P字节。
- 物理存储器被分割为物理页(PP,也称为页帧),大小为P字节。
- 在任何时刻,虚拟页面的三个子集:
- 未分配的:VM系统还未分配的页
- 缓存的:当前缓存在物理存储器中的已分配页
- 未缓存的:没有缓存在物理存储器中的已分配页
DRAM缓存的组织结构
- 用SRAM缓存来表示位于CPU和主存之间的L1、L2和L3高速缓存
- 用DRAM缓存来表示虚拟存储器系统的缓存,它在主存中缓存虚拟页
- DRAM缓存的组织结构完全是由巨大的不命中开销驱动的。
页表
- 页表将虚拟页映射到物理页。
- 每次地址翻译硬件将一个虚拟地址转换为物理地址时都会读取页表。
- 操作系统负责维护页表的内容,以及在磁盘与DRAM之间来回传送页。
- 页表就是一个页表条目的数组。
- 虚拟抵制空间中的每个页在页表中叫做页表
- 虚拟抵制空间中的每个页在页表中一个固定偏移量处都有一个PTE。
缺页
- DRAM缓存不命中称为缺页。
- 在虚拟存储器的习惯说法中,块被称为页。
- 在磁盘和存储器之间传送页的活动叫做交换或者页面调度。
- 页从磁盘换入DRAM和从DRAM换出磁盘,当有不命中发生时,才换入页面的这种策略称为按需页面调度。
- 得益于局部性,虚拟存储器工作得相当好。
- 局部性原则保证了在任何时刻,程序将往往在一个较小的活动页面集合上工作,这个集合叫做工作集或者常驻集。
- 不是所有的程序都能展现良好的时间局部性,如果工作集的大小超出了物理存储器的大小,那么程序将产生不幸的状态,叫做点播,这时页面将不断地换进换出。
- 可以利用Unix的getrusage函数监测缺页的数量。
虚拟存储器作为存储器管理的工具
- VM简化了链接和加载、代码和数据共享,以及应用程序的存储器分配。
- 简化链接:独立的地址空间允许每个进程的存储器映像使用相同的基本格式,而不管代码和数据实际存放在物理存储器的何处。
- 简化加载:虚拟存储器还使得容易向存储器中加载可执行文件和共享对象文件。
- 简化共享:独立地址空间为操作系统提供了一个管理用户进程和操作系统自身之间共享的一致机制。
- 简化存储器分配:虚拟存储器为向用户进程提供一个简单的分配额外存储器的机制。
- 分配虚拟页的一个连续的片(chunk),从地址0x08048000处开始(对于32位地址空间),或者从0x400000处开始(对于64位地址空间)。
虚拟存储器作为存储器保护的工具
- 如果一条指令违反了许可条件,那么CPU就除法一个一般保护故障,将控制传递给一个内核中的异常处理程序。Unix外壳一般将这种异常报告为段错误。
地址翻译
- 地址翻译符号小结(p543):
- 地址翻译是一个N元素的虚拟地址空间(VAS)中的元素和一个M元素的物理空间地址(PAS)中元素之间的映射,MAP:VAS→PAS∪Ø
- CPU中的一个控制寄存器,页表基址寄存器(PTBR),指向当前页表。
- n位的虚拟地址包含两个部分:
- 一个p位的虚拟页面偏移(VPO)
- 一个(n-p)位的虚拟页号(VPN)
- MMU利用VPN来选择适当的PTE。
- 当页面命中时,CPU硬件执行的步骤:
- 第一步:处理器生成一个虚拟地址,并把它传送给MMU
- 第二步:MMU生成PTE地址,并从高速缓存/主存请求得到它。
- 第三步:高速缓存/主存向MMU返回PTE
- 第四步:MMU构造物理地址,并把它传送高速缓存/主存
- 第五步:高速缓存/主存返回所请求的数据字给处理器。
- 处理缺页要求硬件和操作系统内核协作完成:
- 第一步到第三步与以上相同
- 第四步:PTE中的有效位是零,所以MMU触发了一次异常。传递CPU中的控制到操作系统内核中的缺页异常处理程序。
- 第五步:缺页处理程序确定出物理存储器中的牺牲页,如果这个页面已经被修改了,则把它换出到磁盘。
- 第六步:缺页处理程序页面调入新的页面,并更新存储器中的PTE。
- 第七步:缺页处理程序返回到原来的进程,再次执行导致缺页的指令。CPU将引起缺页的虚拟地址重新发送给MMU。
利用TLB加速地址翻译
- 翻译后备缓冲器(TLB)
- TLB是一个小的、虚拟寻址的缓存,期中每一行都保存着一个由单个PTE组成的块。
- TLB通常有高度的相连性。
- 如果TLB=2^t个组,那么TLB索引是由VPN的t个最低位组成的,而TLB标记是由VPN中剩余的位组成的。
- 当TLB命中时的关键:所有的抵制翻译步骤都是在芯片上的MMU中执行的 ,因此非常快。
- 第一步:CPU产生一个虚拟地址
- 第二步和第三步:MMU从TLB中取出相应的PTE
- 第四步:MMU将这个虚拟地址翻译成一个物理地址,并且将它发送到高速缓存/主存
- 第五步:高速缓存/主存将所请求的数据字返回给CPU
Linux虚拟存储器区域
- Linux将虚拟存储器组织成一些区域(段)的集合。
- 一个区域是已经存在着的(已分配的)虚拟存储器的连续片(chunk)。
- 区域的概念允许虚拟地址空间有间隙。
- 内核为系统中的每个进程维护一个单独的任务结构。
- 任务结构中的元素包括或者指向内核运行该进程所需要的所有信息。
- mm_struct描述了虚拟存储器的当前状态。
- pgd指向第一级页表的基址。
- mmap指向vm_area_structs(区域结构)的链表,期中每个区域结构都描述了当前虚拟地址空间的一个区域。
- vm_start:指向这个区域的起始处
- vm_end:指向这个区域的结束处
- vm_port:描述这个区域内包含的所有页的读写许可权限。
- vm_flags:描述这个区域内的页面是与其他进程共享的,还是这个进程私有的。
- vm_next:指向链表中下一个区域结构
Linux缺页异常处理
- 虚拟地址A是否合法?(A在某个区域结构定义的区域内吗?)
- 缺页处理程序搜索区域结构的链表,把A和每个区域结构中的vm_start和vm_end做比较。如果这个指令不合法的,那么缺页处理程序就出发一个段错误,从而终止这个进程。
- 试图进行的存储器访问是否合法?(进程是否有读、写或者执行这个区域内页面的权限?)
- 如果试图进行的访问是不合法的,那么缺页处理程序会触发一个保护异常,从而终止这个程序。
存储器映射
- Linux通过将一个虚拟存储器与一个磁盘上的对象关联起来,以初始化这个虚拟存储器区域的内容,这个过程称为存储器映射。
- 虚拟存储器区域可以映射到两种类型的对象中的一种:
- Unix文件系统中的普通文件
- 匿名文件
- 一旦一个虚拟页面被初始化,它就在一个由内核维护的专门的交换文件之间换来换去。
- 交换文件也叫做交换空间或者交换区域。
- 一个对象可以被映射到虚拟存储器的一个区域,要么作为共享对象,要么作为私有对象。
- 一个映射共享对象的虚拟存储器区域叫做共享区域。类似的,还存在私有区域。
- 私有对象使用写时拷贝技术被映射到虚拟存储器中。
动态存储器分配
- 动态存储器分配器维护着一个进程的虚拟存储器区域,称为堆(heap)。
- 对于每个进程,内核维护着一个变量brk,它指向堆的顶部。
- 分配器将堆视为一组不同大小的块的集合来维护。
- 每个块就是一个连续的虚拟存储器片,要么是已分配的,要么是空闲的。
- 分配器有两种基本风格:
- 显式分配器:要求应用显式地释放任何已分配的块。
- 隐式分配器:要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这块。
- 隐式分配器页叫做垃圾收集器,而自动释放未使用的已分配的块的过程叫做垃圾收集。
mallock和free函数
- 程序通过调用malloc函数来从堆中分配块。
- malloc函数返回一个指针,指向大小为至少size字节的存储器块,这个块会为可能包含在这块内的任何数据对象类型做对齐。
- 动态存储器分配器,可以通过使用mmap和munmap函数,显式地分配和释放堆存储器,还可以通过sbrk函数。
- sbrk函数通过将内核的brk指针增加incr来扩展和收缩堆。
- 如果成功,则返回brk的旧值,否则,它返回-1,并将errno设置为ENOMEM。
- 如果incr为0,那么sbrk就返回brk的当前值。
- 用一个负的incr来调用sbrk是合法的,返回值指向距新堆顶向上abs(incr)字节处。
- free函数来释放已分配的堆块
分配器的要求和目标
- 显式分配器在相当严格的约束条件下工作:
- 处理任意请求序列
- 立即响应请求
- 只使用堆
- 对齐块
- 不修改已分配的块
- 吞吐率最大化和存储器使用率最大化
- 最有用的标准是峰值利用率。
碎片
- 造成堆利用率很低的主要原因是碎片。
- 碎片的两种形式
- 内部碎片
- 外部碎片
- 内部碎片是在一个已分配块比有效载荷大时发生的。
- 外部碎片是当空闲存储器合计起来足够满足一个分配请求,但是没有一个单独的空闲块足够大可以来处理这个请求时发生的。
实现问题
- 空闲块组织:我们如何记录空闲块
- 放置:我们如何选择一个合适的空闲块来放置一个新分配的块?
- 合并:我们如何处理一个刚刚被释放的块
放置已分配的块
- 分配器执行这种搜索的方式是由放置策略确定的。
- 常见的放置策略
- 首次适配
- 下一次适配
- 最佳适配
合并空闲块
- 假碎片:有许多可用的空闲块被切割成小的、无法使用的空闲块。
- 解决假碎片问题,任何实际的分配器都必须合并相邻的空闲块,这个过程成为合并。
- 分配器可以选择立即合并或者推迟合并。
垃圾收集
- 垃圾收集器:动态存储分配器
- 垃圾:程序不再需要的已分配块
- 垃圾收集:自动回收堆存储的过程
C程序中常见的与存储器有关的错误
-
- 间接引用坏指针
- 读为初始化的存储器
- 允许栈缓冲区溢出
- 假设指针和它们所指向的对象是相同大小的
- 造成错位错误
- 引用指针,而不是它所指向的对象
- 误解指针运算
- 引用不存在的变量
- 引用空闲堆块中的数据
- 引起存储器泄漏