虚拟存储器


物理和虚拟寻址

物理寻址

计算机系统的主存被组织成一个由M个连续的字节大小的单元组成的数组。每个字节都有唯一的物理地址。

=>给定以上这种简单结构,CPU访问储存器的最自然的方式就是使用物理寻址。

虚拟寻址:

使用虚拟寻址时,CPU通过生成一个虚拟地址来访问主存,这个虚拟地址在被送到存储器前被转换为适当的物理地址。

将一个虚拟地址转换为物理地址的任务叫做地址翻译

CPU芯片上的存储器管理单元,利用存放在主存中的查询表来动态翻译虚拟地址,该表内容是由操作系统来管理的。


地址空间

地址空间是一个非负整数的有序集合:

{0,1,2……}

线性地址空间:地址空间中整数是连续的。

虚拟地址空间:在一个带虚拟存储器的系统中,CPU从一个N=2^n个地址的地址空间中生成虚拟地址的空间

{0,1,2,……,N-1}

物理地址空间:与系统中物理存储器的M个字节相对应。

{0,1,2,……,M-1}//M不要求是2的幂,这里简化讨论

一个地址空间的大小是由表示最大地址所需要的位数来描述的。

虚拟存储器的基本思想:

允许每个数据对象有多个独立的地址,其中每个地址都选自一个不同的地址空间。

=>主存中的每个字节都有一个选自虚拟地址的虚拟地址和一个选自物理地址空间的物理地址。


虚拟存储器作为缓存的工具

VM系统将虚拟存储器分割为称为虚拟页的大小固定的块。

大小:P=2^p

物理存储器被分割为物理页(页帧),大小也为P。

在任意时刻,虚拟页面的集合都被分为三个不相交的子集:

  • 未分配的:没有任何数据与它们相联,不占任何磁盘空间。
  • 缓存的:当前缓存的物理存储器中的已分配页。
  • 未缓存的:没有缓存在物理存储器中的已分配页。

DRAM缓存的组织结构:

  • 由于大的不命中处罚,DRAM缓存是全联的。
  • 与硬件SRAM相比,操作系统对DRAM缓存使用了更加复杂精密的替换算法。
  • 对磁盘的访问时间很长,DRAM缓存总是使用写回,而不是直写。

页表

页表:将虚拟页映射到物理页。

每次地址翻译 硬件将虚拟地址转换为物理地址时都会读取页表。

页表就是一个页表条目(PTE)的数组。

虚拟地址空间中每个页中一个固定偏移量处都有一个PTE。

一个有8个虚拟页和4个物理页的系统的页表。

页命中

设置有效位:使地址翻译硬件知道VP2是缓存在存储器中。

缺页

DRAM缓存不命中称为缺页。

缺页异常调用内核中的缺页异常处理程序。该程序会选择一个牺牲页,在此例中就是存放在PP3中VP4.如果VP4已经被修改了,那么内核就是将它拷贝回磁盘。

局部性原则保证了在任意时刻,程序将往往在一个较小的活动页面集合上工作,这个集合叫做工作集或者常驻集。

只要我们的程序有好的时间局部性,虚拟存储器系统就能工作得相当好。

颠簸:页面不断出现换进换出。


虚拟存储器作为存储管理工具

操纵系统为每个进程提供了一个独立的页表,因而也就是一个独立的虚拟地址空间。

VM简化了链接和加载,代码和数据共享,以及应用程序的存储器分配。

  • 简化链接
  • 简化加载
  • 简化共享
  • 简化存储器分配

虚拟存储器作为存储器保护的工具

SUP位表示进程是否必须运行在内核模式下才能访问该页。

READ和WRITE位控制对页面的读和写访问。

如果一条指令违反了这些许可条件,那么CPU就触发一个一般保护故障,将控制传递给一个内核中的异常处理程序。

Unix一般将这种错误报告叫做段错误


地址翻译

地址翻译:将一个N元素的虚拟地址空间(VAS)中的元素和一个M元素的物理地址空间(PAS)中元素之间的映射。

