各位书友大家好,欢迎做客我的栏目!
在之前的几期文章中,我们聊过存储器技术,以及为弥合处理器与内存性能差距而诞生的存储器层次结构。
今天,我们将把视线转向存储器层次中的另一位“主角”——虚拟存储器,看看它是如何让计算机突破物理内存限制的。
目录
导图

为什么需要虚拟存储器,从“内存焦虑”说起

图1 虚拟地址
虚拟存储器的概念最早出现在20世纪60年代。随着计算机硬件技术的发展,计算机的存储能力不断提高,但程序性能仍受物理内存的限制。
冯诺依曼体系结构的一个原则是:程序要想运行必须先进入内存,如果内存无法容纳全部的程序和数据就会发生内存溢出。当时的计算机系统内存相对较小(几十KB~几MB),而程序变得越来越复杂和庞大,远远超过了物理内存的容量。
而且对于程序员而言,内存管理也十分困难。程序员需要将对物理内存而言过于庞大的程序划分为互斥的片段,并在执行时间根据用户程序控制来加载或卸载,以确保程序绝对不会尝试访问超出计算机现有的物理内存。
没错,虚拟存储器技术一开始就是为了解决内存不足和内存管理的问题,它通过分页和交换机制,将部分数据和代码存储在磁盘上,程序在需要时从磁盘读取数据到内存,实现了内存的虚拟扩展,并将内存管理交由操作系统完成。具体的实现方式我们放在后文讲。
虚拟存储器:不止扩容,竟还有隐藏价值?
虚拟存储器技术的大范围应用可以以20世纪70年代 IBM System/370的发布为标志,此时工程师已经看到虚拟存储器技术其他更令人惊讶的好处。

图2 多道程序设计
20世纪60年代出现的多道程序处理(Multiprogramming)标志着计算机系统能够同时运行多个程序,这项操作系统技术允许在单处理器上同时加载并运行多个程序。当一个程序等待诸如磁盘访问的长延时操作时,处理器可以切换到另一个程序,从而提高CPU的利用率。

图4 进程地址空间
每个进程都需要独立的地址空间,这引入了内存隔离的需求。如果让正在处理的进程独占内存,实现起来倒是方便,但每次切换进程都需要在内存和磁盘之间交换上下文,这会明显削弱多道程序处理带来的好处。而且许多进程无法充分使用整个内存空间,所以还会导致利用率降低。

图4 虚拟存储器技术
虚拟存储器技术如何解决该问题?它为每个进程分配独立的虚拟地址空间,再通过硬件将虚拟地址空间映射/翻译到实际的物理地址上。两者间的翻译过程是以一种对程序员而言透明的方式进行的,即程序的执行环境会保证将实际私有物理存储空间分配给不同的进程,满足内存隔离需求。
虚拟存储器技术如何满足内存管理需求?虚拟存储器为程序提供抽象的地址空间,程序员不再需要考虑指令和数据存储的具体位置,从而无需再控制程序加载和卸载。

图5 虚拟存储器提供内存保护
虚拟存储器技术还有其他优势,比如简化内存保护机制的实现。在虚拟存储器技术出现之前,内存保护往往需要直接管理物理地址,并通过软件层面进行各种边界检查,既复杂又容易出错。
虚拟存储器提供的内存隔离功能本身就实现了进程间的保护,隔离进程间的干扰。同时虚拟存储技术还在对虚拟地址进行翻译的过程中附加权限标志检查步骤进一步加强保护,每次访问内存都进行硬件级别的权限检查,一旦发现不符合权限要求的访问便立即触发异常。
虚拟存储器技术还缩短了程序的启动时间,因为程序启动之前不再需要物理存储器中的所有代码和数据,而是按需要放进内存中。
虚拟存储器技术还能够简化程序的加载过程,因为它允许同一程序可以定位到物理存储器中的任何位置,只需要改变映射关系即可。
揭秘实现逻辑
页式、段式到底怎么 “干活”?
虚拟存储器系统可分为两类:页式存储(Paging)和段式存储(Segmentation)。虚拟存储器技术将物理存储器划分为块,页式存储采用大小固定的块,而段式存储采用大小可变的块。

图6 页式存储和段式存储
页式存储系统的页大小固定,使用单一地址即可索引到目标页中的某个位置,虚拟地址分为页编号和页内偏移量两部分,与高速缓存的索引类似。
段式存储系统的段大小并不固定,虚拟地址需要1个字来表示段号,1个字表示段内偏移量,共需要2个字。

