[A-10]ARMv8/ARMv9-Memory-页表的概念和使用场景

文摘   2024-09-07 18:56   辽宁  
ver0.2
前言
长在红旗下,沐浴在春风里的宝宝们,对牛郎和织女的故事一定不陌生。一个来自天上、一个来自人间两个人,给中华文明贡献了一段感人至深的爱情故事。因为爱情来自不同的世界,让故事的走向更加的曲折,好在每年的七夕,银河上都会架起一座鹊桥让两人相会,才让善良的人们心里获得了安慰。而我们的上一篇文章中虚拟地址空间和物理地址空间也是来自两个不同的世界,他们鹊桥又在哪里,他们又是如何相会的?我们本篇文章会介绍内存世界中的鹊桥--页表(Translation Table),并讲解如何通过这座桥让虚拟地址空间和物理地址空间完成映射(相会)。
正文
1 页表
1.1 页表诞生的背景
承接前序的文章,我们还是在一个SOC的架构下讨论问题,这是我们一贯的模式。今天我们还是对典型的基于ARM体系的SOC架构进行局部放大,如图1-1所示。

图1-1 基于ARM的内存子系统High-Level架构

我们发现主存中有一些叫做“Translation Tables”的东西,CPU的微架构中L2和L3级别的Cache中有一些Cache Line中缓存着“Table Entry”的东西,当然TLB我们应该也已经很熟悉了,那么这些元素在CPU工作的过程中扮演着什么角色,他们之间的关系又是如何,下面就开始慢慢讨论。
前序的文章中,我们介绍了MMU工作的基本原理。PE-Core想要工作离不开MMU的辅助,软件送给PE-Core的都是虚拟地址,而总线上跳动的电信号却不是按照虚拟地址编码的,而是物理地址。MMU最核心的作用就是地址翻译,将PE-Core送给它的虚拟地址变成物理地址,然后通过物理地址从主存中加载指令和数据到Cache中供PE-Core继续使用。我们一个简单的Linux系统为例,现代操作系统最重要的一个设计思想就是隔离,其实就是把各个任务隔离,大家彼此谁都不打扰,各自干好自己事情。那到底要隔离什么,其实就是硬件资源,这里就包括内存资源。我们知道Kernel是Linux体系下的资源大总管,它要怎么当好这个家呢?A进程要申请一段内存加载执行指令,kernel怎么能保证这段内存将来不会被B进程使用呢?很简单就是记账。Kernel把整个内存资源的使用和分配情况记账就行了,这个账本就是页表(Translation Table)。只要CPU在执行指令的过程中要使用内存,不论是从磁盘加载指令和计算用的数据,还是从以太网接口、蓝牙接口、或者其他外设接口来的数据,都要经过内存,都要记账。

图1-2 High-Level MMU工作机制框图

我们把图1-1简化一下,如图1-2所示。有了账本之后,PE-Core的工作就轻松多了,它拿到一个虚拟地址(VA)让MMU检查一下TLB,看看这个VA是否已经已经分配过物理内存,如果分配过直接返回物理地址给PE-Core就行了。如果没查到,就得派遣MMU的小弟(Table Walk Unit)去Cache中查一下账本(内存中页表的部分副本),看看是否之前记录过,如果记录过说明这个虚拟地址(VA)已经分配了物理内存,直接返回物理地址就行了。如果Cache中的账本没有查到,那就继续到主存中的账本去查,查到了就直接返回VA对应的PA,查不到说明这个VA之前没有分配过物理地址(这个牛郎还没找到他的织女),那就MMU就得抛出一个异常了,告诉PE-Core,先把眼前的事儿放一放,先去给这个VA分配一段物理内存,CPU就会通知Kernel这个资源大总管去分配物理内存,当然分配成功之后同样要在账本上(Translation Tables)中记上一笔,Cache中副本也要更新一下,对应的TLBs也要做一个记录,方便下次使用时能够快速查询,最后把这个物理地址返回给PE-Core,让程序继续执行。关于内存访问中的异常,场景还是非常复杂的,后面会考虑单独写一篇文章讲解,这里我们还是把目光聚集在页表上。上述Table walk Unit的遍历过程可以简化成如图1-3所示。

图1-3 MMU遍历页表的流程

通过上面的讨论,我们可以看到这个账本(Translation Table/页表)贯穿了内存子系统的软件和硬件,并最终让虚拟地址和物理地址牵手成功。这个页表贯穿软件还好,而贯穿硬件的话,我们应该会敏锐的意识到,问题就不那么简单了。的确是这样的,页表也是体系强相关的概念,后面章节会依托ARM体系来深入的了解一下页表。(注意,由于页表也是强依赖软件才能工作,后面的叙述中会以Linux的为背景加入一些操作系统的内存管理的策略辅助我们理解本文的主题。)
1.2 页表的概念
上面的章节中,其实笔者写了好几个版本,就为了引出页表这个概念,都不满意就反反复复的改,抛开笔者的水平低这个事实不谈(哈哈哈哈),主要还是页表牵扯了软件和硬件两大领域,物理是操作系统的内存管理范畴还是硬件MMU的内存管理范畴都离不开页表这个核心的节点,因此一时找不到侧重点,后来想想页表和账本还真是挺像的,就是财务用的那种收纳条或者记账那种非常密的只能写下一个数字的表格,然后就硬着头皮写下来了,大家保护好自己的眼睛。我们还是拉回来,先看一下手册中对页表的描述:
Virtual addresses are translated to physical addresses through mappings. The mappings between virtual addresses and physical addresses are stored in translation tables (sometimes referred to as page tables).
Translation tables are in memory and are managed by software, typically an OS or hypervisor. The translations tables are not static, and the tables can be updated as the needs of software change. This changes the mapping between virtual and physical addresses.

Each of these virtual address spaces is independent, and has its own settings and tables.

参考手册中的表述,分几个方面来理解页表的基本概念:
(1)先来看一下翻译,为啥叫页表,"Translation Tables"应该翻译为“翻译表”好像比较合适,但是ARM的手册中说有时又叫做"Page Tables"(页表)。其实,这里就是一个如何分割虚拟地址空间的概念,ARM体系下可以将虚拟地址空间按照块(Block)和页(Page)进行划分,两种最重要的区别就是一个颗粒度大,一个颗粒度小。为什么会这样,那说来话长了,涉及到早期计算机体系的软硬件发展的历史,也涉及到现在大型软件系统的使用场景,但是现代操作系统中使用最多的还是Page这种划分的方式,比如Linux中内存管理子系统的设计思想中,Page的概念更是非常重要。后面我们都会慢慢讲到Translation Tables的细节,这里统一都叫做页表。
(2) 页表的作用就是记录VA和PA的映射关系,如图1-4所示。

图1-4 VA映射到PA框图

上面这个图只说明了映射的关系,但是看过前面文章的人已经已经知道,ARM内存的虚拟地址空间和物理地址空间也是有划分的,不同的地址空间彼此使用的页表也不是不一样的,而且是各自独立的,如图1-5所示。

图1-5 ARM内存空间中的页表

上面我们列出了不同地中空间中的页表,当然这里没有列出全部的类型,感兴趣的同学,可以看一下前序的文章,这里我们不再赘述,关于Stage2阶段的地址翻译我们计划会专门规划一篇文章讲解,这里也不会展开讨论。
(3) 每个空间的地址页表都是由自己空间所在的软件模块自行管理,如Hypervisor和GuestOS中的kernel(一般情况下页表对于EL0层面的任务是透明的。),不管是在系统的初始化阶段还是在使用的过程中,统统都是软件自己负责。而且,页表的映射关系和附着的属性也不是静态的,而是随着系统的内存使用负荷情况和自身任务的上下文在运行时都会做出必要的更新调整。
(4) 页表不仅仅记录了虚拟地址到物理地址的映射关系,而且还有自己的一套设置项,也就是说对这个映射关系还可以附一些属性。
以上就是就是我们对页表(Translation Tables)基本概念的分解,看来这个“账本”不单单是记账那么简单,而是内有乾坤,不仅和特定的虚拟内存空间绑定,而且有特定的属性选项可以供软件在运行时进行定制。
1.3 页表项
在了解了页表的概念之后,我们就要继续放大页表深入下去,看一看页表内部的结构到底是什么样子的,如图1-6所示。

图1-6 HIGH-Level 页表框图

从图1-6中我们可以看出,页表内部对需要被映射的虚拟地址空间按照块为单位进行了分解,每一块内存区域用一个Entry(项)进行表示。为什么要这样搞?回答这个问题之前,先让我们来看一看早期的内存分配策略,如图1-7所示。

图1-7 早期操作系统的内存分配策略

这样分的话还比较简单,大概有几个进程,大家各自占用一段。但是这样分的话,会有这么几个问题:
(1) 进程之间的物理内存大家都可以访问彼此之间全凭一个信誉,但是是人都会犯错,编写的代码也会犯错,这个如果只影响自己还好,如果越界了就会影响别人,于是进程之间的地址保护问题就变得非常急迫,那么操作系统的设计师们就在系统级的软件模块中,比如kernel中,融入了地址保护的概念,看来地址问题解决了。
(2) 进程之间的地址解决了,大家都在各自的空间内工作,但是第二个问题就诞生了,内存的使用效率问题,如果这么静态的搞分配的话,那么难免就会有进程A内存不够用了,进程B的空间还没有用完空闲的情况。而且还会存在一些空间虽然被占用了,但是进程在整个生命周期内都不会再用到或者很少用到这些空间中数据的场景,这也会造成内存使用效率低下。
为了解决安全问题和提高内存的访问效率,就引入了虚拟地址空间的概念,进程之间虚拟地址空间独立,进程和内核彼此虚拟地址空间独立,Guest OS和Hypervisor之间虚拟地址空间独立...大家都独立了就从逻辑上避免了互相越界发生内存踩踏。而为了提高物理内存的使用效率,又对虚拟地址空间申请内存的单位做了规划,按照块和页为单位进行申请,而这一切都要靠内核通过记账的形式进行管理,这个账本就是页表,这个页表中的每一条项目记录的都是一次内存分配的行为。这种分配的方式就是现代操作系统普遍使用的分段和分页机制,让任何虚拟地址空间中的一段区域和对应可以分配的物理地址空间的相同大小的物理地址空间的区域自由映射,映射的结果由页表进行记录,由内核或者Hypervisor进行管理,大致的关系如图1-8所示。

图1-8 内存分配示意图

想想MMU工作时候的场景,PE-Core实际上是不断在喂VA给MMU,而我们现代操作系统地址编码是以Byte为单位的,如果每个字节的访问都按照Byte为单位那样去计算然后再缓存,不用说肯定是效率低下的。引入了分页机制后再看看效果,内核的每次内存分配都不是一个字节而是一个区域(One Block or One Page),不仅内存的页表会有记录,CPU中TLBs中也会记录这个分配的结果,下次再有访问内存的行为,MMU拿着CPU送过来的VA就先到TLBs中去找一下,看看这个虚拟地址,是否在最近分配的内存区域之内,根据局部性原来,大概率会命中,那么就会拼接出一个物理地址直接返回给CPU,这个拼接物理地址的过程也是地址翻译的过程,后面章节我们还会详细讨论。
这里我们就了解到了一条重要信息,内存的每次分配都是一个区域而不是一个字节,大多数的访问内存的行为其实都是在已经分配好的区域内轻松完成。那么这个区域到底多大比较合适,图1-8中可以看出有的时候分配是以页为单位,有的时候分配是以块为单位,这又是为什么?要回答这个问题,就要结合具体的软件运行的上下文了,比如一个进程中运维着一个大型数据库,比如云服务器或者一些企业管理系统,那么此时分配一个大的Block区域(物理内存连续的区域)给进程来承载数据库就比较好,而对那种只申请一两个字节的空间或者是一些复杂的结构体,比如几十上百个字节,肯定还是小一点的区域比较好,比如Page。其实这里引申出来一个问题,就是分配的颗粒度问题,下面章节会继续讨论,到这里大家知道一个事情就行,那就是内存的每次分配行为都需要一个内存中的页表中的一个页表项进行记录。图1-8中还提到了多级页表的概念,这里也暂时不展开讨论,后面会有专门的文章进行分析。
1.4 页表与地址翻译
有了前面的铺垫,我们可以对图1-2进一步的简化,看一下地址翻译的过程,如图1-9所示。

图1-9 One-Level地址翻译

通过图1-9结合图1-3,我们再回过头来看一下MMU(Table walk Unit)的工作,是不是更加的清晰了。所谓的页表遍历就是在页表中找到相应的页表项,然后使用页表项的中"PA base"(内核分配物理空间时候的记账行为)拼接上虚拟地址的Offset组成一个物理地址返回给CPU。伴随着这次返回还包括这个地址的权限属性的情况,以及物理地址空间的信息。异常情况情况下,要返回异常的原因,比如缺页的情况下,CPU就要通知内存分配物理空间。
结语
本文还有姊妹篇,也是写着写着发现篇幅实在是太长了,大家看着也累,就拆成两篇文章了。那么文本其实还是完整的讲了一个主题的,那就是页表诞生的背景,以及页表在硬件内存管理和软件内存管理中起到的串联作用。我们把页表比喻成内存软件空间和硬件空间鹊桥,让他们结合在一起,过上幸福美满的生活,哈哈哈哈。下一篇文章,我们会详细阐述多级页表架构和页表项的格式,以及多级页表架构下虚拟内存的映射过程,请大家保持关注。
Reference
[00] <corelink_dmc520_technical_reference_manual_100000_0202_00_en.pdf>
[01] <corelink_dmc620_dynamic_memory_controller_technical_reference_manual.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_systems_100941_0101_02_en.pdf>

[11] <79-LX-LK-z0002_奔跑吧Linux内核-V-2-卷1_基础架构.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,双数据率同步动态随机存储器

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