Home 《虚拟内存的架构和操作系统支持》笔记(四):现代虚拟内存的软件实现
Post
Cancel

《虚拟内存的架构和操作系统支持》笔记(四):现代虚拟内存的软件实现

在介绍了虚拟内存的硬件细节之后,我们现在来关注操作系统层面对虚拟内存的支持。通常操作系统的支持分为两部分,首先操作系统必须和虚拟内存的硬件部分相匹配(如TLB、PTW等),其次操作系统需要从低级的硬件细节抽象出高级的软件操作。在这一节中我们重点讨论操作系统如何合理高效地管理虚拟内存。

我们首先回顾一下分配内存的各个抽象层次。在进程的虚拟地址空间分配虚拟内存块是操作系统负责的问题,不过在单个虚拟内存块内分配对象与处理碎片则是用户代码以及运行时系统负责的内容,虽然内存分配器对性能也非常重要,但是我们在此暂时不展开讨论。我们更需要关注的是页帧的分配,由于物理内存空间通常很有限且很容易碎片化,如何高效管理页帧是操作系统的一大重要任务。

虚拟内存管理

由操作系统维护的最重要的数据结构之一是跟踪每个进程相关虚拟内存的结构,例如Linux使用一个由VMA区域组成的VM区域树,指向VMA树的指针由进程的mm_struct数据结构维护,每个地址空间有一个内存描述符。VMA树记录了进程所使用的所有VMA区域,每个VMA区域是一个连续的虚拟地址范围且互相不重叠,VMA区域的大小是系统页面大小的整数倍。

Linux在VMA树中跟踪两种类型的虚拟内存映射:使用mmap系统调用分配的、有文件支持的映射,用来表示代码页面、库、数据文件和设备;匿名映射,表示没有文件支持的区域,如栈和堆。VMA区域维护一个指向每个区域的虚拟地址开始和结束的指针,以及表明该页是否可读、可写或可执行的页面保护位。下图展示了一个mmaped VMA区域的例子。

4-1 内存描述符指向一个区域,该区域包含了多个mmaped VMA内存区域

在程序的执行过程中,VMA区域会不断增加或删除,如下图所示。调用mmap时,VMA区域会被扩大,内核试图将这些新的页面与现有的相邻VMA合并。VMA树是一个频繁访问的数据结构,每一次page fault、每一个映射操作等都需要对VMA树进行查找,由于一个进程可能有多个VMA,VMA树数据结构必须能够快速查找。在Linux中,和其他许多数据结构一样,VMA树是用红黑树来实现的,可以实现O(log(n))的搜索时间。

4-2 VMA的增加或删除

按需分页和懒分配

有两种方法来管理新的虚拟内存分配。第一种方法是所有新添加的虚拟页面可以立即分配新的物理页帧,且页表立即改变,但这种方法的问题是浪费内存,因为我们还不知道进程是否会真的访问新分配的所有虚拟页面。第二种更主流的方法是懒分配,是一种按需分页的方式,在这种方法中,只有当程序第一次试图访问新的虚拟页面时才会进行物理页帧的分配。

4-3 动态内存管理与page fault情况下的处理

上图显示了Linux如何处理新虚拟页面分配的page fault。程序首先使用malloc来分配内存,malloc会调用brk/sbrk系统调用来增加堆的大小;然后,brk扩大了堆的VMA;接下来,程序试图第一次访问新的页面,但是会触发一次page fault,在这个例子中,堆的增长不涉及磁盘访问,是一个次要page fault;最后,操作系统为此分配一个物理页面,并在页表中创建PTE。

写时复制

当一个页面被复制时,新的页面可以不立即复制新的物理页帧,只有在该新页面需要被写入时才进行分配。首先,原始页帧仍然存在,两个虚拟页面都指向该页帧,并被标记为只读。当写入发生时,页帧被复制,虚拟页面被重新分配到自己的页面副本上,并恢复原始的权限。

写时复制是通过控制页面权限位来实现的,操作系统会把PTE标记为不可写,但是在操作系统内部的数据结构中,该页相关的虚拟内存区域标记为可写,因此处理器对该区域执行store指令时,TLB或页表会触发一个page fault,然后由操作系统的page fault处理程序发现该页被标记为写时复制,然后由操作系统进行后续的处理。

地址空间布局随机化(ASLR)

ASLR通过使攻击者更难预测进程的目标地址来阻止攻击,随机安排关键数据区域的地址空间位置,包括可执行文件的基地址、堆、栈、库的位置。由于ASLR依赖于攻击者猜测到运行中的区域位置的低概率,通过使搜索空间更 大来提高安全性,因此,当虚拟地址空间熵更高时,ASLR更有效,一般来说,熵可以通过增加发生随机化的VMA区域的数量来增加。

局部性管理

当物理内存容量有限时,一些页面必须被交换到磁盘,为其他页面腾出空间,因而一个关键的问题是如何识别一个页面是否有用,为此,我们引入工作集的概念。

工作集

一个程序的工作集是计算机科学中最重要的概念之一,决定了一个进程在给定时间间隔内所需要的内存量。一个进程在时间t的工作集W(t,a)是该进程在时间间隔(t-a, t)内所引用的数据集合。根据程序的局部性原理,进程在时间(t, t+a)内访问的页面集可以通过在(t-a,t)期间访问的页面来近似。

看似简单的工作集概念对系统设计却有着很重要的影响,如果一个进程有太多的页被保存在容量有限的主存储器中,那么其他的进程就没有足够的空间,但如果一个进程在内存中的页面数太少,page fault的频率就会增加,进程就会变得不活跃或被阻塞,以等待磁盘数据传输。

用来识别程序工作集的关键机制是区分被程序引用过的内存页和没有被引用过的内存页。被引用的页面被保留在主内存中,而那些很久没有被引用的页面则是被驱逐出内存的候选者。为了实现虚拟内存,我们需要实现以下三个目标的机制:(1)检测对内存或磁盘上的页面的引用,以确定程序的工作集;(2)选择内存中的哪些页面要被驱逐,以便为从磁盘上载入的页面腾出空间;(3)选择何时将页面从磁盘载入内存。我们现在介绍解决这三个问题的方法。

最简单的页面置换策略

我们首先讨论最优方案,尽管在实践中无法实现,但可以是实际算法的一个参考。过去的工作表明,最优算法,也称为Belady算法,会驱逐最长时间内不会被使用的内存页,也就是说,最优算法依赖于对未来访问模式的准确预测,并驱逐在未来最不会被用到的页面。下图展示了这个算法的例子。

4-4 最优的页面置换算法

相比之下,我们也可以实现随机替换,优势在于非常简单,但缺点是可能非常影响性能,在实践中很少使用。

FIFO算法实现简单且也可以提供比较良好的页面替换候选者,需要驱逐页面时,最老的页面成为最主要的替换对象。这一算法的动机在于最早被分配的页面也可能在未来不再被使用,但实际情况是,FIFO的性能通常低于更复杂的方案。下图展示了FIFO算法的例子。

4-5 FIFO页面置换算法

LRU页面置换策略

最常用的替换算法是LRU置换策略,实现方法通常是维护每个页面的计数器或时间戳,在内存访问时更新时间戳,然后页面替换时扫描所有的时间戳以确定最旧的页面,不过这样占用的内存空间较大,实际上可能有其他方法来进行优化。下图展示了理想的LRU算法的例子,但大多数操作系统实际上使用了近似的LRU算法以提升性能。

4-6 LRU页面置换算法

最广泛采用的近似LRU算法是时钟算法,具体的实现细节可以参考操作系统的教材。下图展示了时钟算法的一个例子。

4-7 时钟页面置换算法

页面缓冲

驱逐页面并不是唯一需要进行优化的操作,由于IO可能延迟很高,操作系统通常会实施高效的分页方案,避免在关键路径上执行IO操作。比如如果一个dirty页面很快就要被驱逐,那么可以在实际驱逐之前就写回到磁盘。通常我们可以维护一个空闲的物理页帧池,当池中的页面数量减少到预设的阈值时,操作系统可以根据替换算法预先写回和/或驱逐页面,并加入到空闲物理页帧池中,页面可以再磁盘访问量小的时候进行写回,并把该页在页表中标记为clean。页面有时也不一定要马上写回到磁盘,而是可以分配页面缓冲区来缓冲对IO的访问,且内存中的缓冲区本身也可以被快速访问。

物理内存分配

设计内存分配器的主要挑战是如何有效地管理碎片化。外部碎片是一种对分配系统可见的内存碎片形式,指的是内存中的空闲区域,由于任何一个区域都小于正在分配的新内存区域的大小,所以不能重复使用;内部碎片只对进程可见,指的是单个分配单元中浪费的空间数量。例如,如果一个想要的分配单元不能被容纳在现有页面的空闲空间内,那么这个不可用的空间就是浪费的。动态内存分配器的目标就是减少这两种类型的碎片。

最简单的内存分配器

最简单的内存分配器包括best fit、first fit、worst fit,如下图所示。

4-8 最简单的内存分配器,第一个例子

伙伴分配

Linux等现代操作系统采用的是伙伴分配器,动机在于:(1)内存分配请求可以按大小来预先分类到不同的类别,解决通用分配器的查找问题;(2)数据结构不仅要快速分配,还要快速释放内存;(3)尽可能保持空闲物理页面的连续性,减少外部碎片。下面两张图展示了伙伴分配器是如何设计和访问的,具体的设计细节同样可以参考操作系统教材。

4-9 伙伴分配器

4-10 伙伴分配器

内存池和Slab分配

伙伴分配器的主要问题是无法消除内部碎片,因此用户程序可能通常会在较大的虚拟内存块中建立内存池,然后为用户的内存分配对象提供单独的用户级分配器。

大多数操作系统还维护了专门用于内核内存分配的特殊slab分配器,slab分配器是由几个软件的slab缓存组成的,这些slab位不同类型的内核数据结构预留内存空间,每个slab的大小是一个物理页,通过bitmap来记录使用情况。

页面着色

内存分配器根据每个页面在缓存中的位置(即位于哪个set)来进行页面着色。页面着色通常被认为是一种改善非完全组相联的缓存性能的机制,假设在最坏情况下,一个进程的工作集中每一个页帧最终都被映射到同一个set中,在这种情况下,该set将被反复刷新,导致低命中率,而其他set则没有被充分利用。为了避免这种情况,我们可以进行页面着色,使工作集中的页面均匀分布在不同set上。在实践中,页面着色对性能的好处还没有被普遍证明超过带来的额外成本,目前还没有被普遍推广,研究人员仍然在继续探索改进页面着色的新方法。

反向映射

反向映射解决的问题是,虚拟内存页面替换算法可能选择换出某些映射到一个或多个进程虚拟地址的物理页面,也就是说多个PTE指向同一个物理页,但页面替换算法选择换出该页面的时候,并不知道这些信息,反向映射可以帮助识别这样的映射。

反向映射数据结构的设计空间和页表非常相似,但性能没有那么关键,通常以优先搜索树的形式来实现,使查询过程尽可能简单。

This post is licensed under CC BY 4.0 by the author.

《虚拟内存的架构和操作系统支持》笔记(三):现代虚拟内存的硬件实现

《虚拟内存的架构和操作系统支持》笔记(五):缓存与内存一致性