第九章 虚拟存储器

虚拟存储器是计算机系统最重要的概念之一,它是对主存的一个抽象

三个重要能力:

  • 它将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据,通过这种方式,高效的使用了主存
  • 它为每个进程提供了一致的地址空间,从而简化了存储器管理
  • 它保护了每个进程的地址空间不被其他进程破坏

9.1 物理和虚拟寻址

1.物理地址

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

根据物理地址寻址的是物理寻址。

2.虚拟地址

虚拟存储器被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。

使用虚拟寻址时,CPU通过生成一个虚拟地址VA来访问主存,这个虚拟地址在被送到存储器之前先转换成适当的物理地址(这个过程叫做地址翻译,相关硬件为存储器管理单元MMU

9.2 地址空间

1.地址空间

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

{0,1,2,……}

2.线性地址空间

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

3.虚拟地址空间

CPU从一个有 N=2^n 个地址的地址空间中生成虚拟地址,这个地址空间成为称为虚拟地址空间。

4.地址空间的大小

由表示最大地址所需要的位数来描述。

N=2^n:n位地址空间

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

 

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

VM系统将虚拟存储器分割为虚拟页,虚拟页面的集合分为三个不相交的子集:

  • 未分配的:系统还未分配的页,未存储数据,不占用磁盘空间。
  • 缓存的:当前缓存在物理存储器中的已分配页
  • 未缓存的:没有缓存在物理存储器中的已分配页。

页表

页表:将虚拟页映射到物理页,就是一个页表条目的数组。

PTE:由一个有效位和一个n位地址字段组成的,表明了该虚拟页是否被缓存在DRAM中。

页命中

当CPU读取一个字的时候,地址翻译硬件将虚拟地址作为一个索引来定位PTE,并从存储器中读取它。

缺页

DRA缓存不命中称为缺页。在不命中发生时,换入页面的策略称为按需页面调度。

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

按需页面调度和独立的虚拟地址空间的结合简化了链接和加载、代码和数据共享,以及应用程序的存储器分配。

  • 简化链接:独立的地址空间允许每个进程的存储器映像使用相同的基本格式,而不管代码和数据实际存放在物理存储器的何处。

  • 简化加载:虚拟存储器使得容易想存储器中加载可执行文件和共享文件对象。

  • 简化共享:独立地址空间为操作系统提供了一个管理用户进程和操作系统自身之间共享的一致机制。

  • 简化存储器分配:虚拟存储器为向用户进程提供一个简单的分配额外存储器的机制。

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

通过在PTE上添加一些额外的许可来控制对一个虚拟页面的内容访问。

 

9.6 地址翻译

  • 地址翻译符号小结(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。

9.7  例子

1、利用TLB加速地址翻译

  • 翻译后备缓冲器(TLB)
  • TLB是一个小的、虚拟寻址的缓存,期中每一行都保存着一个由单个PTE组成的块。
  • TLB通常有高度的相连性。
  • 如果TLB=2^t个组,那么TLB索引是由VPN的t个最低位组成的,而TLB标记是由VPN中剩余的位组成的。
  • 当TLB命中时的关键:所有的抵制翻译步骤都是在芯片上的MMU中执行的 ,因此非常快。
  • 第一步:CPU产生一个虚拟地址
  • 第二步和第三步:MMU从TLB中取出相应的PTE
  • 第四步:MMU将这个虚拟地址翻译成一个物理地址,并且将它发送到高速缓存/主存
  • 第五步:高速缓存/主存将所请求的数据字返回给CPU

2、Linux虚拟存储器区域

  • Linux将虚拟存储器组织成一些区域(段)的集合。
  • 一个区域是已经存在着的(已分配的)虚拟存储器的连续片(chunk)。
  • 区域的概念允许虚拟地址空间有间隙。
  • 内核为系统中的每个进程维护一个单独的任务结构。
  • 任务结构中的元素包括或者指向内核运行该进程所需要的所有信息。
  • mm_struct描述了虚拟存储器的当前状态。
  • pgd指向第一级页表的基址。
  • mmap指向vm_area_structs(区域结构)的链表,期中每个区域结构都描述了当前虚拟地址空间的一个区域。
    • vm_start:指向这个区域的起始处
    • vm_end:指向这个区域的结束处
    • vm_port:描述这个区域内包含的所有页的读写许可权限。
    • vm_flags:描述这个区域内的页面是与其他进程共享的,还是这个进程私有的。
    • vm_next:指向链表中下一个区域结构

3、Linux缺页异常处理

  • 虚拟地址A是否合法?(A在某个区域结构定义的区域内吗?)
    • 缺页处理程序搜索区域结构的链表,把A和每个区域结构中的vm_start和vm_end做比较。如果这个指令不合法的,那么缺页处理程序就出发一个段错误,从而终止这个进程。
  • 试图进行的存储器访问是否合法?(进程是否有读、写或者执行这个区域内页面的权限?)
    • 如果试图进行的访问是不合法的,那么缺页处理程序会触发一个保护异常,从而终止这个程序。

9.8 存储器映射

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

映射对象:

1.Unix文件系统中的普通文件

2.匿名文件(全都是二进制0)

一、共享对象和私有对象

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

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

2、私有对象
  • 私有对象运用的技术:写时拷贝
  • 在物理存储器中只保存有私有对象的一份拷贝

fork函数就是应用了写时拷贝技术,至于execve函数:

二、使用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

参数含义:

  • start:这个区域从start开始
  • fd:文件描述符
  • length:连续的对象片大小
  • offset:距文件开始处的偏移量
  • prot:访问权限位,具体如下:

    PROT_EXEC:由可以被CPU执行的指令组成
    PROT_READ:可读
    PROT_WRITE:可写
    PROT_NONE:不能被访问
  • flag:由描述被映射对象类型的位组成,具体如下:

    MAP_ANON:匿名对象,虚拟页面是二进制0
    MAP_PRIVATE:私有的、写时拷贝的对象
    MAP_SHARED:共享对象

2、删除虚拟存储器:

include

include <sys/mman.h>

int munmap(void *start, size_t length);
成功返回0,失败返回-1

从start开始删除,由接下来length字节组成的区域。

 

9.9 动态存储器分配

  • 动态存储器分配器维护着一个进程的虚拟存储器区域,称为堆(heap)。
  • 对于每个进程,内核维护着一个变量brk,它指向堆的顶部。
  • 分配器将堆视为一组不同大小的块的集合来维护。
  • 每个块就是一个连续的虚拟存储器片,要么是已分配的,要么是空闲的。
  • 分配器有两种基本风格:
    • 显式分配器:要求应用显式地释放任何已分配的块。
    • 隐式分配器:要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这块。
      • 隐式分配器页叫做垃圾收集器,而自动释放未使用的已分配的块的过程叫做垃圾收集。

1、mallock和free函数

  • 程序通过调用malloc函数来从堆中分配块。
  • malloc函数返回一个指针,指向大小为至少size字节的存储器块,这个块会为可能包含在这块内的任何数据对象类型做对齐。
  • 动态存储器分配器,可以通过使用mmap和munmap函数,显式地分配和释放堆存储器,还可以通过sbrk函数。
    • sbrk函数通过将内核的brk指针增加incr来扩展和收缩堆。
    • 如果成功,则返回brk的旧值,否则,它返回-1,并将errno设置为ENOMEM。
    • 如果incr为0,那么sbrk就返回brk的当前值。
    • 用一个负的incr来调用sbrk是合法的,返回值指向距新堆顶向上abs(incr)字节处。
  • free函数来释放已分配的堆块

2、分配器的要求和目标

  • 显式分配器在相当严格的约束条件下工作:
    • 处理任意请求序列
    • 立即响应请求
    • 只使用堆
    • 对齐块
    • 不修改已分配的块
  • 吞吐率最大化和存储器使用率最大化
  • 最有用的标准是峰值利用率。

3、碎片

  • 造成堆利用率很低的主要原因是碎片。
  • 碎片的两种形式
  • 内部碎片
  • 外部碎片
  • 内部碎片是在一个已分配块比有效载荷大时发生的。
  • 外部碎片是当空闲存储器合计起来足够满足一个分配请求,但是没有一个单独的空闲块足够大可以来处理这个请求时发生的。

4、实现问题

  • 空闲块组织:我们如何记录空闲块
  • 放置:我们如何选择一个合适的空闲块来放置一个新分配的块?
  • 合并:我们如何处理一个刚刚被释放的块

5、放置已分配的块

  • 分配器执行这种搜索的方式是由放置策略确定的。
  • 常见的放置策略
    • 首次适配
    • 下一次适配
    • 最佳适配

6、合并空闲块

  • 假碎片:有许多可用的空闲块被切割成小的、无法使用的空闲块。
  • 解决假碎片问题,任何实际的分配器都必须合并相邻的空闲块,这个过程成为合并。
  • 分配器可以选择立即合并或者推迟合并。

9.10 垃圾收集

    • 垃圾收集器:动态存储分配器
    • 垃圾:程序不再需要的已分配块
    • 垃圾收集:自动回收堆存储的过程

9.11 C语言中常见的与存储器有关的错误

  • 间接引用坏指针:在进程的虚拟地址空间中有较大的洞,没有映射到任何有意义的数据,如果试图引用一个指向这些洞的指针,操作系统就会以段异常来终止程序。典型的错误是:

    scanf("%d",val);

  • 读未初始化的存储器:虽然bass存储器位置总是被加载器初始化为0,但对于堆存储器却并不是这样的。

  • 允许栈缓冲区溢出:如果一个程序不检查输入串的大小就。写入栈中的目标缓冲区,程序就会出现缓冲区溢出错误。

  • 假设指针和指向他们的对象大小是相同的。

  • 造成错位错误。

  • 引用指针,而不是他所指向的对象。

  • 误解指针运算:忘记了指针的算术操作是以它们指向的对象的大小为单位来进行,而这种大小单位不一定是字节。

  • 引用不存在的变量。

  • 引用空闲堆块中的数据。

  • 引起存储器泄露:当不小心忘记释放已分配块,而在堆里创建了垃圾时,就会引起存储器泄露。

参考资料: