09 虚拟内存

虚拟内存

本章的核心内容:

  1. 虚拟内存是如何工作的
  2. 应用程序如何使用和管理虚拟内存

物理和虚拟寻址

虚拟寻址的简单过程?

CPU中的内存管理单元(MMU)利用存放在主存中的查询表来动态翻译虚拟地址

简单的虚拟寻址系统

地址空间

如何理解虚拟地址和物理地址的区别?

物理地址是实际的物理内存空间地址
虚拟地址是利用磁盘生成的逻辑内存空间

为什么需要引入虚拟内存的机制?

没有虚拟内存,程序员需要自己去管理物理地址,因此每次都需要人为地去计算和分配地址,这是一项复杂而且繁琐的工作。
虚拟内存的好处有:

  1. 让主存作为虚拟内存的缓存,更好的发挥稀缺的主存空间的作用
  2. 虚拟内存把内存的管理交给操作系统,不需要程序员去分配内存,提高了工作的效率和内存的安全。
  3. 固定的虚拟内存空间简化了程序的编译、链接、加载等等

缓存

什么是虚拟内存?

概念上来说,虚拟内存是一个存放在磁盘上N个连续字节大小的数组。
这个数组的内容被缓存在主存中

如何理解虚拟页的存在?

和其他缓存一样,数据被分割为块来作为传输单元
虚拟页中的块也被称为虚拟页(VP)
与之在主存上对应的叫物理页(PP)

虚拟内存的出现比缓存要早,所以这里延续传统,并没有使用虚拟块

虚拟页的三种不相交状态是?

  • 未分配:虚拟内存中未分配(未使用,没有任何数据关联)的页
  • 未缓存:虚拟内存中已分配的页
  • 已缓存:缓存在主存的已分配虚拟空间

如何区分SRAM和DRAM?

为了区分不同的缓存

  • SRAM:L1,L2,L3高速缓存
  • DRAM:虚拟内存的缓存——主存

DRAM为什么不命中处罚很大?

DRAM比SRAM慢10倍
读写磁盘要比DRAM慢大约100000多倍

DRAM为了减少不命中处罚的手段?

  1. 采用较大的块(虚拟页)
  2. DRAM采用全相联的方式(没有组之间的区别)
  3. 采用更复杂精密的替换算法
  4. DRAM总是采用写回的方式

如何管理虚拟内存在DRAM中的缓存(是否命中,地址转换,替换策略)?

使用页表进行管理
页表是虚拟页和物理页的一个映射关系,除此还可以维护页的其他属性。
虚拟页的每一页都对应页表中的一个条目(PTE)

条目与虚拟页三种状态的对应?

  1. 有效位为1:已缓存
  2. 有效位为0,空地址:表示未分配页
  3. 有效位为0,有数据:表示未缓存

页表

如何处理命中、缺页?

页命中

命中未缓存的表条目叫做缺页

缺页

缺页异常处理

为什么虚拟内存需要读写磁盘,性能反而更好?

良好的局部性是缓存良好工作的唯一保证

内存管理

虚拟页如何简化了内存的管理?

  1. 简化链接:独立的内存空间简化了链接器的设计和实现,固定的内存格式(如代码段总是从虚拟地址0x400000开始),把实际物理地址的分配问题交给底层,专注于链接的算法。链接完成时即可确定部分静态的内存地址
  2. 简化加载:更容易加载可执行文件和共享对象文件,只需要分配目标文件中固定的节即可
  3. 简化内存分配: 用户不需要通过实际的物理内存去计算空闲内存,让这些都交给操作系统去实现

内存保护

如何进行内存的保护?

PTE中有3个权限字段,控制读、写和访问

内存保护

地址翻译

如何从虚拟地址转换成物理地址?

地址翻译

页面命中时如何转换?

页面命中

缺页时如何转换?

缺页

地址转换如何和高速缓存结合?

VM与高速缓存的结合

