第五章、 虚拟内存
真题考点
考点一:虚拟内存作为缓存的工具
(1)在任意时刻,虚拟页面的集合都分为三个不相交的子集:
——未分配的:VM 系统还未分配(或者创建)的页。未分配的关联,因此也就不占用任何磁盘空间。缓存的:当前已缓存在物理内存中的已分配页。
——未缓存的:未缓存在物理内存中的已分配页。
(2)页表
页表(page table)是一个存放在物理内存中的数据结构,页表将虚拟页映射到物理页。每次地址翻译硬件将一个虚拟地址转换为物理地址时,都会读取页表。
考点二:地址翻译
(1)虚拟地址和物理地址的组成
(2)页命中和缺页
页面命中完全是由硬件来处理的,与之不同的是,处理缺页要求硬件和操作系统内核协作完成,当页面不命中时,CPU 硬件执行的步骤:
- 处理器生成一个虚拟地址,并把它传送给 MMU
- MMU 生成 PTE 地址,并从高速缓存/主存请求得到它。
- 高速缓存/主存向 MMU 返回 PTE
- PTE 中的有效位是零,所以 MMU 触发了一次异常,传递 CPU 中的控制到操作系统内核中的缺页异常处理程序。
- 缺页处理程序确定出物理内存中的牺牲页,如果这个页面已经被修改了,则把它换出到磁盘。
- 缺页处理程序页面调入新的页面,并更新内存中的 PTE
- 缺页处理程序返回到原来的进程,再次执行导致缺页的指令。CPU 将引起缺页的虚拟地址重新发送给 MMU
(3)TLB(加速地址翻译)
在 MMU 中包括了一个关于 PTE 的小的缓存,称为翻译后备缓冲器(TLB)
(4)Intel Core i7/Linux 内存系统的相关考点
- Ll、L2 和 L3 高速缓存是物理寻址的,块大小为 64 字节。L1 和 L2 是 8 路组相联的,而 L3 是 16 路组相联的。
- 页大小可以在启动时被配置为 4KB 或 4MB,Linux 使用的是 4KB 的页,并使用四级页表。
- Linux 虚拟内存空间:Linux 为每个进程维护了一个单独的虚拟地址空间。内核虚拟内存包含内核中的代码和数据结构。内核虚拟内存的某些区域被映射到所有进程共享的物理页面。例如,每个进程共享内核的代码和全局数据结构。
考点三:内存映射
(1)内存映射
Linux 通过将一个虚拟内存区域与一个磁盘上的对象(object)关联起来,以初始化这个虚拟内存区域的内容,这个过程称为内存映射。虚拟内存区域可以映射到两种类型的对象中的一种:
- Linux 文件系统中的普通文件
- 匿名文件:映射到匿名文件的区域中的页面有时也叫做请求二进制零的页
无论在哪种情况中 旦一个虚拟页面被初始化了,它就在一个由内核维护的专门的交换文件之间换来换去。交换文件也叫做交换空间,在任何时刻,交换空间都限制着当前运行着的进程能够分配的虚拟页面的总数。
(2)结合内存映射再看 fork()、execve()
———fork()
当 fork 函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的 mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
——execve()
假设运行在当前进程中的程序执行了如下的 execve 调用:
execve('a.out", NULL, NULL);
execve 函数在当前进程中加载并运行包含在可执行目标文件 a.out 中的程序,用 a.out 程序有效地替代了当前程序。加载并运行 a.out 需要以下几个步骤:
1)删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
2)映射私有区域。为新程序的代码、数据、bss 和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为 a.out 文件中的.text 和.data 区。 bss 区域是请求二进制零的,映射到匿名文件,其大小包含在 a.out 中。栈和堆区域也是请求二进制零的,初始长度为零。
3)映射共享区域。
4)设置程序计数器(PC)。
考点四:动态内存分配
(1)分配器
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块(block)的集合来维护。每个块就是一个连续的虚拟内存片(chunk),要么是已分配的,要么是空闲的。分配器有两种基本风格。
——显式分配器:要求应用显式地释放任何已分配的块。
——隐式分配器:要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器
(2)碎片
造成堆利用率很低的主要原因是一种称为碎片的现象,当虽然有未使用的内存但不能用来满足分配请求时,就发生这种现象:
——内部碎片:是在一个已分配块比有效载荷大时发生的。在任意时刻,内部碎片的数量只取决于以前请求的模式和分配器的实现方式。产生的原因有:1)维护数据结构产生的开销;2)增加块大小以满足对齐的约束条件;3)显式的策略决定(比如, 返回一个大块以满足一个小的请求)
——外部碎片:当空闲内存合计起来足够满足一个分配请求,但是没有一个单独的空闲块足够大可以来处理这个请求时发生的。
(3)空闲块的组织
——隐式空闲链表:通过头部中的大小字段—隐含地连接所有块显式空闲链表:在空闲块中使用指针
——分离的空闲列表:按照大小分类,构成不同大小的空闲链表
——按块按大小排序—平衡树:在每个空闲块中使用一个带指针的平衡树,并使用长度作为权值
①隐式空闲链表:
(a)概念
在这种情况中,一个块是由一个字的头部、有效载荷,以及可能的一些额外的填充组成的。头部编码了这个块的大小(包括头部和所有的填充),以及这个块是已分配的还是空闲的。头部后面就是应用调用 malloc 时请求的有效载荷。有效载荷后面是一片不使用的填充块,其大小可以是任意的。需要填充有很多原因。比如,填充可能是分配器策略的一部分,用来对付外部碎片。或者也需要用它来满足对齐要求。
(b)放置已分配的块——放置策略
一些常见的策略是首次适配、下一次适配和最佳适配。
放置策略 | 描述 | 比较 |
首次适配 | 从头开始搜索空闲链表,选择第一个合适的空闲块 | 速度(吞吐率):下一次适配 > 首次适配 > 最佳适配 内存利用率:最佳适配 >首次适 配 >下一次适配 |
下一次适配 | 是从链表的起始处开始每次搜索 | |
最佳适配 | 配检査每个空闲块,选择适合所需请求大小的最小 空闲块 |
②显式的空闲链表
一种更好的方法是将空闲块组织为某种形式的显式数据结构。例如,堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个 pred(前驱) 和 siicc(后继)指针。
③分离的空闲链表
维护多个空闲链表,其中每个链表中的块有大致相等的大小。
(a)简单分离存储
使用简单分离存储,每个大小类的空闲链表包含大小相等的块,每个块的大小就是这个大小类中最大元素的大小。
(b)分离适配
分配器维护着一个空闲链表的数组。每个空闲链表是和一个大小类相关联的,并且被组织成某种类型的显式或隐式链表
(c)伙伴系统
伙伴系统(buddy system)是分离适配的一种特例,其中每个大小类都是 2 的幂。
考点五:垃圾收集
垃圾收集器将内存视为一张有向可达图(reachability graph),该图的节点被分成一组根节点(root node)和一组堆节点(heap node),每个堆节点对应于堆中的一个已分配块。根节点对应于这样一种不在堆中的位置,它们中包含指向堆中的指针。这些位置可以是寄存器、栈里的变量,或者是虚拟内存中读写数据区域内的全局变量。