图7 页式存储地址翻译
页式存储的虚拟/逻辑地址转换依赖于页表,页表存储在内存中。通过虚拟地址页编号查询页表获取物理地址页编号,再与虚拟地址的页偏移量拼接成最终的物理地址。

图8 段式存储地址翻译
段式存储的虚拟/逻辑地址转换依赖于段表,每个段包含基址和界限,段内偏移不能超过段长度。通过虚拟地址段号查询段表获取物理基地址,虚拟地址的段偏移量相加得到最终的物理地址。
页式存储和段式存储对比
页式存储的思想和高速缓存类似,将内存划分为多个固定的页大小,分配时需寻找到空闲的页,而替换时需依照替换算法选择页。
页式存储允许分散存放数据块,也就是说虚拟地址连续的数据在物理地址上可以分散,因为页表会正确映射。

图8 段式存储
段式存储的思想是将进程在逻辑上划分为不同的模块/段,比如代码、数据、堆栈等。虚拟地址中的段号正是标识这些不同的逻辑段,编译器或汇编器为每个逻辑段分配一个段号。同时段号对应的基地址只有一个,这就要求段必须存储在物理内存的连续地址上(单一基地址无法表示非连续块)。
段式存储的分配和替换也更为繁琐,由于可变的段大小,需要维护空闲内存块列表。空闲块列表记录所有未被占用的连续内存区域,分配时根据分配算法在列表中寻找满足大小的空闲块,而替换则是先淘汰旧段再执行分配过程。
若寻找到的块大于需求,需将空闲块分割为已分配块和剩余空闲块,已分配块信息更进入段表,剩余空闲块信息更新空闲块列表。释放内存时需要反向操作,合并相邻空闲块以形成连续大块,否则会因无法满足请求块大小而不能被分配进而导致浪费(形象地称之为碎片)。
内存碎片可分为外部碎片和内部碎片。

图9 外部碎片
外部碎片指内存中分散的、不连续的小块空闲空间,这些空间总和足够容纳新进程,但因地址不连续而无法被分配。

图10 内部碎片
内部碎片是指内存分配空间比实际需求空间更大时导致的内存浪费。
显然段式存储的连续内存分配会产生更多的外部碎片,而页式存储的固定页大小会产生更多的内部碎片。
页式存储的虚拟地址空间连续,物理分页细节由操作系统和硬件自动管理,所以页式存储对程序员完全透明。段式存储按程序的逻辑结构划分段,这允许程序员在编写程序时根据需要来设计和管理不同类型的数据和代码。
由于段大小不固定,调入调出内存时不如分页方便。相比之下,分页更容易实现虚拟内存保护和按需分配。所以目前已经很少有计算机使用纯粹的分段方法。下文针对页式存储进行介绍。
页式存储实现
虚拟存储器技术使得主存储器和辅助存储器形成两级存储层次,主存可看作是辅助存储器的缓存。但虚拟存储器主要由操作系统负责管理,硬件为其提供支持,这与高速缓存明显不同。
我们在<存储层级的“金字塔密码”:从概念架构到高速缓存设计与性能量化解析>一文中对存储器层次结构进行了四个方面的讨论:映射策略、替换策略和写策略。我们可以用这三面镜子照一下主存和辅存的两级存储层次,看下有哪些异同。
第一,如何映射?由于访问辅助存储器代价过大,因此操作系统会允许将块放在主存的任意位置来降低页错误(发生缺失),降低访问辅助存储的可能性,相当于全相联映射。页式存储的虚拟地址的索引过程已在前面有过介绍。
第二,如何替换?操作系统常使用LRU算法以尽可能降低页错误数。
第三,如何写入?访问辅助存储器代价过大,操作系统常采用写回策略。

图11 页表
虚拟地址的翻译需要经过页表,页表存储在内存中,这意味着每次访问存储器在逻辑上需要分两步进行。第一步存储器访问是为了获取物理地址,第二步存储器访问是为了获取数据。
局部性思想可以降低存储器访问的次数,那么是否适用于虚拟存储器访问?高速缓存的缺失意味着第二步一定需要,那么只能在第一步上做文章。通过将页表放置在一个特殊的缓存中,降低获取物理地址所需的访存次数,该缓存被称为TLB(translation look aside buffer)。

图12 TLB
TLB作为页表的缓存,因其规模较小常常用全相联映射的实现方式。页表的访问地址是虚拟地址中的页编号字段,因此通过虚拟地址页编号与TLB中所有的有效Tag进行比较的方式进行索引。如果命中,则使用索引出的物理地址编号与虚拟地址偏移组合形成的地址访问内存;如果缺失,则需要访问内存中或下一层级TLB中(TLB也可像高速缓存一样,组织为多层次)的页表。

图13 页表各字段
页表中的R/W和U/S标志位用于满足内存保护需求,内存访问需要先检查这些标志位以判断是否满足权限。
R/W标志位代表内存页面的读/写权限,U/S标志位代表程序的工作模式。体系结构向处理器中加入代表安全级别的同心环状结构,最受信任的程序能够访问全部信息,第二受信任的程序访问除最内层级别之外的所有信息,以此类推。比如RISC-V架构分别为用户进程和操作系统进程各提供一种工作模式用于区分,分别是U-mode和S-mode。
页表也会 “占地方”?多级页表来 “瘦身”
页表存储在内存中,我们也许可以计算下页表占用的空间大小。
假设一个32位系统,每个页大小是4KB,则页表需要维护1M个条目。如果每个条目包含4字节,则页表总占用4MB空间。
同时整个虚拟地址空间需要一个完整的页表,但对于大多数进程而言仅会使用部分虚拟地址空间,而未使用的页表表项仍占用内存。

图14 多级页表
多级页表方案可以减少内存的占用,并降低内存的浪费。
首先将虚拟地址空间在逻辑上划分为多级,每一级有单独的页表。

图15 多级页表地址翻译
将虚拟地址根据所分的级别分为多段,逐渐索引页表。
Level1页表中的每个表项存储某个Level2页表的基地址,Level2页表中的每个表项存储某个Level3页表的基地址。
此时仅需Level1页表需要常驻在内存中;只有在访问特定虚拟地址时,操作系统才会为对应的Level2和Level3页表分配内存。
不过多级页表会增加内存的访问次数,这会增加地址转换的开销,前文介绍的TLB技术可以缓解该影响。同时页表的管理变得更为复杂,当某些页被迁移到辅助存储器中时,会涉及到更多页表项的更新和管理。
页大小怎么选?大了好还是小了好?
近年来微处理器支持多种页大小(4KB/16KB/64KB/2MB/1GB),架构师可从以下方面评估:
⭕页面越大,所需页面表条目越少,内存开销越小
⭕页面越大,所需缓存的条目变小,TLB大小不变时,TLB的缺失率会降低
⭕页面越大,在VIPT缓存技术时,允许不增大相联度的前提下增加高速缓存大小
⭕页面越大,向辅助存储器传送较大页时效率更高,因为一些延迟操作可以被摊销更多字节上
❗页面越大,内部碎片更多
❗页面越大,进程的启动时间的越长,因为需要整个页面全部载入后才能启动
虚拟存储器与高速缓存:如何 “和平共处”?
我们<存储器层次结构性能优化:6 大基础策略,让你的系统跑更快>一文遗留一个问题:虚拟地址索引的缓存无法在多个进程间共享数据,如何解决?

图16 虚拟地址索引 物理地址标签
答案就是虚拟地址索引物理地址标签(VIPT)的方案。虚拟地址由虚拟页编号和页偏移组成,物理地址由物理页编号和页偏移组成。在前文描述的页式存储地址翻译过程我们已经知道,虚拟地址的页偏移和物理地址的页偏移是完全一致的。
因此如果仅使用虚拟地址页偏移中的bit索引高速缓存,同时高速缓存内部的标签使用物理地址的高位进行存储,这就相当于使用物理地址索引高速缓存,从而解决了使用虚拟地址索引高速缓存带来的进程间无法共享数据的问题。
与此同时,虚拟地址页编号索引TLB和虚拟地址页偏移索引高速缓存的过程可以并行执行降低命中时间,TLB索引出的物理地址再与高速缓存索引出的标签进行比较判断命中或缺失。
虚拟地址页偏移位宽固定,这会限制高速缓存索引地址位宽不能超过某个值。如果采用直接映射,这会进一步限制高速缓存大小;如果采用组相联映射,可以通过相联度的方式增加高速缓存大小,因为这不会影响高速缓存索引地址位宽;如果采用全相联,则不会产生影响。
感谢您的阅读和陪伴,本文对虚拟存储器技术全面介绍,希望对您有帮助。欢迎在留言区发表您的意见和看法,同时欢迎您加入我们的讨论,共同交流想法。
关注我 每周日做知识分享
微信公众号(欢迎关注):我想成为大佬

1万+

被折叠的 条评论
为什么被折叠?