如何进一步减小地址转换的开销?

在MMU中建立一个PTE的缓存,称为翻译后备缓存器(TLB)
和一般的缓存一样,采用组相联的方式
使用VPN进行索引

TLB

TLB命中和不命中

如何減少页表占用的内存空间?

一个32位的虚拟空间,4KB的页大小,4字节的PTE
那么每个进程都需要一个4MB的页表
这样下去内存还是吃不消

常见的压缩方式是采用层次结构的页表

多级页表

  1. 如果上级页表中的PTE是空的,那么相应的二级页表就不存在
  2. 只有一级页表才总需要在主存中,其他级页表按需调入调出

多级页表如何寻址?

MMU必须访问多个PTE

多级页表的寻址

这种空间的压缩导致了更多次的内存访问,需要付出多少的时间代价?

由于不同层次的PTE都可以在TLB中缓存
所以多级页表的地址翻译并不会比单级页表慢很多

综合:案例分析

实际案例研究——运行linux的intel core i7

core i7的地址翻译?

Linux的虚拟内存结构?

一个Linux进程的虚拟内存

Linux如何组织虚拟内存?

Linux使用区域(段)来组织虚拟内存
Linux为每个进程都维护这一个任务结构(task_struct源码)

Linux使用区域(段)来组织虚拟内存

Linux在进程的上下文中维护

Linux中使用区域组织虚拟内存的意义?

  1. 将功能相同的虚拟页组织起来
  2. 允许虚拟地址空间有空隙,不需要去记录不存在数据的虚拟页
  3. 记录了每个区域的权限

Linux中如何处理缺页?

Linux缺页

内存映射

什么是内存映射?

将一个虚拟内存区域和磁盘上的对象关联起来,以初始化虚拟内存区域的内容,这个过程就叫内存映射
可以简单的理解为初始化虚拟内存

哪些硬盘对象可以进行内存映射?

  1. 普通文件对象(包括之前说的可执行文件),按需调度(发生缺页时才放到物理内存中)
  2. 匿名文件:由内核创建,当引用到匿名文件的虚拟页时,只会用二进制零覆盖牺牲页(如果牺牲页有修改,则换出),然后更新页表,不会有实际的数据传输,所以也叫请求二进制零的页。

如何管理共享对象的虚拟内存?

共享对象

如何管理私有对象的虚拟内存?

私有对象

fork函数如何管理虚拟内存?

fork之后,拷贝原进程的虚拟空间,并将两个进程的虚拟页都设置为只读,写时复制

execve函数如何管理虚拟内存?

  1. 删除已经在的用户区域
  2. 映射私有区域(新程序的代码、数据、bss和栈区域)
  3. 映射共享区域
  4. 设置程序计数器(PC):指向代码区域的入口点

加载器内存映射

用户级内存映射:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* start:内存中的建议起始位置
* length:分配长度
* prot:内存权限
* flags:对象类型(匿名?私有?写时复制?)
* fd:文件(0表示匿名对象文件)
* offset:文件偏移
*/
void mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);

/**
* start:内存中的开始位置
* length:长度
*/
int munmap(voit *start, size_t length);

动态内存分配

什么是动态内存分配器?

动态内存分配器维护一个进程的虚拟内存区域,称为堆
堆中分为不同大小的块来进行管理
有已分配和空闲两种状态

为什么要使用动态分配?

使用动态内存分配管理堆更方便,也有更好的可移植性。

分配器有两种风格:

  • 显式分配器:要求应用显式的释放已分配块
  • 隐式分配器:分配器检查无用块并释放

相关细节:

  1. mallco要求的内存过大,返回null并设置errno
  2. mallco不初始化,初始化调用calloc
  3. 重新分配调用realloc

动态内存分配的要求和目标?

要求:

  1. 分配请求必须有相应的释放请求
  2. 分配请求需要立即执行,不允许重新排列或者缓存
  3. 只操作堆空间
  4. 对齐块
  5. 不允许修改已分配块