CPU中有一个页表基寄存器指向当前的页表。

n位虚拟地址包括两个部分:

一个p位虚拟页面偏移(VPO)
一个(n-p)位的虚拟页号(VPN)

*物理页面偏移与VPO是相同的!

页表实现过程:

当前页命中的执行步骤(完全由硬件处理):

  1. 处理器生成虚拟地址,传给MMU
  2. MMU生成PTE地址,并从高速缓存/主存请求得到他
  3. 高速缓存/主存向MMU返回PTE
  4. MMU构造物理地址,并把它传给高速缓存/主存
  5. 高速缓存/主存返回所请求的数据给处理器。

处理缺页(要求硬件和操作系统内核协作完成):

  1. 处理器生成虚拟地址,传给MMU
  2. MMU生成PTE地址,并从高速缓存/主存请求得到他
  3. 高速缓存/主存向MMU返回PTE
  4. PTE中有效位是零,MMU触发异常,传输CPU中的控制到操作系统内核中的缺页异常处理程序。
  5. 缺页异常处理程序确定牺牲页,如果这个页面已经被修改了,则把它换出磁盘。
  6. 缺页异常处理程序调入新的页面,更新存储器中的PTE
  7. 缺页异常处理程序返回原来的进程,再次执行导致缺页的指令。CPU将引起缺页异常虚拟地址重新发送给MMU。因为虚拟页面现在缓存在物理存储器中,所以就会命中。

结合高速缓存和虚拟存储器:

主要思路:将地址翻译发生在高速缓存查找之前。

*页表条目可以缓存

利用TLB加速地址翻译

翻译后备缓冲器(TLB):是一个小的、虚拟存储的缓存,其中每一行都保存着一个由单个PTE组成的块。

TLB通常具有高度的相关性。

TLB命中的关键点:所有的地址翻译步骤都是在芯片上的MMU中执行的

  1. CPU产生一个虚拟地址
  2. MMU从TLB中取出相应的PTE
  3. MMU将这个虚拟地址翻译成一个物理地址,并且将它发送到高速缓存/主存
  4. 高速缓存/主存将所请求的数据字返回给CPU

多级页表

用来压缩页表的常用方法是使用层次结构的页表。

多级页表从两个方面减少的存储器的要求:

  • 如果一级页表中的一个PTE是空的,那么相应的二级页表根本不会存在,这代表着巨大的潜在节约。
  • 只有一级页表在主存中。减少了主存的压力:只有最常用的二级页表才需缓存在主存中。

多级页表的地址翻译:


案例研究:Intel Core i7/Linux存储器系统

Core i7存储器系统:

处理包:

四个核,一个大的所有核共享的L3高速缓存,DDR3存储器控制器

Core i7地址翻译

Core i7地址翻译过程:

第一级、第二级以及第三集页表中的条目的格式:

第四级条目格式:

PTE的三个权限位:

  • R/W位:确定内容是读写还是只读
  • U/S位:确定是否能在用户模式访问该页
  • XD位(禁止执行位):64位系统中引入,可以用来禁止从某些存储器页取指令

两个缺页处理使用的位:

  • A位(引用位):实现页替换算法
  • D位(脏位):告诉是否必须写回牺牲页

Core i7 MMU如何使用四级页表来将虚拟地址翻译为物理地址:

Linux虚拟存储器系统

LInux为每一个进程维持一个单独的虚拟地址空间:

内核虚拟存储器包含内核中的代码和数据结构。

内核虚拟存储器的某些区域被映射到所有进程共享的物理页面。

LInux虚拟存储器区域

LInux将虚拟存储器组织为一些区域

一个区域:已经存在着的(已分配)虚拟存储器的连续片。

区域的概念重要在:它允许虚拟地址空间中存在间隙。

内核为系统中的每个进程维护一个单独的任务结构(源代码中的task_struct)

任务结构中的元素包含或者指向内核运行该进程所需要的所有信息。

一个具体区域的区域结构包括以下字段:

  • vm_start:指向区域的起始处
  • vm_end:指向区域的结束处
  • vm_prot:描述这个区域包含的所有页的读写许可权限
  • vm_flags:描述这个区域的页面是是共享的还是私有的
  • vm_next:指向链表中的下一个区域

LInux缺页异常处理

缺页处理程序执行以下步骤:

  1. 虚拟地址A是否合法?

    不合法->缺页处理程序触发一个段错误,终止进程。

    合法->2

  2. 试图进行的存储器访问是否合法?

    不合法->缺页处理程序触发一个段保护异常,终止进程。

    合法->3

  3. 选择一个牺牲页面,如果这个牺牲页面被修改过,就将它交换出去,换入新的页面并更新页表。


存储器映射

存储器映射:Linux通过将一个虚拟存储器区域与一个磁盘上的对象关联起来,以初始化这个虚拟存储器区域的内容的过程。

虚拟存储器区域可以映射到两种类型的对象中的一种:

  1. Unix文件系统中的普通文件
  2. 匿名文件

一旦虚拟页面被初始化了,它就有一个内核维护的专门的交换文件之间换来换去。

重要point:交换空间都限制着当前运行着的进程能够分配的虚拟页面的总数。

一个对象可以被映射到虚拟存储器的一个区域,要么作为共享对象,要么作为私有对象。

一个映射到共享对象的虚拟存储器区域叫做共享区域,私有区域类似。

共享对象对于所有把它映射到自己的虚拟存储器进程来说都是可见的。

即使映射到多个共享区域,物理存储器中也只需要存放共享对象的一个拷贝。

私有对象-写时拷贝技术:在物理存储器中只保存有私有对象的一份拷贝。

使用mmap函数的用户级存储器映射

Unix进程可以使用mmap函数来创建新的虚拟存储器区域,并将对象映射到这些区域中。

函数的可视化解释:

prot:访问权限位,具体如下:

  • PROT_EXEC:这个区域的页面由可以被CPU执行的指令组成
  • PROT_READ:这个区域的页面可读
  • PROT_WRITE:这个区域的页面可写
  • PROT_NONE:这个区域的页面不能被访问

删除虚拟存储器的区域:


动态存储器分配

堆是一个请求二进制0的区域,紧接在未初始化的bss区域后开始,并向上(更高的地址)生长。有一个变量brk指向堆的顶部

分配器的两种基本风格:

  • 显示分配器-malloc和free
  • 隐式分配器/垃圾收集器

malloc和free函数

sbrk函数:

free函数:调用释放已分配块。

ptr参数必须指向一个从malloc、calloc或者reallov获得的已分配块的起始位置。

为什么要使用动态分配?:因为经常知道程序实际运行时,它们才知道某些数据结构的大小。

分配器的要求:

  • 处理任意请求序列
  • 立即响应请求
  • 只使用堆
  • 对齐块
  • 不修改已分配的块

分配器的目标:

  • 最大化吞吐率(吞吐率:每个单位时间里完成的请求数)
  • 最大化存储器利用率——峰值利用率最大化

碎片

虽然有未使用的存储器,但是不能用来满足分配请求时,发生这种现象。

  • 内部碎片

    发生在一个已分配块比有效载荷大的时候

  • 外部碎片

    发生在当空闲存储器合计起来足够满足一个分配请求,但是没有一个单独的空间块足以处理这个请求时发生

隐式空闲链表

堆块:

头部,有效载荷,可能的额外填充

隐式空闲链表

将堆组织成一个连续的已分配块和空闲块的序列:

point:系统对齐要求和分配器对块格式的选择会对分配器上的最小块大小有强制的要求。

放置策略:当一个应用请求一个k字节的块,分配器搜索空闲链表,查找一个足够大可以放置所请求的空闲块。

  • 首次适配:从头开始搜索空闲链表,选择第一个合适的空闲块
  • 下一次适配:从上一次搜索的结束位置开始搜索
  • 最佳适配:检索每个空闲块,选择适合所需请求大小的最小空闲块

申请额外的堆存储器:分配器调用sbrk函数向内核请求额外的堆存储器。

