Arm Linux内存管理(一)

发布时间 2023-10-08 19:21:30作者: zxddesk

Arm Linux内存管理(一)

 

一、Arm linux的基本概念

1. Arm Linux物理内存

Arm平台内存大小的定义在DTS设备树中定义

arch/arm/boot/dts/vexpress-v2p-ca9.dts中

内核在启动过程中,需要解析dts文件。代码的调用关系为:start_kernel() ->setup_arch ->setup_machine_fdt()->early_init_dt_scan_nodes()->early_init_dt_scan_memory()

解析"memory"描述的信息从而得到内存的base_address和size信息,最后内存块信息通过early_init_dt_add_memory_arch()->memblock_add()函数添加到memblock子系统中。

2. Linux内核Memblock子系统

memblock是内核在启动初期用于管理物理内存的机制,它从dtb中解析出物理内存信息,并通过特定的数据结构来管理这些信息。同时它还在memblock初始化之后,伙伴系统启用之前,承担系统的内存分配任务。在内核使用内存前,需要初始化内核的表项,初始化页表主要在map_lowmem()函数中。函数调用如下start_kernel()->setup_arch()->paging_init()

3. 内存的UMA和NUMA架构

一致性内存访问UMA(Uniform Memory Access)也可以称为对称多处理器SMP(Symmetric Multi-Process)。意思是所有的处理器访问内存花费的时间是一样的。也可以理解整个内存只有一个node。(但事实上严格意义的UMA结构几乎不存在)。非一致性内存访问NUMA(Non-Uniform Memory Access)意思是内存被划分为各个node,访问一个node花费的时间取决于CPU离这个node的距离。每一个cpu内部有一个本地的node,访问本地node时间比访问其他node的速度快。所以Linux为了对 NUMA 进行描述,从Linux2.4开始引入了存储节点,把访问时间相同的存储空间称为一个存储节点。进而 Linux 将物理内存划分为三个层次来管理:存储节点,管理区,页面。

4.内存页page的概念

内核把物理页作为内存管理的基本单位。尽管处理器的最小可寻址单位通常为字(甚至字节),但是,内存管理单元(MMU,管理内存并把虚拟地址转换为物理地址的硬件)通常以页为单位进行处理。正因为如此,MMU以页(page) 大小为单位来管理系统中的页表(这也是页表名的来由)。从虚拟内存的角度来看,页就是最小单位。

linux内核的内存划分

5. 内存区块ZONE的概念

对页表的初始化完成之后,内核就可以对内存进行管理了,但是内核并不是统一对待这些页面,而是采用区块zone的方式来管理。据内存使用方式的不同,每个内存节点又可被进一步划分为不同的zone。如在有些架构中,只有特定范围的内存地址才可以用于DMA,因此这部分内存被划分到了DMA zone中。在32位系统中,由于内核虚拟地址空间只有1G,造成高于一定范围的物理内存(通常为896M)无法进行线性映射,这些在内核初始化时没有建立线性页表的内存被划分到了hignmem zone中。其它的内存则被划分到了normal zone中。Linux 2.6把每个内存节点的物理内在划分为3个管理区(zone)。

linux内核的内存区块

ZONE_ DMA: 可以用来DMA操作的页。 ( <16MB)

ZONE_NORMAL :正常规则映射的页。 (16MB~896MB)

ZONE_ HIGHMEM: 高内存地址的页,并不永久性映射。 ( >896MB)

  以上这些不同zone的内存,其对系统的重要程度是不一样的,如对于系统来说hignmen内存最廉价,而DMA内存是最珍贵的。因此,在内存分配策略中,同一个node的内存,位于hignmem zone中的会被优先分配,当该zone的内存不满足条件时,就会从normal zone中分配,只有当以上两个zone中内存都不满足条件时才会从dma zone分配。当然,对于有特殊要求的内存分配请求,可以通过设置相应的flag来指定所需分配的zone。

  在内核中,内存管理的最小单位是页,每个页通过一个struct page结构体管理。 one会通过相应的数据结构管理内存页,如该zone中的哪些页是空闲的,哪些页是已分配的。或者它们是热的还是冷的等,其中热页表示该页已经加载到了高速缓存中,冷页表示页未被加到高速缓存中。

6. 内存池的概念

  • 内存池(Memory Pool)是一种动态内存分配与管理技术,通常情况下,程序员习惯直接使用new,delete,malloc,free等API申请和释放内存,这样导致的后果就是:当程序运行的时间很长的时候,由于所申请的内存块的大小不定,频繁使用时会造成大量的内存碎片从而降低程序和操作系统的性能。
  • 内存池则是在真正使用内存之前,先申请分配一大块内存(内存池)留作备用。当程序员申请内存时,从池中取出一块动态分配,当程序员释放时,将释放的内存放回到池内,再次申请,就可以从池里取出来使用,并尽量与周边的空闲内存块合并。若内存池不够时,则自动扩大内存池,从操作系统中申请更大的内存池
解决内碎片问题,但是内碎片问题无法避免,只能尽可能的降低,由于向内存申请的内存块都是比较大的,所以能够降低外碎片问题。
一次性向内存申请一块大的内存慢慢使用,避免了频繁的向内存请求内存操作,提高内存分配的效率。

7. 内存屏障

