[A-11]ARMv8/ARMv9-Memory-多级页表架构

文摘   2024-09-15 18:06   辽宁  
ver 0.3
前言
书接上文,我们用了一篇文章,搞清楚了一个事实:架在内存的虚拟地址空间和物理地址空间上联通彼此的这座鹊桥其实就是一个账本,记录着软件和硬件之间的联系,或者说一个虚拟地址空间的区域和物理地址空间区域的映射关系。上一节,我们循着MMU的工作的流程,大致的讲述了账本每个目录项记录的内容。在文章的结尾举例了一个一级页表工作的例子,这里思考第几个问题:
(1) 一级页表是不是满足所有场景下的需求?
(2) 虚拟和物理之间区域的映射size是怎么定的?
(3) 这个账本的目录项的详细格式到底是由谁规定的?具体的格式到底是啥样的?
(4) 具体的映射过程又是啥样的?
下面我们将带着这些问题,进入正文。
正文
1.多级页表
1.1 背景
这一节我们主要是算一笔细账,算账之前让我们简要看一下虚拟地址空间。随着计算机技术的发展,虚拟地址空间也不断增长,从8位到16位、32位再到现在普遍的64位。对于32位或64位系统而言,其可寻址的虚拟地址空间极大,如32位系统支持4GB的虚拟地址空间,而对于64位的系统虚拟地址的空间就变得更大了,如图1-1所示。

图1-1 典型的64Bits虚拟空间配置

前面的文章我们介绍页表的基本概念的时候讲过了,页表就是一个账本记录着虚拟内存空间和物理内存空间一段区域的映射关系。当然,我们也说过这个区域有两种形式一种是Block的形式,一种是Page的形式,我们先把Block的形式放一放,先看看按照页划分,这笔账能算成啥样。要算账,肯定得准备一下,我们先建立一个单级页表的模型,如图1-2所示。

图1-2 典型的单级页表模型

条件
我们对上面的模型先做一个归纳:
(1) 虚拟地址空间(VA Size):EL0/EL1空间的虚拟地址空间256TB(Linux进程的地址,48Bits有效位)。
(2) 页面的Size(Page Size):假定为常见的以4KB的颗粒度对虚拟地址空间进行切片划分。
(3) 页表的项Size(Translation Tables):按照8个字节设计,记录一次虚拟地址的分配映射过程。
计算
我们来算一笔细账:按照这个单级页表模型管理一个进程的虚拟地址空间需要消耗多少物理空间来存放页表(账本)?
(1) 根据已知条件Page_Size = 4K = 2^12也就是说48位的进程地址空间中需要拿出12Bits进行页内的寻址,也可以说一次分配的区域需要消耗地址空间的低12Bits
(2) 根据上面的推导,一个页表项目TTE(Translation Table Entry)可以记录低12Bits的空间,那么一个进程的地址空间内页表项的总数
TTE_Count = 2^(48 - 12) = 2 ^ 36。 
(3) 一个TTE由于要存储被分配的物理区域的起始物理地址和该内存区域的属性,我们假定TTE_Size = 8Bytes那么一个进程的内存空间总共需要多少物理内存来存放页表(TTS Translation Tables Size):
TT_Size = TTE_Count X TTE_Size = 2 ^ 36 X 8 = 2 ^ 39(Bytes)。
总结
我们对上面模型的计算结果进行简要总结:
(1) 4G物理地址空间也就2 ^ 32(Bytes),这2 ^ 39(Bytes)的地址空间来放账本,实在是太大了,显然是不可能接受的。
(2) 假如我们能对单级页表进行改造压缩,从 2 ^ 39 (Bytes) 压缩到100 M(B)的空间,那么也是有问题的,因为这100M也仅仅是一个进程消耗的,如果你的系统中有10个、20个、30个....进程,这个存放页表的物理地址开销也是不断的在膨胀,这对任何场景下的计算机系统的内存资源的使用都是一种浪费,再叠加上页表的内存区域是要求被连续存放的属性,那么这种浪费会更加的明显。 
分析
通过上面的总结,我们已经了解到了单级页表的局限性,下面就是要对这个单级模型进行深度分析,找到一个优化的思路。在上一篇文章中,我们介绍页表的概念的时候提到过,页表是由RichOS以软件的形式分配的,而页表项的初始化却是动态的,只有给虚拟地址空间的一段区域(例如One Page)分配物理内存的时候才会被初始化,也就是说页表项管理的末端物理内存,只有被使用的时候才会被锁定,不用的区域可以根据需要分配给别的上下文使用。那么,我们对单级页表进行改造,改造的思路也是实现页表的动态分配,用的时候才分配。可是,页表的空间要求连续的属性制约着单级别的页表做不到动态分配这一点。既然单级别做不到,那么再加一个级别看看行不行,也就是对单级页表进行划片,我们保证每片内的页表是连续的,然后在使用的过程中,一个片内的也表项耗尽再使用分配下一片页表空间,这样也可以达到了节约物理内存的目的, 我们沿着这个思路对上面的模型进行优化。
优化
我们对上面的模型进行优化改造一下,如图1-3所示。

图1-3 典型的两级页表模型

按照上面我们分析的情况,我们推导出了一个优化思路,就是对单级页表模型进行进一步分片。通过图1-2可以看到,我们对上面的模型做了一个改动从单级页表分成两级页表。记得在公司培训新员工和一些比较资深有一定工作经验的同事的时候,讲Linux的多级页表的这个环节大家都不是很理解,很多人经过笔者的培训之后蒙圈了,怀疑人生了。后来我总结了一下,主要问题是出在大家对硬件还是不够了解,缺乏必要的基础知识,直接让大家接受生硬的结果,消化起来还是有困难的(总之和笔者的水平没有任何关系,都是别人的错,哈哈哈哈)。那到底怎么理解多级页表呢?还是老办法,通过记账的方式类比:高级页表记录的低级页表的分配情况,低级页表记录的是物理页的分配情况。那么我们按照这个思路对上面的模型进行总结:
(1) 首先我们将原来的一级页表平均划分为2 ^ 9 = 512片,那么L1 级别的Translation Tables只需要 2 ^ 9 = 512 个页表项就可以记录 L2 级Translation Tables全部的分配使用情况。
(2) L2 Translation Tables的每一片内的页表项数目也变少了,从2 ^ 36变成了 2 ^ 27 (36 -9 = 27 因为我们分了9Bits给L1 级别的页表使用),也就是我们可以实现记录2 ^ 27 这么大区域的页表动态划分了。
(3) 其他条件不变,
Page_Size = 4K、TTE_Size = 8 Bytes.
效果
两级页表架构会消耗多少物理内存来保存账本呢? 
(1) L1级别的页表项目共有512项,
L1_TT_Count = 2 ^ 9 = 512.
(2) L1级别的页表Size,L1-TT-Size = L1-TT_Count X TTE_Size =(1) X 8  = 2 ^ 12 (Bytes)
(3) L2级别的页表从一个连续的空间的整块,被平均分成了512片,那么每片的消耗的物理内存:L2_Per_TTE_Size = (2 ^ 39) /(512)  = 2 ^ 27 X 8 = 2 ^ 30 (Bytes)
(4) 经过改造,我们可以通过二级页表实现记录( VA_Size = 2 ^ 27 )虚拟地址空间区域的页表的动态划分了,记录其他的L2级别区域的页表在不使用的时候可以不用在内存中创建。
(5) 改造后,在使用一片L2级别页表的前提下,总共会消耗多少物理内存来保存页表:TT_Size_OPT = L1_TT_Size + L2_Per_TTE_Size = 2 ^ 12  +   2 ^ 30 . (单位:Byte)
(6) TT_Size_OPT 显然比优化前 2 ^ 39 节约了 ( 2 ^ 39 - 2 ^ 30 )这么多的空间,代价是额外消耗了2 ^ 12 的物理内存空间,收益还是非常大。
结论
经过上面的建模、优化过程,我们可以看到增加了一级页表之后,内存的消耗减少了,变成了2 ^ 12  +   2 ^ 30 (Bytes),看上去还是非常大,能不能继续优化呢?答案是肯定的,继续增加页表的级数,对记录2 ^ 27空间的L2级页表进一步的细分,增加级数,还能够节约内存,大家可以自行推导,这里我们直接说结论:
(1) 如果页表不分级管理,由于页表项要求在物理内存内连续存放的限制,在64位体系下的系统架构内,消耗的物理内存特别大,而且大部分都是无效的,被浪费的。
(2) 通过多级页表的架构可以实现更加精细的内存分配的管控,能够在各种上下文下,更加高效的利用物理内存,从而不会因为存储页表而浪费过多的内存。
(3) 多级页表通过引入额外的索引层级,可以有效地减少内存中页表项的存储需求的同时,由于可以根据需要动态加载页表项,因此可以进一步降低内存占用,从而进一步的推高物理内存的使用效率。
(4) 多级页表还可以提高地址映射的灵活性,允许更灵活地管理虚拟地址空间。
这里还要额外强调一点,能够使用多级页表架构,还要有局部性原理的加持。局部性原理:程序在执行时往往会倾向于访问最近使用过的数据或指令。这意味着不是所有的虚拟地址都会被频繁访问。因此,可以将页表进行分级,只在需要时加载对应级别的页表项到内存中,从而减少内存资源的浪费。
1.2 多级页表架构
在前面的章节中,我们通过建立模型算了一笔账,经过分析得出了一个结论,要管理好内存空间必须要引入多级页表进行更精细化的管理才能够实现,灵活、经济、高效的目的。实际上随着硬件技术和操作系统设计的不断进步,多级页表逐渐被广泛应用于各种计算机系统中。特别是在处理大规模内存和复杂多进程环境时,多级页表展现出了其独特的优势,那么本节我们就结合ARM体系看一下的多级页表架构。
ARM体系洗下影响多级架构的因素有很多,比如Block的Size、Page的Size、地址翻译的阶段(stage1 or 2)、Translation Table的格式...等等,这里我们不会展开讨论,只举一个例子(VMSAv8-64 translation using the 4KB granule)介绍下ARM体系的多级架构,以印证本文的结论。
1.2.1 页表的级别
既然是多级架构,那么首先就要对页表进行分级,如图1-4所示。

图1-4 VMSAv8-64-4k 页表分级

通过图1-4可以看出,在当前配置下,可以简要概括如下:
(1) ARM的内存体系被分成了5级,但是有效的基数是4级。
(2) 每级页表的页表项的数目都是512 = 2 ^ 9
(3) 每个级别都会根据输入地址(IA Input Address)中的一部分位数,进行页表项的索引。
(4) 最低级别的页表L3 的页表项只能记录页的分配情况,或者说只能映射到页Page。
(5) L0、L1、L2的页表项可以混合使用,可以记录Page和Block的分配情况。
1.2.2 块的级别
上一个小结,我们看到当前配置下,页表可以分成4级,也就是说在进行页分配的时候,可以将整个分配链路分配成5级(4级页表+1级页)。我们知道,ARM体系下,除了按照页的这种颗粒度进行虚拟地址空间的映射,还可以进行更大连续物理地址空间的映射,那就是块,如图1-5所示。

图1-5 VMSAv8-64-4k 块分级

通过图1-5可以看出,在当前配置下,可以对块的分级简要概括如下:
(1) ARM的内存体系下,块可以分成3个颗粒度,但是有效的只有 1GB和2MB两种。
(2) 由于颗粒度远大于当前的PageSize(4KB),所以页表中最低级别L3级的页表项不能记录这两种块的分配或者说是映射情况。
(3) 2MB块分配的只能通过L2级别的页表项记录,1GB的分配只能通过L1级别的页表项进行记录。
上面的结论也可以和图1-4分析出来的结论互相印证。
1.2.3 输入地址与多级页表
这里面介绍下输入地址,在Stage1阶段的输入地址就是软件中的虚拟地址,CPU传给MMU的就是这个地址,通过这个地址扣动了地址翻译的扳机,而这个地址的有效范围是可以配置的,如图1-6所示。

图1-6 VMSAv8-64 典型的虚拟地址空间分配策略

上图中的有效地址空间为48Bits,也就是说在当前的配置下,有效的IA地址就是48Bits,那么我们再来看一下图1-7。

图1-7 VMSAv8-64-4k IA于页表的映射

通过图1-7,结合我们前面的分析,可以看出。虚拟地址在多级页表的架构中,充当了索引的角色,当一个IA被提交给MMU进行翻译的时候,它会被切割成很多段,然后在各级页表中进行对页表项的索引工作。需要注意的是IA的末端,进行的是最终要范围的物理地址单元的索引,这一部分的详细算法,我们会在下一篇文章中详细介绍。
1.2.4 不同颗粒度下多级页表架构
有了前面的分析之后,我们就可以呈现出Stage-1阶段多级页表架构在内存中的完整的拓扑,如图1-8、1-9、1-10所示。