假碎片:很多可用的空闲块被切割为很小的无法使用的空闲块。

合并:解决假碎片问题,任何实际的分配器都必须合并相邻的空闲块。

  • 立即合并
  • 推迟合并

带边界的合并:

实现一个简单的分配器

分配器使用memlib.c包所包含的一个存储器系统模型。
目的:

允许在不干涉已存在的系统层malloc包的情况下运行分配器。

序言块:是初始化时创建的,而且永不释放.

结尾块:是一个特殊的块,总是以它为结束。

序言块和结尾块是一种消除合并时边界条件的技巧。

分配器使用一个单独的私有全局变量,总是指向序言块。

显式空闲链表

将空闲组织为某种形式的显式数据结构。

空闲链表中的块排序策略:

  • 后进先出

    将新释放的块放置在链表的开始处。

  • 地址顺序

    链表的每一块的地址都小于它后继的地址。

双向链表的堆块形式:

分离的空闲链表

分离存储:维护多个空闲链表,其中链表中的块有大致相等的大小。

思路:将所有可能的块大小分成一些等价类,(叫做大小类)

  1. 简单分离存储

    每个大小类的空闲链表包含大小相等的块,每个块的大小就是这个大小类中最大元素的大小。

  2. 分离适配

    每个空闲链表是和一个大小类相关联的,并且被组织成某种类型的显示或隐式链表,每个链表包含潜在的大小不同的块,这些块的大小是大小类的成员。

对分离空闲链表的简单的首次适分配搜索,其存储器近似于对整个堆的最佳适配搜索的存储器利用率。

  1. 伙伴系统

    其中每个大小类都是2的幂

    个块的地址和它的伙伴的地址只有一位不同。

主要优点:快速搜索和快速合并。


垃圾收集

垃圾收集器是一种动态存储分配器,它自动释放程序不再需要的已分配块,这些块被称为垃圾。

自动回收堆存储的过程叫做垃圾收集。

可达图:

有向边p->q意味着块p中的某个位置指向块q中的某个位置。

当存在一条从任意根节点出发并到达p的有向路径时,说节点p是可达的。
不可达点就是垃圾。

Mark&Sweep垃圾收集器

Mark&Sweep收集器由标记(mark)阶段和清除(sweep)阶段组成。

  • ptr isPtr(ptr p):如果p指向一个已分配块中的某个字,那么就返回一个指向这个块的起始位置的指针b,否则返回NULL
  • int blockMarked(ptr b):如果已经标记了块b,那么就返回true
  • int blockAllocated(ptr b):如果块b是已分配的,那么久返回ture
  • void markBlock(ptr b):标记块b
  • int length(ptr b):返回块b的以字为单位的长度(不包括头部)
  • void unmarkBlock(ptr b):将块b的状态由已标记的改为未标记的
  • ptr nextBlock(ptr b):返回堆中块b的后继

将已分配集合维护成一棵平衡二叉树。

C程序的Mark&Sweep收集器必须是保守的,原因:C语言不会使用类型信息来标记存储器位置。

存储器有关错误

  • 间接引用坏指针
  • 读未初始化的存储器
  • 允许栈缓冲区溢出
  • 假设指针和它们指向的对象是相同大小的
  • 造成错位错误
  • 引用指针,而不是它所指向的对象
  • 误解指针运算
  • 引用不存在的变量
  • 引用空堆块中的数据
  • 引起存储器泄露

如果程序不检查输入串大小就写入栈中的目标缓冲区,那么这个程序就会有缓冲区溢出错误

纠正方法:使用fgets函数,限制输入串大小。

小结:

虚拟存储器的三个重要功能:
三个重要能力:

  1. 在主存中自动缓存最近使用的存放磁盘上的虚拟地址内容。
  2. 虚拟存储器简化了管理,进而又简化了链接,在进程共享数据,进程的存储器分配以及程序加载。
  3. 虚拟存储器通过在每条页表条目中加入保护位,从而简化了存储器保护。

这一章需要好好理解地址空间啊,各种概念的结构,我比较喜欢看图来学习他们。