存屏障 memory barrier,由于超标量技术和乱序执行,指令的并行执行。通过volatile告诉编译器不对指令优化,另外处理器侧实现弱一致性内存模型,同时也提供了3条内存屏障指令。

  • DMB: Data Memory Barrier
  • DSB: Data synchronization Barrier
  • ISB: Instruction synchronization Barrier

内存屏障指令主要解决多核访问统一内存地址的问题。所以程序员必须使用内存屏障指令来显示的告诉处理器这两个内存有数据依赖关系

二、 Linux启动页表建立

在内核初始化阶段会对内核空间的页表进行一一映射,实现的函数依然是create_mapping()。

start_kernel->setup_arch->paging_init->map_mem->_map_memblock->create_mapping

ARM64启动过程中,如何建立初始化阶段页表的过程?从bootloader到kernel的时候,MMU是off的(顺带的负作用是无法打开data cache),为了提高性能,加快初始化速度,我们必须某个阶段(越早越好)打开MMU和cache,而在此之前,我们必须要设定好页表。

在初始化阶段,我们mapping三段地址,一段是identity mapping,其实就是把物理地址mapping到相等的虚拟地址上去,在打开MMU的时候需要这样的mapping(ARM ARCH强烈推荐这么做的)。第二段是kernel image mapping,内核代码欢快的执行当然需要将kernel running需要的地址(kernel txt、dernel rodata、data、bss等等)进行映射了,第三段是blob memory对应的mapping。
在本文中,我们会混用下面的概念:page table和translation table、PGD和Level 0 translation table、PUD和Level 1 translation table、PMD和Level 2 translation table、Page Table和Level 3 translation table。最后,还是说明一下,本文来自4.1.10内核(部分来自4.4.6),有兴趣的读者可以下载来对照阅读本文。

在汇编代码阶段的head.S文件中,负责创建映射关系的函数是create_page_tables。create_page_tables函数负责identity mapping和kernel image mapping。

  • identity map:是指把idmap_text区域的物理地址映射到相等的虚拟地址上(恒等映射),这种映射完成后,其虚拟地址等于物理地址。idmap_text区域都是一些打开MMU相关的代码。
  • kernel image map:将kernel运行需要的地址(kernel txt、rodata、data、bss等等)进行映射。

identify map,也就是物理地址和虚拟地址是相等的。为什么需要这么一个映射呢?我们都知道在MMU打开之前,CPU访问的都是物理地址,那么当MMU打开后访问的就是虚拟地址了,这段页表的映射就是从CPU到打开MMU之前的这段代码物理地址的映射,防止开启MMU后,无法获取页表。可以从System.map文件中查看这些代码:

  • 当CONFIG_PGTABLE_LEVELS=4时,pgd–>pud–>pmd–>pte
  • 当CONFIG_PGTABLE_LEVELS=3时,没有PUD页表:pgd(pud)–>pmd–>pte
  • 当CONFIG_PGTABLE_LEVELS=2时,没有PUD和PMD页表:pgd(pud, pmd)–>pte

Linux内核编译后,kernel image是需要进行映射的,包括text,data等各种段。汇编结束后的内存映射关系如下图所示:

三、Cache的原理

1. cache概述

处理器访问主存储器使用地址编码方式。Cache也使用类似的地址编码方式,因此处理器使用这些编码地址可以访问各级cache。下图是一个经典的cache架构图

经典cache架构图

经典cache架构

处理器在访问存储器时,会把地址同时传递给TLB(Translation Lookaside Buffer)和cache。TLB是一个用于存储虚拟地址到物理地址转换的小缓存,处理器先使用EPN(effectivepagenumber)在TLB中进行查找最终的RPN(RealPageNumber)。如果这期间发生TLB miss,将会带来一系列严重的系统惩罚,处理器需要查询页表。假设这里TLB Hit,此时很快获得合适的RPN,并得到相应的物理地址(PhysicalAddress,PA)。同时,处理器通过cache编码地址的索引域(cacheLinelndex)可以很快找到相应的cache line组。但是这里的cacheblock的数据不一定是处理器所需要的,因此有必要进行一些检查,将cache line中存放的地址和通过虚实地址转换得到的物理地址进行比较。如果相同并且状态为匹配,就会发生cache命中(cache hit).那么处理器经过字节选择和偏移(Byte Select And Align)部件,最终就可以获取所需要的数据。如果发生cachemiss,处理器需要用物理地址进一步访问主存储器来获得最终数据,数据也会填充到相应的cacheline中。上述描述的是VIPT(virtualIndexphgsicalTag)的cache组织方式,将会在问题9中详细介绍。

如图1.4所示,是cache的基本的结构图。

Cache结构图

注意VIPT/VIVT和PIPT cache的区别,以上是VIVT cache。

数据访问过程,也可以参考下面:

1 )CPU内核(图中的ARM)发出VA请求读数据,TLB(translation lookaside buffer)接收到该地址,那为什么是TLB先接收到该地址呢?因为TLB是MMU中的一块高速缓存(也是一种cache,是CPU内核和物理内存之间的cache),它缓存最近查找过的VA对应的页表项,如果TLB里缓存了当前VA的页表项就不必做translation table walk了,否则就去物理内存中读出页表项保存在TLB中,TLB缓存可以减少访问物理内存的次数。

2页表项中不仅保存着物理页面的基地址,还保存着权限和是否允许cache的标志。MMU首先检查权限位,如果没有访问权限,就引发一个异常给CPU内核。然后检查是否允许cache,如果允许cache就启动cache和CPU内核互操作。

3)如果不允许cache,那直接发出PA从物理内存中读取数据到CPU内核。

4)如果允许cache,则以VA为索引到cache中查找是否缓存了要读取的数据

如果cache中已经缓存了该数据(称为cache hit)则直接返回给CPU内核,如果cache中没有缓存该数据(称为cache miss),则发出PA从物理内存中读取数据并缓存到cache中,同时返回给CPU内核。但是cache并不是只去CPU内核所需要的数据,而是把相邻的数据都去上来缓存,这称为一个cache line。ARM920T的cache line是32个字节,例如CPU内核要读取地址0x30000134~0x3000137的4个字节数据,cache会把地址0x30000120~0x3000137(对齐到32字节地址边界)的32字节都取上来缓存。

原文链接:

2. Cache基本概念

术语 描述
Cache Line Cache是由一组称为缓存行(Cacheline)的固定大小的数据块组成,其大小是以突发读或者突发写为基础的。
Cache hit/miss CPU访问的内存数据在cache中存在时,称为cache命中
(cache hit),否则称为cache miss。大多数现代CPU内部会有相关管理单元来统计cache命中率或cache miss率
Write through 写穿。当Cache hit时,更新的数据会同时写入cache和内存。cache和主存的数据始终保持一致。
Write Back 写回。当Cache hit时,只更新cache中的数据。每个cacheline中会有一个bit位记录数据是否被修改过,称之为dirty bit.主存中的数据可能是未修改的数据,而修改的数据躺在cache中。cache和主存的数据可能不一致。
Read Allocate 读分配。当读数据发生Cache miss时,分配一个cacheline缓存从主存读取的数据。默认情况下,cache都支持读分配。
Write Allocate 写分配。当写数据发生Cache miss时,如果支持写分配,先从主存中加载数据到cache line中(相当于先做个读分配动作),然后更新cache line中的数据。如果不支持写分配,写指令只会更新主存数据,不分配cacheline.
Ambiguity 歧义
Alias 别名
术语描述
VIVT virtualindexvirtualtag,虚拟地址索引,虚拟地址标签。
关于index,tag等内容,单独讲解
VIPT virtualindexphysicaltag,虚拟地址索引,物理地址标
PIPT physicalindexPhysicaltag,物理地址索引,物理地址标
关于index,tag等内容,单独讲解
Cache thrashing cache颠簸
Clean cacheline的dirty bit.如果dirty bit为1,cacheline
的内容写回下一级存储,并将dirty bit设为0
lnvalidate 检查对应内存cacheline的validbit为1则将
其设置为0
Flush 每条cacheline先clean,之后再invalid

2.1 VIVT-虚拟高速缓存

VIVT(Virtually-Indexed Virtually-Tagged)

虚拟高速缓存以虚拟地址作为查找对象,即虚拟地址做 index,虚拟地址做 Tag,在查找 Cache line 过程中不借助物理地址,在 ARM 初期的 ARM4、ARM5 等处理器的 Cache 主要采用这种方式。

虚拟高速缓存(VIVT)以虚拟地址(VA)作为查找对象,当CPU发出虚拟地址时,Cache 根据送来的虚拟地址中的 index 部分在 Cache中查找对应的 Cache line,将查找的 Cache line 对应的 Tag 部分与虚拟地址(VA)的 Tag 部分进行比较,若 Tag 部分相同,则命中。

之后根据Cache line中的有效位(valid)确定 Cache line 中的数据是否有效,有效则根据虚拟地址中的 Offset 部分找到对应的数据,将其返回给 CPU。若在比较 Tag 过程中发现不对应,则发生 Cache miss,此时需要将虚拟地址通过 MMU 转为物理地址后根据物理地址找到对应的主存数据存放地址,将数据返回给 CPU 和 Cache。

优点

CPU在进行读取或写入操作时,不需要每次都将虚拟地址通过 MMU 转为物理地址,这大大节省了 CPU 等待的时间。同时由于不需要进行虚拟地址转物理地址,所以硬件设计也相对更加简单。

缺点

当然 VIVT 也有它的不足,在进行数据查找过程中,由于使用虚拟地址作为 Tag,所以引入两个问题。 歧义(ambiguity) 和 别名(alias) 。为了保证系统的正确工作,可以使用操作系统来避免出现歧义和别名两个问题。

2.2 VIPT-物理标记的虚拟高速缓存

VIPT(Virtually-Indexed Physically-Tagged)

在使用虚拟存储器的系统中,物理标记的虚拟高速缓存VIPT 使用虚拟地址做 index ,物理地址做 Tag。 在利用虚拟地址索引 Cache 的同时利用 TLB/MMU 将虚拟地址转换为物理地址。然后将转换后的物理地址与虚拟地址索引到的 Cache line 中的 Tag 作比较,如果匹配则命中。这种方式要比 VIVT 实现复杂,在进行进程切换时,不在需要对 Cache 进行 invalidate 等操作(因为匹配过程中需要借物理地址)。但是这种方法仍然存在 Cache 别名(Alias)的问题(即两个不同的虚拟地址映射到同一物理地址,且位于不同的 Cache line)。

物理标记的虚拟高速缓存(VIPT)以物理地址部分位作为 Tag ,使用虚拟地址 index 进行 Cache line 的索引。当 CPU 发出虚拟地址(VA)时,根据虚拟地址的 index 进行 Cache line 的查找,同时,内存管理单元(MMU)将虚拟地址(VA)转为物理地址(PA),当地址转换完成,同时 Cache 控制器也对 Cache line 查找完成,此时将查找到的 Cache line 对应的 Tag 和物理地址 Tag 域进行比较,以判断 Cache 是否命中。

2.3 PIPT-物理高速缓存

PIPT(Physically-Indexed,Physically-Tagged)

在使用虚拟存储器的系统中,物理高速缓存 PIPT 中的 Tag 和 index 均为物理地址,而 CPU 发出的是虚拟地址,这便需要通过 TLB/MMU 查询内存中的页表,先将 CPU 发出的虚拟地址转换为物理地址,再进行 Cache 的缓存查找,这种先将 CPU 发出的虚拟地址转为物理地址,之后将物理地址作为访问 Cache 的 Tag 和 index 的方式称为 PIPT(Physically-Indexed,Physically-Tagged)。

ARM 的 Cortex-A 系列高性能处理器大多采用 PIPT 方式,PIPT 的方式在芯片的设计要比 VIPT 复杂很多,而且需要等待 TLB/MMU 将虚拟地址转换为物理地址后,才能进行 Cache line 寻找操作,所以相对的速度相较于 VIPT 慢。由于寻址 TLB 的过程也需要消耗一定的时间,对处理器的周期时间会造成一定的影响,所以一些处理器将访问 TLB 的过程单独作为一个流水线,用以减少访问 TLB 对处理器周期时间的影响。

物理高速缓存 PIPT使用物理地址做 Tag 和 index 。首先CPU 发出的虚拟地址经过 MMU 转换成物理地址,之后将物理地址发往 Cache 控制器,通过物理地址中的 index 部分对 Cache line 进行查找,查找到对应 Cache line 后将物理地址的 Tag 部分与 Cache line 中的 Tag 部分进行比较,如果相同,则命中,之后根据有效位(valid)进一步确定是否直接返回给 CPU,如果Tag 部分不相同,则将内存中对应地址的数据取出返回给 CPU 和 Cache。

3 Cache原理

3.1 多级cache

cache是多级的,在一个系统中你可能会看到L1、L2、L3, 当然越靠近core就越小,也是越昂贵。一般来说,对于bit.LITTLE架构中,在L1是core中,L1又分为L1 data cache和 L1 Instruction cache, L2 cache在cluster中,L3则在BUS总线上。

在ARM架构中,L1 cache都是VIPT的,也就是当有一个虚拟地址送进来,MMU在开始进行地址翻译的时候,Virtual Index就可以去L1 cache中查询了,MMU查询和L1 cache的index查询是同时进行的。如果L1 Miss了,则再去查询L2,L2还找不到则再去查询L3。 注意在arm架构中,仅仅L1是VIPT,L2和L3都是PIPT。

3.2 bit-LITTLE架构的cache

bit-LITTLE的架构图

在bit-LITTLE的架构中,L1是在core中的,是core私有的;L2是在cluster中的,对cluster中的core是共享的;L3则对所有cluster共享。bit-LITTLE的架构的一个cache层级关系图如下所示:

3.3 DynamIQ架构架构的cache

随着时代的发展科技的进步,除了了DynamIQ架构后,整个系统的system memory架构也在悄然无息的发生了变化。刚开始的架构如下所示:L1/L2在core中,L3在DSU中,DSU对外是ACE接口,结合CCI 来维护多cluster之间的缓存一致性。

后来随着CHI的越来越成熟,又变成了:L1/L2在core中,L3在DSU中,DSU对外是CHI接口,结合CMN来维护多cluster之间的缓存一致性。

但是不管怎么说,在dynamIQ的架构中,L1和L2都在core中的,都是core私有的;L3则是在cluster中的,对cluster中的core是共享的;如有L3或system cache,则是所有cluster共享。dynamIQ的架构的一个cache层级关系图如下所示:

3.4 Cache的组织结构

Cache从结构上分为全相连 直接相连 多路组相连(如4路组相连)cache。在一个core中一个架构中一个SOC中,所有cache的组织形式并不是都一样的。即使L1 D-cache和L1 I-cache的组织形式,也都可能不是一样的的。 具体的组织形式是怎样的,需要查询你的core TRM手册。

因为有了多路组相连这个cache,所以也就有了这些术语概念:

Tag是cache内存地址的一部分,用于关联与某一行数据的主内存地址。

64位地址的最高位(Tag)告诉cache信息来自于主内存。cache的总大小衡量了其所能容纳的数据量。尽管用于容纳Tag值的RAM不包括在计算中,但是Tag确实占用了cache中的物理空间。

Cache Line 一个Tag关联一组Cache数据

如果为每个Tag地址保存一个字的数据,效率会很低,所以通常在同一个Tag下将几个位置组合在一起。这种逻辑块通常被称为Cache Line,指的是缓存中最小的可加载单元,即主内存中的一个连续字块。当一个Cache Line包含已缓存的数据或指令时,它被认为是有效的,而当它不包含已缓存的数据或指令时,则是无效的。与每一行数据相关的是一个或多个状态位。通常情况下,你有一个有效位,将该行标记为包含可使用的数据。这意味着该地址标签代表了一些真实的值。在数据缓存中,你可能还有一个或多个脏位,标记缓存行(或其一部分)是否包含与主内存内容不相同(比其新)的数据。

Index是内存地址的一部分,它决定了在cache的第几行可以找到这个地址。

64位地址的中间位,即Index,标识了此地址是在Cache的第几行。Index被用作查找Cache RAM的地址,不需要作为Tag的一部分进行存储。

Way是一个缓存的细分,每条Way的大小相等,并以相同的方式进行索引(Index)。一个Set由共享一个特定索引的所有方式的Cache Line组成。这意味着地址的低几位(称为Offset)不需要存储在标签中。你需要的是整行的地址,而不是行内每个字节的地址。因此,64位地址中其余的五个或六个最不重要的位总是0。

总结一下:Cache由一片片Set和对应的Tag表组成。对于每片Set,它又由Cache Line一条条组成。Tag表标记了各行Tag所关联的Cache Line。

index : 用白话理解,其实就是在一块cache中,一行一行的编号(事实是没有编号/地址的)

Set :用index查询到的cache line可能是多个,这些index值一样的cacheline称之为一个set

way:用白话来说,将cache分成了多个块(多路),每一块是一个way

cache TAG :查询到了一行cache后,cacheline由 TAG + DATA组成

cache Data :查询到了一行cache后,cacheline由 TAG + DATA组成

cache Line 和 entry 是一个概念

3.5 Cache的分配策略

读分配(read allocation)

当CPU读数据时,发生cache缺失,这种情况下都会分配一个cache line缓存从主存读取的数据。默认情况下,cache都支持读分配。

读分配(read allocation) 写分配(write allocation)

当CPU写数据发生cache缺失时,才会考虑写分配策略。当我们不支持写分配的情况下,写指令只会更新主存数据,然后就结束了。当支持写分配的时候,我们首先从主存中加载数据到cache line中(相当于先做个读分配动作),然后会更新cache line中的数据。

写直通(write through)

当CPU执行store指令并在cache命中时,我们更新cache中的数据并且更新主存中的数据。cache和主存的数据始终保持一致。

读分配(read allocation)写回(write back)

当CPU执行store指令并在cache命中时,我们只更新cache中的数据。并且每个cache line中会有一个bit位记录数据是否被修改过,称之为dirty bit(翻翻前面的图片,cache line旁边有一个D就是dirty bit)。我们会将dirty bit置位。主存中的数据只会在cache line被替换或者显示的clean操作时更新。因此,主存中的数据可能是未修改的数据,而修改的数据躺在cache中。cache和主存的数据可能不一致

3.6 Cache的查询过程

很多时候cache都是和MMU一起使用的(即同时开启或同时关闭),因为MMU页表的entry的属性中控制着内存权限和cache缓存策略等。

高速缓存控制器(cache controller )是负责管理高速缓存内存的硬件块,其方式对程序来说在很大程度上是不可见的。它自动将代码或数据从主存写入缓存。它从core接收读取和写入内存请求,并对高速缓存或外部存储器执行必要的操作。

当它收到来自core的请求时,它必须检查是否能在缓存中找到所请求的地址。这称为缓存查找(cache look-up)。它通过将请求的地址位的subset(index)与与缓存中的physical TAG 进行比较来做到这一点。如果存在匹配,称为命中(hit),并且该行被标记为有效,则使用高速缓存进行读取或写入。

当core从特定地址请求指令或数据,但与缓存标签不匹配或标签无效时,会导致缓存未命中,请求必须传递到内存层次结构的下一层,即 L2缓存或外部存储器。它还可能导致缓存行填充。缓存行填充会导致将一块主内存的内容复制到缓存中。同时,请求的数据或指令被流式传输到core。这个过程软件开发人员不能直接看到。在使用数据之前,core不需要等待 linefill 完成。高速缓存控制器通常首先访问高速缓存行内的关键字。例如,如果您执行的加载指令在缓存中未命中并触发缓存行填充,则内核首先检索缓存行中包含所请求数据的那部分。这些关键数据被提供给core流水线,而缓存硬件和外部总线接口随后在后台读取缓存线的其余部分。

总结一下就是:先使用index去查询cache,然后再比较TAG,比较TAG之后再检查valid标志位。但是这里要注意:TAG包含了不仅仅是物理地址,还有很多其它的东西,如NS比特位等,这些都是在比较TAG的时候完成

假设一个4路相连的cache(如cortex-A710),大小64KB, cache line = 64bytes,那么 1 way = 16KB,indexs = 16KB / 64bytes = 256 (注: 0x4000 = 16KB、0x40 = 64 bytes)

0x4000 – index 0

0x4040 – index 1

0x4080 – index 2

0x7fc0 – index 255

0x8000 – index 0

0x8040 – index 1

0x8080 – index 2

0xbfc0 – index 255

3.7 cache的一致性

Cache的一致性是由MESI协议来实现,由scu(snoop control unit)单元实现cache一致性处理。

参考: 高速缓存与一致性 - 知乎 (zhihu.com)

Arm提供了一系列指令去操作cache,维护cache的一致性:

Arm提供了操作cache的指令, 软件维护操作cache的指令有三类:

Invalidation:其实就是修改valid bit,让cache无效,主要用于读

Cleaning: 其实就是我们所说的flush cache,这里会将cache数据回写到内存,并清楚dirty标志

Zero:将cache中的数据清0, 这里其实是我们所说的clean cache.

什么时候需要软件维护cache:

(1)、当有其它的Master改变的external memory,如DMA操作

(2)、MMU的enable或disable的整个区间的内存访问,如REE enable了mmu,TEE disable了mmu.

针对第(2)点,cache怎么和mmu扯上关系了呢?那是因为:

mmu的开启和关闭,影响了内存的permissions, cache policies

以下是对维护cache的一致性指令做出的一个总结

cache一致性指令的使用示例

cache一致性的API

在操作系统中,我们只需要调用相关的API即可,也无需牢记以上的维护cache一致性的命令。

比如在Linux Kernel 操作Cache的API如下所示:

linux/arch/arm64/mm/cache.S

linux/arch/arm64/include/asm/cacheflush.hvoid __flush_icache_range(unsigned long start, unsigned long end);

int invalidate_icache_range(unsigned long start, unsigned long end);

void __flush_dcache_area(void *addr, size_t len);

void __inval_dcache_area(void *addr, size_t len);

void __clean_dcache_area_poc(void *addr, size_t len);

void __clean_dcache_area_pop(void *addr, size_t len);

void __clean_dcache_area_pou(void *addr, size_t len);

long __flush_cache_user_range(unsigned long start, unsigned long end);

void sync_icache_aliases(void *kaddr, unsigned long len);

void flush_icache_range(unsigned long start, unsigned long end)

void __flush_icache_all(void)

当有其它硬件(如DMA)和CPU访问同一块内存时,那么这个时候的操作需要小心,一般也就是记得调用invalid cache和flush cache相关的函数即可

多核多cluster多系统之间缓存一致性

有三种机制可以保持多核多系统cache一致性:

禁用缓存是最简单的机制,但可能会显着降低 CPU 性能。为了获得最高性能,处理器通过管道以高频率运行,并从提供极低延迟的缓存中运行。缓存多次访问的数据可显着提高性能并降低 DRAM 访问和功耗。将数据标记为“非缓存”可能会影响性能和功耗。

软件管理的一致性是数据共享问题的传统解决方案。在这里,软件(通常是设备驱动程序)必须清除或刷新缓存中的脏数据,并使旧数据无效,以便与系统中的其他处理器或主设备共享。这需要处理器周期、总线带宽和功率。

硬件管理的一致性提供了一种简化软件的替代方案。使用此解决方案,任何标记为“共享”的缓存数据将始终自动更新。该共享域中的所有处理器和总线主控器看到的值完全相同。

燃鹅,我们在ARM架构中,默认使用的却是第三种 硬件管理的一致性, 意思就是:做为一名软件工程师,我们啥也不用管了,有人帮我们干活,虽然如此,但我们还是希望理解下硬件原理。

再讲原理之前,我们先补充一个场景:

假设在某一操作系统中运行了一个线程,该线程不停着操作0x4000_0000地址处内存(所以我们当然期望,它总是命中着),由于系统调度,这一次该线程可能跑在cpu0上,下一次也许就跑在cpu1上了,再下一次也许就是cpu4上了(其实这种行为也叫做CPU migration)

或者举个这样的场景也行:

在Linux Kernel系统中,定义了一个全局性的变量,然后多个内核线程(多个CPU)都会访问该变量.

在以上的场景中,都存在一块内存(如0x4000_0000地址处内存)被不同的ARM CORE来访问,这样就会出现了该数据在main-memory、SCU-0的L2 cache、SCU-1的L2 cache、8个Core的L1 cache不一致的情况。

既然出现了数据在内存和不同的cache中的不一致的情况,那么就需要解决这个问题(也叫维护一致性),那么怎么维护的呢,上面也说了“使用 硬件管理的一致性”,下面就以直接写答案的方式,告诉你硬件是怎样维护一致性的。

多cluster之间的缓存一致性

cluster和外界的接口,可以是ACE或CHI(目前常用的是ACE,后面的趋势可能是CHI)

  • 如果使用的是ACE,那么多cluster之间的一致性,依靠CCI + ACE 来维护
  • 如果使用的是CHI,那么多cluster之间的一致性,依靠CMN + CHI来维护

bit.LITTLE架构 和 DynamIQ架构 的系统中的缓存一致性

我们先看一张老的bit.LITTLE架构图

core1、core1的cache的一致性,是由SCU-0来维护的(至于这里是不是遵守了MESI协议,答案:YES)

cluster0、cluster1的cache的一致性,是由于CCI-400来维护的(至于这里是不是遵守了MESI协议,答案:NO)再看一张比较新的图(DynamIQ架构的)吧

core0的cache(含L1/L2)、core1的cache(含L1/L2)的一致性 由DSU-0来维护(至于这里是不是遵守了MESI协议,答案: YES)

cluster0的 L3 cache、cluster1的 L3 cache、Mali的L2 cache的一致性是由于CCI-550来维护的(至于这里是不是遵守了MESI协议,NO)

看完以上信息,我们再次总结一下,我们学习cache一致性,我们最大的困惑或瓶颈是啥,是不理解MESI吗?应该还是对架构的理解和认知。学习MESI,不如去学习cache硬件基础、cache TAG、DSU、CCI-550原理吧。

多核之间的缓存一致性,由SCU(DSU)硬件来执行维护,使用MESI协议

多cluster之间的缓存一致性,由 CCI/CMN 来执行维护,没有使用MESI协议.

多级cache之间的替换策略

1、L1 cache的替换策略是什么,L2和L3的呢

2、哪些的替换策略是由硬件决定的(定死的,软件不可更改的),哪些的替换策略是软件可以配置的?

3、在经典的 DynamIQ架构 中,数据是什么时候存在L1 cache,什么时候存进L2 cache,什么时候又存进L3 cache,以及他们的替换策略是怎样的? 比如什么时候数据只在L1? 什么时候数据只在L2? 什么时候数据只在L3? 还有一些组合,比如什么时候数组同时在L1和L3,而L2没有? 这一切的规则是怎样定义的?

本文讨论经典的DynamIQ的cache架构,忽略 big.LITTLE的cache架构

dynamIQ 架构中 L1和L2之间的替换策略,是由core的inclusive/exclusive的硬件特性决定的,软无法更改

core cache之间的替换策略,是由SCU(或DSU)执行的MESI协议中定义的,软件也无法更改。

cluster cache之间的替换策略,是由于MMU页表中的内存属性定义的(innor/outer/cacheable/shareable),软件可以修改

4. MMU原理

MMU是处理器/核(processer)中的一个硬件单元,通常每个核有一个MMU。MMU由两部分组成:TLB(Translation Lookaside Buffer)和table walk unit。

4.1 MMU的架构

快表,直译为旁路快表缓冲,也可以理解为页表缓冲,地址变换高速缓存。

由于页表存放在主存中,因此程序每次访存至少需要两次:一次访存获取物理地址,第二次访存才获得数据。提高访存性能的关键在于依靠页表的访问局部性。当一个转换的虚拟页号被使用时,它可能在不久的将来再次被使用到,。

TLB是一种高速缓存,内存管理硬件使用它来改善虚拟地址到物理地址的转换速度。当前所有的个人桌面,笔记本和服务器处理器都使用TLB来进行虚拟地址到物理地址的映射。使用TLB内核可以快速的找到虚拟地址指向物理地址,而不需要请求RAM内存获取虚拟地址到物理地址的映射关系。

从协处理器CP15的寄存器2(TTB寄存器,translation table base register ARM架构,X86中是CR3)中取出保存在其中的第一级页表(translation table)的基地址。这个基地址指的是PA,也就是说页表是直接按照这个地址保存在物理内存中的。

以TTB中的内容为基地址,以VA[31:20]为索引值在一级页表中查找对应表项。这个页表项保存着第二级页表(coarse page table)的基地址,这同样是物理地址,也就是说第二级页表也是直接按这个地址存储在物理内存中的。以VA[19:12]为索引值在第二级页表中查出表项,这个表项中就保存着物理页面的基地址,我们知道虚拟内存管理是以页为单位的,一个虚拟内存的页映射到一个物理内存的页框,从这里就可以得到印证,因为查表是以页为单位来查的。有了物理页面的基地址之后,加上VA[11:0]这个偏移量就可以取出相应地址上的数据了。

这个过程称为Translation Table Walk,Walk这个词用得非常形象。从TTB走到一级页表,又走到二级页表,又走到物理页面,一次寻址其实是三次访问物理内存。注意这个“走”的过程完全是硬件做的,每次CPU寻址时MMU就自动完成以上四步,不需要编写指令指示MMU去做,前提是操作系统要维护页表项的正确性,每次分配内存时填写相应的页表项,每次释放内存时清除相应的页表项,在必要的时候分配或释放整个页表。

  • TTBR:Translation Table Base Register,页表基地址寄存器,存放的是页表基地址(物理地址),页表的起始地址是操作系统软件维护的。页表基地址寄存器和各级页表项中存放的都是物理地址,而不是虚拟地址.
    MMU的TTBR0和TTBR1,二级页表基地址转换
    0x0 ~0x0000 FFFF FFFF FFFF是 TTBR0
    0xFFFF 0000 0000 0000 是TTBR1


4.2 什么是页表?
页表的作用

  • 地址管理
    将虚拟地址转换为物理地址
  • 权限管理
    管理cpu对物理页的访问,如读写执行权限
  • 隔离地址空间

隔离各个进程的地址空间,使其互不影响,提供系统的安全性,打开mmu后,对没有页表映射的虚拟内存访问或者有页表映射但是没有访问权限都会发生处理器异常,内核选择杀死进程或者panic;通过页表给一段内存设置用户态不可访问, 这样可以做到用户态的用户进程不能访问内核地址空间的内容;而由于用户进程各有一套自己的页表,所以彼此看不到对方的地址空间,更别提访问,造成每个进程都认为自己拥有所有虚拟内存的错觉;通过页表给一段内存设置只读属性,那么就不容许修改这段内存内容,从而保护了这段内存不被改写;对应用户进程地址空间映射的物理内存,内核可以很方便的进行页面迁移和页面交换,而对使用虚拟地址的用户进程来说是透明的;通过页表,很容易实现内存共享,使得一份共享库很多进程都可以映射到自己地址空间使用;通过页表,可以小内存加载大应用程序运行,在运行时按需加载和映射