目标:

  1. 单位时间内完成的请求数尽可能多
  2. 堆的利用率尽可能大(已分配块的有效负载占比尽可能多)

什么是外部碎片和内部碎片?

  • 内部碎片:已分配块大小和有效负载大小之差
  • 外部碎片:分散的空闲块

实现动态分配器需要考虑哪些问题?

  1. 空闲块组织:我们如何记录空闲块?
  2. 放置:我们如何选择一个合适的空闲块来放置一个新分配的块?
  3. 分割:在我们将一个新分配的块放置到某个空闲块之后,我们如何处理这个空闲块中的剩余部分?
  4. 合并:我们如何处理一个刚刚被释放的块?

如何记录空闲块?

一个简单的堆块的结构

  1. 一个块由字的头部、有效载荷以及可能的额外的填充组成。
  2. 头部编码了这个块的大小(包括头部和所有的填充)以及该块的状态(已分配或者空闲的)。
  3. 上图显示的是该块强加了一个双子的对齐约束条件(一个字为2B),该块的大小就总是8的倍数,则块大小的最低3位总是0。
  4. 这里的填充有两个原因:
    • 分配器的策略,用于对抗外部碎片。
    • 满足对齐要求。

隐式空闲链表

  1. 空闲块是通过头部中的大小字段隐含地连接着的。
  2. 后面的已分配但是大小为0的头部做为隐式链表的结尾标志。

显式式空闲链表

每个空闲块中,都包含一个前驱和后继指针

如何放置新的请求块?

放置策略:

  1. 首次适配:从头开始搜索空间链表,选择第一个合适的空闲块。
  2. 下一次适配:和首次适配很相识,只不过不是链表开始出开始每次搜索,而是从上一次查询结束的地方开始。
  3. 最佳适配:搜索每一个空闲块,选择合适所需请求大小的最小空闲块。

优点:

  1. 首次优点:将大的空间块保留在列表的后面,缺点:靠近列表起始处留下空闲块的碎片,增加了对较大块的搜索时间。
  2. 下次:knuth提出,比首次运行快一些,尤其是列表前面布满许多小的碎片时,然而下次比首次利用率要低得多。
  3. 最佳:利用率最高,但由于对堆的彻底的搜索,时间复杂度高;

如何分割空闲块?

一旦分割器找到一个匹配的空闲块,他就必须做另一个决定,那就是分配这个 空闲块中多少空间,一个是选择整个空间,简单快捷,但缺点是内部碎片,如果放置策略趋于产生好的匹配,额外的内部碎片可以接受;

然而,如果匹配的不太好,那么分配器通常会选择讲这个空间块分割为两个部分,一个部分为分配块,另外变成一个空的空闲块。

如果分配器不能为请求找到合适的空闲块将发生什么?

为了解决假碎片的问题,任何实际的分配器都必须合并相邻的空闲块,这个过程叫做合并,这就出现一个重要策略决定,也就是在每次一个块释放时,就合并所有的空闲块?还是推迟合并,知道某个请求分配失败,然后再去扫描整个堆,合并所有空闲块。

合并后的内存块也不够用怎么办?

然而这样还是不能得到足够大的块,分配器就会想内核请求额外的堆空间。

垃圾收集

可达图

  • 堆节点:已分配块
  • 根节点:寄存器中的变量、栈中的变量、虚拟内存读写区域的全局变量

分配的内存未回收会有什么影响?

内存将不够

简要描述标记&清理回收算法?

算法函数
伪代码
示例

C中的标记&回收算法有什么难题?

C无法区别是指针还是一个常量

C中常见的与内存相关的问题

本文标题:09 虚拟内存

文章作者:Sun

发布时间:2019年08月27日 - 18:08

最后更新:2019年08月27日 - 18:08

原始链接:https://sunyi720.github.io/2019/08/27/系统原理/09 虚拟内存/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。