图1-8 VMSAv8-64-4k 多级页表架构

图1-9 VMSAv8-64-16k 多级页表架构

图1-10 VMSAv8-64-64k 多级页表架构

通过对比,简单归纳如下,如图1-11所示:
(1) 不同颗粒度下,由于末端PageSize的不同,导致各级页表的索引范围和IA的映射不一致。
(2) 颗粒度越大,ARM支持级数也不一样,例如,64KB的配置下,最多支持2级。
(3) 对于Blcok的支持也不一样,例如16K配置下,Block只支持32MB的块映射。

图1-11 VMSAv8-64 不同颗粒度下多级页表架构对比

1.2.4 应用
上面描述的ARM架构只是表示硬件实际支持能力,但是我们知道页表不光是硬件在使用软件也在使用,实际中要根据具体的场景进行细分配置。如在工程实践中,对于实际使用级数来说,以4K的颗粒度为例,系统可能会选择使用两级或三级页表,这取决于系统的内存管理策略和性能要求。使用两级页表可以减少内存占用,但可能会增加查找时间;而使用三级页表则可以在一定程度上平衡内存占用和查找时间。
具体的参考因素
内存管理策略:系统的内存管理策略会直接影响页表的设计和使用。例如,如果系统需要高效地管理大量内存,可能会倾向于使用更多的页表级数来减少每级表的条目数。
性能要求:性能要求也是决定页表级数的一个重要因素。在需要快速地址转换的场景中,可能会选择使用较少的页表级数来减少查找时间。
结语
本文,前半部分先算了一笔账,发现单级页表架构,会消耗大量的物理内存空间,而这些空间实际上绝大多数都被浪费掉了,在运行时并没有被实际使用。通过引入多级页表,不断的对下级页表进行划片,就可以见过页表的物理内存中存放页表项连续属性要求,又可以动态分配下级页表片,从而起到节约物理内存,提升效率的目的。经过前半部分的铺垫,我们对后半部分ARM体系下的多级页表架构的理解就更加的清晰了,我们以VMSAv8-4k为例,介绍了ARM的多级架构思想。当然,限于篇幅关于多级页表架构相关的系统寄存器,V9-128位的具体体系的介绍暂时按下不表了,但是思想是想通的,感兴趣的朋友,可以沿着本人的思路继续啃相关的手册。下一篇,我们会介绍多级页表架构下,页表项的一些知识,请大家保持关注。
文章开头部分问题,也会在下一篇文章中得到解答,实在是太长了,思来想去还是分成两篇文章来发表。
Reference
[00] <corelink_dmc520_technical_reference_manualn.pdf>
[01] <corelink_dmc620_dynamic_memory_controller_technical.pdf>
[02] <IP-Controller/DDI0331G_dmc340_r4p0_trm.pdf>
[03] <80-ARM-IP-cs0001_ARMv8基础篇-400系列控制器IP.pdf>
[04] <arm_cortex_a725_core_trm_107652_0001_04_en.pdf>
[05] <arm_cortex_a720_core_trm_102530_0001_04_en.pdf>
[06] <DDI0487K_a_a-profile_architecture_reference_manual.pdf>
[07] <armv8_a_address_translation.pdf>
[08] <cortex_a55_trm_100442_0200_02_en.pdf>
[09] <learn_the_architecture_aarch64_memory_management_guide.pdf>

[10] <learn_the_architecture_armv8-a_memory_systemsn.pdf>

Glossary
MMU             - Memory Management Unit
TLB               - translation lookaside buffer
VIPT              - Virtual Index Physical Tag
VIVT              - Virtual Index Virtual Tag
PIPT               - Physical Index Physical Tag
VA                   -  Virtual Address
PA                   -  Physical Address
IPS                  - Intermediate Physical Space
IPA                  - Intermediate Physical Address
VMID               - virtual machine identifier
TLB                  - translation lookaside buffer(地址变换高速缓存)
VTTBR_EL2     - Virtualization Translation Table Base Registers(ArmV8 寄存器)
ASID                 - Address Space Identifier (ASID)
DMC                 - Dynamic Memory Controller
DDR SDRAM   - Double Data Rate Synchronous Dynamic Random Access Memory,双数据率同步动态随机存储器
IA                    - Input Address
OA                  - Output Address

浩瀚架构师
和大家一起探索这个神奇的世界。