4.3 页表遍历过程

下面以arm64处理器架构多级页表遍历作为结束(使用4级页表,页大小为4K):

Linux内核中 可以将页表扩展到5级,分别是页全局目录(Page Global Directory, PGD),页4级目录(Page 4th Directory, P4D),页上级目录(Page Upper Directory, PUD),页中间目录(Page Middle Directory, PMD),直接页表(Page Table, PT),而支持arm64的linux使用4级页表结构分别是 pgd, pud, pmd, pt ,arm64手册中将他们分别叫做L0,L1,L2,L3级转换表,所以一下使用L0-L3表示各级页表。TLB miss时,mmu会进行多级页表遍历遍历过程如下:

1)mmu根据虚拟地址的最高位判断使用哪个页表基地址寄存器作为起点:当最高位为0时,使用ttbr0_el1作为起点(访问的是用户空间地址);当最高位为1时,使用ttbr1_el1作为起点(访问的是内核空间地址) mmu从相应的页表基地址寄存器中获得L0转换表基地址。

2)找到L0级转换表,然后从虚拟地址中获得L0索引,通过L0索引找到相应的表项(arm64中称为L0表描述符,内核中叫做PGD表项),从表项中获得L1转换表基地址。

3)找到L1级转换表,然后从虚拟地址中获得L1索引,通过L1索引找到相应的表项(arm64中称为L1表描述符,内核中叫做PUD表项),从表项中获得L2转换表基地址。

4)找到L2级转换表,然后从虚拟地址中获得L2索引,通过L2索引找到相应的表项(arm64中称为L2表描述符,内核中叫做PUD表项),从表项中获得L3转换表基地址。

5)找到L3级转换表,然后从虚拟地址中获得L3索引,通过L3索引找到页表项(arm64中称为页描述符,内核中叫做页表项)。

6)从页表项中取出物理页帧号然后加上物理地址偏移(VA[11,0])获得最终的物理地址。

ARMv8中,Kernel Space的页表基地址存放在TTBR1_EL1寄存器中,User Space页表基地址存放在TTBR0_EL0寄存器中,其中内核地址空间的高位为全1,(0xFFFF0000_00000000 ~ 0xFFFFFFFF_FFFFFFFF),用户地址空间的高位为全0,(0x00000000_00000000 ~ 0x0000FFFF_FFFFFFFF)

ARMv8中虚拟地址支持:64位虚拟地址中,并不是所有位都用上,除了高16位用于区分内核空间和用户空间外,

有效位的配置可以是:36, 39, 42, 47。这可决定Linux内核中地址空间的大小。

比如使用的内核中有效位配置为CONFIG_ARM64_VA_BITS=39,

用户空间地址范围:0x00000000_00000000 ~ 0x0000007f_ffffffff,大小为512G,

内核空间地址范围:0xffffff80_00000000 ~ 0xffffffff_ffffffff,大小为512G。

页面大小支持:支持3种页面大小:4KB, 16KB, 64KB。

页表支持:支持至少两级页表,至多四级页表,Level 0 ~ Level 3。

4.4 Linux的页表

PGD:Page Global Directory、PUD:Page Upper Directory、PMD:Page Middle Directory、PTE:Page Table Entry。

在ARM32 Linux采用两层映射,省略了PMD,除非定义 CONFIG_ARM_LPAE才会使用3级映射。

PTEs, PMDs和PGDs分别由pte_t, pmd_t 和pgd_t来描述。为了存储保护位,pgprot_t被定义,它拥有相关的flags并经常被存储在page table entry低位(lower bits),其具体的存储方式依赖于CPU架构。 每个pte_t指向一个物理页的地址,并且所有的地址都是页对齐的。因此在32位地址中有PAGE_SHIFT(12)位是空闲的,它可以为PTE的状态位。

ARMV8 Linux页表映射:

39位有效位,4KB大小页面,3级页表

代码路径:

arch/arm64/include/asm/pgtable-types.h:定义pgd_t, pud_t, pmd_t, pte_t等类型;

arch/arm64/include/asm/pgtable-prot.h:针对页表中entry中的权限内容设置;

arch/arm64/include/asm/pgtable-hwdef.h:主要包括虚拟地址中PGD/PMD/PUD等的划分,这个与虚拟地址的有效位及分页大小有关,此外还包括硬件页表的定义, TCR寄存器中的设置等;

arch/arm64/include/asm/pgtable.h:页表设置相关

当CONFIG_PGTABLE_LEVELS=4时:pgd–>pud–>pmd–>pte;

当CONFIG_PGTABLE_LEVELS=3时,没有PUD页表:pgd(pud)–>pmd–>pte;

当CONFIG_PGTABLE_LEVELS=2时,没有PUD和PMD页表:pgd(pud, pmd)–>pte

启动页表

在汇编代码阶段的head.S文件中,负责创建映射关系的函数是create_page_tables。create_page_tables函数负责identity mapping和kernel image mapping。

identity map:是指把idmap_text区域的物理地址映射到相等的虚拟地址上,这种映射完成后,其虚拟地址等于物理地址。idmap_text区域都是一些打开MMU相关的代码。

kernel image map:将kernel运行需要的地址(kernel txt、rodata、data、bss等等)进行映射

Arm Linux 32bit的页表映射: