第九章 虚拟存储器
一个系统中的进程是与其他进程共享CPU和主存资源的。
虚拟存储器(Virtual Memory),是硬件异常,硬件地址翻译,主存,磁盘文件和内核软件的完善交互,它为每个进程提供一个大的,一致的,私有地址空间。
通过一个清晰的机制,虚拟存储器提供了三个要重的能力:
(1)它将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据,通过这种方式,它高效的利用了主存;
(2)它为每个进程提供了一致的地址空间,从而简化了存储器管理;
(3)它保护每个进程的地址空间不被其他进程破坏。
第一节 物理和虚拟寻址
1.物理地址
计算机系统的主存被组织成一个由M个连续的字节大小的单元组成的数组,每字节都有一个唯一的物理地址PA。第一个字节的地址为0,接下来的字节的地址为1,以此类推。。。。
2.虚拟地址
虚拟存储器被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。
将一个虚拟地址转换为物理地址的任务叫做地址翻译。
使用虚拟寻址时,CPU通过生成一个虚拟地址VA来访问主存,这个虚拟地址在被送到存储器之前先转换成适当的物理地址,此过程为地址翻译。
第二节 地址空间
1、地址空间为非负整数地址的有序集合:{0,1,2,...}
2、如果地址是连续的,则称为线性地址空间。
如果计算机有n位地址总线,在一个带有虚拟存储器的系统中,CPU从一个有N = 2n个地址的地址空间中生成虚拟地址,这个地址称为虚拟地址空间:{0,1,2,...,N-1}
3、一个地址空间的大小是由表示最大地址所需要的倍数来描述的。
物理地址空间,与物理存储器的M个字节相对应:{0,1,2,...M-1}。M不要求是2的幂。
4、允许每个数据对象(字节)有多个独立的地址(属性),其中每个地址都选自一个不同的地址空间,就是虚拟地址的基本思想。
主存中的每字节都有一个选自虚拟地址空间的虚拟地址,和一个选自物理地址空间的物理地址。
第三节 虚拟存储器作为缓存的工具
虚拟存储器被组织为一个由存放在磁盘上N个连续的字节大小的单元组成的数组。每个字节都有一个唯一的虚拟地址,这个唯一的虚拟地址是作为到数组的索引的。
VM系统通过将虚拟存储器分割成称为虚拟页(VP)的大小固定的块,来处理这个问题。
每个虚拟页的大小为P = 2p字节。类似地,物理存储器被分割成物理页(PP),大小也为P字节(物理页也被称为页帧)。
在任意时刻,虚拟页面的集合都分为三个不相交的子集:
未分配的:VM系统还未分配(或者创建)的页。未分配的块没有任何数据和它们相关联,因此也就不占用任何磁盘空间。
缓存的:当前缓存在物理存储中的已分配页。
未缓存的:没有缓存在物理存储器中的已分配页。
虚拟存储器——虚拟页VP,每个虚拟页大小为P=2^平字节
物理存储器——物理页PP,也叫页帧,大小也为P字节。
1.DRAM缓存的组织结构
需要知道,这种缓存结构:不命中处罚很大
是全相联的,任何虚拟存储页都可以放置在任何的物理页中,替换算法精密,DRAM缓存总是使用写回(write back)。
2.页表
页表是一个数据结构,存放在物理存储器中,将虚拟页映射到物理页。
页表将虚拟地址映射为物理地址,每个页表项(PTE),有一个有效位,标识该地址是否在内存的缓存中,还有物理页号或磁盘地址:有效位+n位地址字段。
页命中,收到虚拟地址时,根据该虚拟地址查找页表,如果有效位有效,则说明在内存中,则利用该地址构造物理地址。
3.缺页
就是指DRAM缓存不命中。如果地址不再页表中,则牺牲一条记录,加载进新的地址映射和内容。
页:虚拟存储器的习惯说法,就是块
交换=页面调度:磁盘和存储器之间传送页的活动
(1)空地址:表示该虚拟页未被分配,NULL:未分配。
(2)不是空地址:这个地址指向该虚拟页在磁盘上的起始位置。
VP3,VP5:已分配,但是还未被缓存。
VP1:已分配,已缓存。
4.虚拟存储器中的局部性
局部性原则保证了在任意时刻,程序将往往在一个较小的活动页面集合上工作,这个集合叫做工作集/常驻集。
所以只要程序有良好的时间局部性,虚拟存储器系统就能工作的相当好。
不好?
颠簸:工作集大小超出了物理存储器的大小。
第四节 虚拟存储器作为存储器管理的工具
VM简化了链接和加载、代码和数据共享,以及应用程序的存储器分配。
9.5 虚拟存储器作为存储器保护的工具
SUP:表示进程是否必须运行在内核模式下才能访问该页
READ:读权限
WRITE:写权限
9.6地址翻译
1.得到虚拟地址之后,分为两部分,前部分虚拟页号,后部分偏移地址,根据虚拟页号在页表中找到物理页号,偏移地址不变,直接物理页号和偏移地址构成物理地址。
该模块位于cpu内。页面基址寄存器PTBR指向当前页表。
2.每次翻译虚拟地址时,都需要到内存访问页表,为了加快速度,在cpu内直接再弄一个缓存表,缓存TBE的内容,称为TLB。
地址翻译就是一个N元素的虚拟地址空间VAS中的元素和一个M元素的物理地址空间PAS中元素之间的映射。
3.页面命中完全由硬件处理的,而处理缺页要求硬件和OS内核协作完成。
4.结合高速缓存和虚拟存储器
大多数系统是选择物理寻址的方式来访问高速缓存。
地址翻译发生在高速缓存之前,页表目录可以缓存,就像其他的数据字一样.
使用物理寻址,多个进程同时在高速缓存中有存储块和共享来自相同虚拟页面的块成为简单的事情。而且,高速缓存无需处理保护的问题,因为访问权限的检查是地址翻译过程中一部分。
5.利用TLB加速地址翻译
在MMU中包括一个关于PTE的小的缓存,称为TLB。TLB是一个小的,虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块。
CPU产生一个虚拟地址
b.MMU从TLB中取出相应的PTE
c.MMU将这个虚拟地址翻译成一个物理地址,并且将它发送到高速缓存/主存
d.高速缓存/主存将所请求的数据字返回给CPU
6.多级页表
多级页表——采用层次结构,用来压缩页表。
以两层页表层次结构为例,好处是:如果一级页表中的一个PTE是空的,那么相应的二级页表就根本不会存在
只有一级页表才需要总是在主存中,虚拟存储器系统可以在需要时创建、页面调入或调出二级页表,只有最经常使用的二级页表才缓存在主存中。
9.7 Linux虚拟存储器系统
Linux为每个进程维持了一个单独的虚拟地址空间。
内核虚拟存储器包含内核中的代码和数据结构。
一部分区域映射到所有进程共享的物理页面,另一部分包含每个进程都不相同的数据。
Linux虚拟存储器区域
区域:就是已分配的虚拟存储器的连续片,这些页是相关联的。
每个存在的虚拟页面都保存在某个区域中。
一个具体区域的区域结构包括:
9.8 存储器映射
一、共享对象和私有对象
共享对象
1.共享对象对于所有把它映射到自己的虚拟存储器进程来说都是可见的
2.即使映射到多个共享区域,物理存储器中也只需要存放共享对象的一个拷贝。
3.私有对象:私有对象运用的技术:fork函数写时拷贝;在物理存储器中只保存有私有对象的一份拷贝
二、使用mmap函数的用户级存储器映射
1.创建新的虚拟存储器区域
#include <unistd.h>
#include <sys/mman.h>
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
成功返回指向映射区域的指针,若出错则为-1
参数含义:
2.删除虚拟存储器:
include
include <sys/mman.h>
int munmap(void *start, size_t length);
成功返回0,失败返回-1
从start开始删除,由接下来length字节组成的区域。
第九节 动态存储器分配
1.一个动态存储器分配器维护着一个进程的虚拟存储器区域,称为堆。
堆是一个请求二进制0的区域;对于每个进程,内核维护着一个变量brk,它指向堆的顶部。
2.分配器将堆视为一组不同大小的块的集合来维护。
每个块就是一个连续的虚拟存储器组块,要么是已分配的,要么是未分配的。
显式分配器:如通过malloc,free或C++中通过new,delete来分配和释放一个块。
隐式分配器:也叫做垃圾收集器。自动释放未使用的已分配的块的过程叫做垃圾回收。
想要改变一个以前已分配的块的大小,可以使用realloc函数。
分配器必须对齐块,使得它们可以保存任何类型的数据对象。
不修改已分配的块:分配器只能操作或者改变空闲块。一旦被分配,就不允许修改或者移动它。
3.碎片
外部碎片:在一个已分配块比有效载荷在时发生的,易于量化。
外部碎片:当空闲存储器合计起来足够满足一个分配请求,但是没有一个单独的空闲块足够大可以来处理这个请求时发生的,难以量化,不可预测。
4、隐式空闲链表
堆块的格式:
由一个字的头部,有效荷载,和可能的额外填充组成。
空闲块通过头部中的大小字段隐含地连接着,分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。
需要:特殊标记的结束块。
系统对齐要求和分配器对块格式的选择会对分配器上的最小块大小有强制的要求。
5、放置已分配的块——放置策略
放置分配的块的策略有:
首次适配(first fit):从头开始搜索空闲链表,选择第一个合适的空闲块.
下一次适配(next fit):从上一次搜索的结束位置开始搜索。
最佳适配(best fit):检索每个空闲块,选择适合所需请求大小的最小空闲块
如果空闲块已经最大程度的合并,而仍然不能生成一个足够大的块,来满足要求的话,分配器就会向内核请求额外的堆存储器,要么是通过调用nmap,要么是通过调用sbrk函数;
分配器都会将额外的存储器转化成一个大的空闲块,将这个块插入到空闲链表中,然后将被请求的块放置在这个新的空闲块中。
减少分配时间的方法,称为分离存储,维护多个空闲链表,其中每个链表中的块有大致相等的大小。
6、申请额外的堆存储器
用到sbrk函数,通过将内核的brk指针增加incr来扩展和收缩堆。
7、合并空闲块
合并是针对于假碎片问题的,任何实际的分配器都必须合并相邻的空闲块。
有两种策略:
8、实现简单的分配器
序言块和结尾块:序言块是初始化时创建的,而且永不释放;结尾块是一个特殊的块,总是以它为结束。
需要注意强制类型转换,尤其是带指针的,非常复杂。
9、显式空闲链表
分配时间:隐式的,分配时间是块总数的线性时间
但是显式的,是空闲块数量的线性时间。
链表形式:隐式——隐式空闲链表
显式——双向链表,有前驱和后继,比头部脚部好使。
排序策略:后进先出,按照地址顺序维护.
10、分离的空闲链表
分离存储,是一种流行的减少分配时间的方法。一般思路是将所有可能的块大小分成一些等价类/大小类。
分配器维护着一个空闲链表数组,每个大小类一个空闲链表,按照大小的升序排列。
有两种基本方法:
1.简单分离存储:每个大小类的空闲链表包含大小相等的块,每个块的大小就是这个大小类中最大元素的大小。
2.分离适配
每个空闲链表是和一个大小类相关联的,并且被组织成某种类型的显示或隐式链表,每个链表包含潜在的大小不同的块,这些块的大小是大小类的成员。
这种方法快速并且对存储器使用很有效率。
9.10垃圾收集
垃圾收集器是一种动态存储分配器,它自动释放程序不再需要的已分配块,这些块被称为垃圾,自动回收堆存储的过程叫做垃圾收集。
垃圾收集器将存储器视作一张有向可达图,只有当存在一条从任意根节点出发并到达p的有向路径时,才说节点p是可达的,而不可达点就是垃圾。
Mark%Sweep垃圾收集器由标记(mark)阶段和清除(sweep)阶段组成。
标记阶段标:记出根节点的所有可达的和已分配的后继。
清除阶段:释放每个被标记的已分配块。典型地,块头部中空闲的低位中的一位来表示这个块是否被标记了。