【ARM中文手册】第八章 Caches

文摘   2024-11-14 07:24   上海  

8.Caches


"缓存"这个词来源于法语动词“cacher”,意为“隐藏”。将这个词应用于处理器是显而易见的——缓存是处理器用于存储指令和数据的地方,对程序员和系统是隐藏的。在许多情况下,可以说缓存对你是透明的或隐藏的。但正如我们将看到的,了解缓存的操作细节往往非常重要。

当ARM架构最初开发时,处理器的时钟速度和内存的访问速度大致相似。如今的处理器核心要复杂得多,时钟速度可以快上好几个数量级。然而,外部总线和内存设备的频率并没有按同样的比例提升。虽然可以实现与核心速度相同的小块片上SRAM,但这种RAM相对于标准DRAM来说非常昂贵,后者的容量可以大几千倍。在许多基于ARM处理器的系统中,访问外部内存可能需要几十甚至几百个核心周期。

本质上,缓存是一个小而快速的内存块(至少在概念上),它位于核心和主存之间。缓存存储着主存中的项目副本。对缓存的访问速度明显快于对主存的访问。由于缓存只存储了主存内容的一个子集,因此它必须存储主存中项目的地址及其相关的数据。每当核心想要读取或写入某个特定地址时,它会首先在缓存中查找该地址。如果在缓存中找到地址,它将使用缓存中的数据,而不必访问主存。通过减少外部存储访问时间的影响,这显著提高了系统的潜在性能。此外,它通过避免驱动外部信号,减少了系统的功耗。

相对于系统中使用的总内存,缓存的大小较小。更大的缓存会导致芯片成本增加。此外,增加内部核心缓存的大小可能会限制核心的最大速度。如何高效利用这一有限资源是编写高效应用程序以在核心上运行的关键部分。

片上SRAM可以用来实现缓存,缓存存储主存中指令和数据的临时副本。代码和数据具有时间局部性和空间局部性的特性。这意味着程序倾向于在一段时间内重复使用相同的地址(时间局部性),并且倾向于使用彼此接近的地址(空间局部性)。例如,代码中可能包含循环,这意味着相同的代码会被反复执行,或者函数会被多次调用。数据访问(例如对栈的访问)可能局限于内存的较小区域。正是由于核心对RAM的访问表现出这种局部性,而不是完全随机的,这使得缓存的使用变得有效。

写缓冲区是一个将核心执行存储指令时的写操作与外部存储总线解耦的模块。核心将与存储相关的地址、控制和数据值放入一组硬件缓冲区中。像缓存一样,它位于核心和主存之间。这使核心能够继续执行下一条指令,而无需停下来等待较慢的主存完成写操作。

8.1 Why do caches help?

正如我们所看到的,缓存可以加快速度,因为程序的执行并不是随机的。程序倾向于反复访问相同的数据集并反复执行相同的指令集。通过在代码或数据首次被访问时将其移动到更快的内存中,随后的访问就变得更快。提供数据到缓存的初次访问并不比正常访问更快,然而,随后的缓存值访问速度更快,这就是性能提升的来源。核心硬件会检查所有指令提取和数据读写是否在缓存中,尽管显然你必须将某些内存部分(例如包含外设设备的部分)标记为不可缓存的。由于缓存只保存了主存的一部分,必须有一种快速判断你正在查找的地址是否在缓存中的方法。

8.2 Cache drawbacks

缓存和写缓冲区似乎自动带来了好处,因为它们加快了程序的执行速度。然而,它们也引入了一些在无缓存核心中不存在的问题。其中一个缺点是程序的执行时间可能变得不确定。

这意味着,由于缓存较小且只保存主存的一部分,它在程序执行过程中会迅速填满。当缓存满了时,必须移除现有的代码或数据以为新项目腾出空间。因此,在任何特定时刻,应用程序通常无法确定某个指令或数据项是否在缓存中。

这意味着特定代码片段的执行时间可能会显著变化。这在需要强确定性行为的硬实时系统中可能会成为一个问题。

此外,你还需要一种方法来控制缓存和写缓冲区如何访问内存的不同部分。在某些情况下,你希望核心从外部设备(例如外设)读取最新数据。例如,缓存定时器外设的值显然是不明智的。有时你希望核心暂停并等待存储操作完成。因此,缓存和写缓冲区给你增加了一些额外的工作。

有时缓存内容与外部内存的内容可能不同,这是因为处理器可以更新缓存内容,而这些内容尚未写回主存。或者,其他设备可能在核心获取自己的副本后更新了主存。这就是一致性问题。当有多个核心或像外部DMA控制器这样的内存代理时,这个问题尤为突出。

8.3 Memory hierarchy

在计算机科学中,内存层次结构指的是一种内存类型的层次排列,速度更快且容量更小的内存靠近核心,速度较慢且容量较大的内存位于较远处。在大多数系统中,你可以拥有二级存储(如磁盘驱动器)和一级存储(如闪存、SRAM 和 DRAM)。在嵌入式系统中,这通常细分为片上和片外内存。与核心在同一芯片(或至少在同一封装)上的内存通常要快得多。

缓存可以包含在层次结构的任何级别上,并可以在内存系统的不同部分存在访问时间差异的情况下提高系统性能。

在基于 ARM 处理器的系统中,一级缓存(L1 缓存)通常直接连接到核心逻辑,该逻辑负责提取指令以及处理加载和存储指令。这些是哈佛缓存,即指令缓存和数据缓存是分开的,它们有效地表现为核心的一部分。

多年来,由于SRAM的尺寸和速度的改进,L1缓存的大小有所增加。在撰写本文时,最常见的缓存大小是16KB或32KB,因为这是能够在1GHz或更高核心速度下提供单周期访问的最大RAM大小。

许多ARM系统还具有二级缓存(L2缓存)。L2缓存比L1缓存大(通常为256KB、512KB或1MB),但速度较慢,并且是统一的(同时存储指令和数据)。它可以在核心内部,也可以作为外部模块实现,位于核心和内存系统的其余部分之间。ARM L2C-310就是一个这样的外部L2缓存控制器模块的例子。

此外,核心可以以集群的形式实现,其中每个核心都有自己的一级缓存。这样的系统需要维护缓存一致性的机制,以确保当一个核心更改某个内存位置时,这一更改对共享该内存的其他核心可见。

8.4 Cache architecture

在冯·诺依曼架构中,指令和数据使用同一个缓存(统一缓存)。而在改进的哈佛架构中,指令和数据总线是分开的,因此有两个缓存:一个是指令缓存(I-cache),另一个是数据缓存(D-cache)。在许多ARM系统中,你可以拥有独立的指令和数据一级缓存,后面由统一的二级缓存支持。

缓存需要存储地址、一些数据以及一些状态信息。32位地址的高位告诉缓存信息来自主存的哪个位置,这被称为标签(tag)。缓存的总大小是其可以存储数据量的度量,用于存储标签值的RAM不包括在这个计算中。然而,标签确实会占用缓存中的物理空间。

为每个标签地址只保存一个字的数据是低效的,因此通常将多个位置组合在同一个标签下。这个逻辑块通常称为缓存行。地址的中间位,或称为索引,标识该行。索引用于缓存RAM的地址,而不需要作为标签的一部分进行存储。本章稍后将对此进行更详细的介绍。当缓存行包含缓存的数据或指令时,称其为有效;当不包含时,称为无效。

这意味着地址的低几位(偏移量)不需要存储在标签中——你只需要整条缓存行的地址,而不需要行内每个字节的地址,所以最低的五到六位总是0。

与每行数据相关的还有一个或多个状态位。通常会有一个有效位,标记该行包含可以使用的数据(这意味着地址标签表示一些真实值)。在数据缓存中,还可能有一个或多个“脏位”,用于标记缓存行(或其一部分)是否包含与主存不同的数据(即数据更新)。

8.4.1 Cache terminology

以下是一些常用术语的简要总结,可能会有所帮助:

  • 缓存行(line):指缓存中最小的可加载单元,即来自主存的一块连续字节。

  • 索引(index):指内存地址的一部分,用于确定该地址可以在哪一行缓存中找到。

  • 路(way):缓存的一个子部分,每一条路的大小相等,并以相同的方式索引。来自每条缓存路的与特定索引值相关联的缓存行组合在一起形成一个集合(set)。

  • 标签(tag):缓存中存储的内存地址的一部分,用于标识与某一行数据相关联的主存地址。

8.4.2 Direct mapped caches

实现缓存有不同的方法,其中最简单的是直接映射缓存

在直接映射缓存中,主存中的每个位置映射到缓存中的单一位置。然而,由于主存远大于缓存,许多地址会映射到同一个缓存位置。下图显示了一个小型缓存,每行包含四个字节,共有四行。

这意味着缓存控制器将使用地址中的两位(位[3:2])作为偏移量,以选择缓存行内的某个字,使用地址中的两位(位[5:4])作为索引,以选择四个可用缓存行中的一个。地址的其余部分(位[31:6])将作为标签值存储。

为了在缓存中查找某个特定地址,硬件会从地址中提取索引位,并读取缓存中该行关联的标签值。如果两者相同且有效位指示该行包含有效数据,则发生命中。然后,它可以使用地址中的偏移量和字节部分从缓存行中的相关字中提取数据值。如果该行包含有效数据但没有命中(即标签显示缓存中保存的是主存中的另一个地址),那么该缓存行将被移除,并由请求的地址数据替换。

显然,所有具有相同[5:4]位值的主存地址都会映射到缓存中的同一行。在任何给定时刻,缓存中只能有其中的一行。这意味着很容易发生一个称为抖动的问题。考虑一个循环,它反复访问地址 0x00、0x40 和 0x80,如下面的代码所示:

void add_array(int *data1, int *data2, int *result, int size) 
{
int i;
for (i = 0; i < size; i++) {
result[i] = data1[i] + data2[i];
}
}

在这个代码示例中,如果 resultdata1 和 data2 分别指向 0x00、0x40 和 0x80,那么这个循环将导致反复访问映射到基本缓存中同一行的内存位置:

  • 当你第一次读取地址 0x40 时,它不在缓存中,因此会进行一次行填充,将 0x40 到 0x4F 的数据放入缓存。

  • 然后,当你读取地址 0x80 时,它也不在缓存中,因此会进行另一次行填充,将 0x80 到 0x8F 的数据放入缓存——在此过程中,你会丢失缓存中 0x40 到 0x4F 的数据。

  • 结果会写入 0x00。根据分配策略,这可能导致另一次行填充,导致缓存中的 0x80 到 0x8F 数据丢失。

  • 这种情况在每次循环迭代中都会发生,从而导致软件性能下降。因此,直接映射缓存通常不会用于ARM核心的主缓存,但在某些地方确实可以看到它们——例如在 ARM1136 处理器的分支目标地址缓存中。

核心可能有硬件优化,用于处理整个缓存行被写入的情况。这种情况在某些系统中可能占据总周期时间的显著比例。例如,当执行 memcpy() 或 memset() 类函数进行块复制或大块的零初始化时,就会发生这种情况。在这些情况下,先读取将被覆盖的数据没有任何好处。这可能导致缓存的性能特性与正常预期有所不同。

缓存分配策略只是对核心的提示,并不保证某块内存一定会被读入缓存,因此不应依赖它们。

8.4.3 Set associative caches

ARM核心的主缓存始终使用组相联缓存实现。这大大减少了直接映射缓存中出现的缓存抖动问题,提高了程序执行速度,并使执行更具确定性。代价是硬件复杂度增加,且功耗略有增加(因为每个周期都要比较多个标签)。

在这种缓存组织方式中,缓存被分成多个大小相等的部分,称为(ways)。内存位置可以映射到某一条路而不是某一行。地址中的索引字段继续用于选择特定的行,但现在它指向每一条路中的单个行。通常情况下,有2路或4路,但一些ARM实现使用了更多路。

二级缓存实现(例如ARM L2C-310)可以有更多的路(更高的相联度),因为其大小大得多。具有相同索引值的缓存行称为属于同一组。为了检查是否命中,你必须查看该组中的每个标签。

在下图中,显示了一个 2 路缓存。来自地址 0x00(或 0x40,或 0x80)的数据可能会在两个缓存路中的某一条的第 0 行中找到,但不会同时存在于两个缓存路中。

增加缓存的相联度可以降低抖动的概率。理想情况下是全相联缓存,其中任何主存位置都可以映射到缓存中的任何地方。然而,除了非常小的缓存(例如,与MMU的TLB相关的缓存)外,构建这种缓存是不现实的。实际上,对于一级缓存来说,相联度超过4路的性能提升很小,而8路或16路的相联度对于较大的二级缓存则更有用。

8.4.4 A real-life example

在讨论写缓冲区之前,让我们看一个比前两个图中显示的更现实的例子。下图展示了一个4路组相联的32KB数据缓存,具有8字(32字节)的缓存行长度。这种缓存结构可以在Cortex-A7或Cortex-A9处理器上找到。

缓存行长度为8字(32字节),并且有4路。32KB除以4(路数),再除以32(每行的字节数),得出每路有256行。这意味着你需要8位来索引每一路中的行(位[12:5])。在这里,你必须使用地址的位[4:2]来选择行内的8个字中的一个,虽然具体需要多少位来索引行内数据取决于你访问的是字、半字还是字节。在这种情况下,剩余的位[31:13]将作为标签使用。

8.4.5 Cache controller

这是一个负责管理缓存内存的硬件模块,它的操作对程序来说基本是不可见的。它会自动将主存中的代码或数据写入缓存。该模块从核心接收读写内存请求,并对缓存内存或外部内存执行必要的操作。

当它接收到核心的请求时,必须检查请求的地址是否在缓存中,这称为缓存查找。它通过将请求的地址位的子集与缓存中的行关联的标签值进行比较来完成这一操作。如果匹配(命中)且该行标记为有效,则读写操作将使用缓存内存进行。

当核心请求某个地址的指令或数据,但缓存标签不匹配或标签无效时,就会发生缓存未命中,此时请求必须传递到下一级内存层次结构——例如L2缓存或外部内存。这也可能触发缓存行填充,即将主存的一块内容复制到缓存中。同时,请求的数据或指令会流向核心。这一过程是透明的,软件开发人员无法直接看到。

核心不需要等到行填充完成才能使用数据。缓存控制器通常会优先访问缓存行中的关键字。例如,当你执行一个缓存未命中的加载指令并触发缓存行填充时,核心会首先获取包含请求数据的那部分缓存行。关键数据会被提供给核心流水线,而缓存硬件和外部总线接口则在后台读取缓存行的其余部分。

8.4.6 Virtual and physical tags and indexes

本节假设读者对地址转换过程有一定了解。

早期的ARM处理器,如ARM720T或ARM926EJ-S处理器,使用虚拟地址来提供索引和标签值。这种方式的优点是核心可以在不需要虚拟地址到物理地址转换的情况下进行缓存查找。缺点是,系统中更改虚拟到物理地址的映射时,必须先清除和无效化缓存,这可能对性能产生显著影响。

ARM11系列处理器使用了不同的缓存标签方案。在这种方案中,缓存索引仍然来源于虚拟地址,但标签取自物理地址。物理标签方案的优点是,虚拟到物理地址映射的更改不再需要使缓存无效。这对于频繁修改转换表映射的复杂多任务操作系统来说,具有显著的优势。使用虚拟索引有一些硬件上的好处,它意味着缓存硬件可以同时从每一路中的适当行读取标签值,而不需要实际执行虚拟到物理地址的转换,从而提供快速的缓存响应。这种缓存通常被称为虚拟索引、物理标签(VIPT)。包括使用这些标签缓存的Cortex-A系列处理器的缓存特性列在下表中。


然而,VIPT(虚拟索引,物理标签)实现有一个缺点。对于4路组相联的32KB或64KB缓存,地址的[12]和[13]位用于选择索引。如果MMU使用4KB的页面,虚拟地址的[13:12]位可能与物理地址的[13:12]位不相等。因此,如果多个虚拟地址映射指向同一个物理地址,可能会出现缓存一致性问题。这一问题通过在内核翻译表软件上施加一些限制来解决。这被称为页面着色问题,在其他处理器架构上也由于同样的原因存在。

通过使用物理索引、物理标签(PIPT)缓存实现可以避免此问题。Cortex-A系列处理器的数据缓存使用这种方案。这意味着页面着色问题得以避免,但代价是硬件复杂性增加。

8.5 Cache policies

缓存操作中有许多不同的选择需要做出。需要考虑是什么原因导致从外部内存加载一行数据到缓存中(分配策略),以及控制器如何决定在组相联缓存中使用哪一行来存放新数据(替换策略)。当核心执行写操作并命中缓存时(写策略),也必须加以控制。

8.5.1 Allocation policy

当核心执行缓存查找时,如果所需的地址不在缓存中,它必须决定是否进行缓存行填充并将该地址从内存中复制过来。

  • 读分配策略 仅在读取时分配缓存行。如果核心执行了一个缓存未命中的写操作,缓存不会受到影响,写操作将直接传递到下一级内存层次结构。

  • 写分配策略 为未命中的读取或写入分配缓存行(因此更准确地称为读写缓存分配策略)。对于缓存未命中的内存读取和写入,都会执行缓存行填充。这通常与回写写策略结合使用。

8.5.2 Replacement policy

当发生缓存未命中时,缓存控制器必须从该组缓存行中选择一行来存放新的数据。被选中的缓存行称为受害者行(victim)。如果受害者行包含有效且已修改的数据,那么在新数据写入受害者行之前,必须将该行的内容写入主存。这称为驱逐(eviction)。

替换策略 控制受害者行的选择过程。地址的索引位用于选择缓存行的集合,而替换策略则从该集合中选择要替换的特定缓存行。

  • 轮替替换(Round-robin 或 Cyclic Replacement)意味着你有一个计数器(受害者计数器),它在可用的缓存路中循环,当达到最大路数时重新回到0。

  • 伪随机替换 随机选择一组中的下一个缓存行进行替换。受害者计数器以伪随机的方式递增,可以指向集合中的任何一行。

  • 最近最少使用(LRU)替换用于替换最近最少使用的缓存行或页面。

大多数ARM处理器支持轮替伪随机策略。Cortex-A15处理器还支持LRU

轮替替换策略通常更具可预测性,但在某些使用场景下可能表现不佳,因此通常更偏向于使用伪随机策略。

8.5.3 Write policy

当核心执行存储指令时,会对要写入的地址进行缓存查找。如果写操作命中缓存,有两种策略可供选择:

  • 直写策略(Write-through):在这种策略下,写操作同时写入缓存和主存,这意味着缓存和主存保持一致性。由于写入主存的次数增加,对于某些频繁更新相同内存区域的使用场景,直写策略比回写策略慢。如果是连续的大块内存写操作,且写操作可以被缓冲,那么直写可能同样高效。如果预计内存不会很快被读取(如大规模内存复制或内存初始化),那么最好不要用这些写操作占用缓存空间。

  • 回写策略(Write-back):在这种情况下,写操作仅写入缓存,而不写入主存。这意味着缓存行和主存可能包含不同的数据。缓存行中保存的是较新的数据,而主存中包含的是较旧的数据(即“过时”数据)。为了标记这些行,每个缓存行都有一个关联的脏位(dirty bit)。当写操作更新缓存但未更新主存时,脏位会被设置。如果缓存后来驱逐了脏位被设置的缓存行(脏行),该行将被写回主存。使用回写缓存策略可以显著减少对慢速外部内存的访问,从而提高性能并节省功耗。然而,如果系统中有其他代理与核心同时访问内存,则必须考虑一致性问题。

8.6 Write and Fetch buffer

写缓冲区 是核心内部的一个硬件模块(有时也存在于系统的其他部分),由多个缓冲区实现。它接收与核心写入内存操作相关的地址、数据和控制值。当核心执行存储指令时,它可能将相关的详细信息,如写入的位置、要写入的数据和事务大小,放入缓冲区。核心不需要等待写入完成到主存后再继续执行,可以立即执行下一条指令。写缓冲区本身将核心接收到的写入操作逐渐转移到内存系统中。

写缓冲区可以提高系统性能,它通过让核心不必等待存储操作完成来实现。实际上,只要写缓冲区中有空间,写缓冲区就是一种隐藏延迟的方式。如果写操作的数量较少或间隔较大,写缓冲区不会满。但如果核心生成写入操作的速度比它们传输到内存的速度快,写缓冲区最终会填满,性能提升也会随之减少。

一些写缓冲区支持写合并(也称为写组合)。它们可以将多次写入操作(例如,写入相邻字节的连续流)合并为一次突发操作。这可以减少对外部内存的写入流量,从而提高性能。

显然,当访问外设时,写缓冲区的行为有时可能不是你想要的。你可能希望核心在写入完成后再继续执行下一步,而不是立即进行。有时你可能希望一系列字节逐一写入,而不希望存储操作被合并。

类似的组件,称为取指缓冲区,可以用于读取操作。在某些系统中,特别是核心通常包含预取缓冲区,用于在指令实际插入流水线之前,从内存中预读取指令。通常,这些缓冲区对用户是透明的。我们在讨论内存排序规则时,会考虑与此相关的某些潜在风险。

8.7 Cache performance and hit rate

命中率定义为在特定时间内,缓存命中次数除以对缓存的总内存请求次数,通常以百分比计算。类似地,未命中率是缓存未命中的总次数除以对缓存的总内存请求次数。你也可以单独计算读取或写入的命中或未命中次数。显然,更高的命中率通常会带来更高的性能。对于典型软件的命中率,很难给出具体的示例数据,因为命中率高度依赖于关键代码或数据的大小和空间局部性,当然还与缓存的大小有关。

有一些简单的规则可以遵循,以提高性能。最明显的一条是启用缓存和写缓冲区,并尽可能使用它们(通常适用于包含代码的内存系统的所有部分,通常是RAM和ROM,而不是外设)。在Cortex-A系列处理器中,如果指令内存被缓存,性能会显著提升。将频繁访问的数据集中存放在内存中也有助于提升性能。例如,频繁访问的数组如果其基地址位于缓存行的起始位置可能会受益。

在内存中获取数据值涉及获取整个缓存行;如果缓存行中的其他字没有被使用,性能提升将很少甚至没有。这可以通过以“缓存友好”的方式访问数据来缓解。例如,顺序访问地址(如访问数组的一行)有利于缓存行为,而不可预测或非顺序的访问模式(如链表)则没有。

较小的代码可能比较大的代码更好地缓存,有时这会带来看似矛盾的结果。例如,一段C代码在为Thumb(或最小尺寸)编译时可能完全适合缓存,但在为ARM(或最大性能)编译时可能不适合缓存,因此反而比优化版本运行得更快。

8.8 Invalidating and cleaning cache memory

缓存的清理和无效化可能在外部内存内容发生更改时需要进行,以便移除缓存中的过时数据。这也可能在与MMU相关的活动(如更改访问权限、缓存策略或虚拟地址到物理地址的映射)后需要进行。

在描述清理和无效化操作时,常用到“flush”这个词。而ARM通常只使用“清理”(clean)和“无效化”(invalidate)这两个术语。

  • 无效化缓存或缓存行:意味着将其数据清空。这通过清除一个或多个缓存行的有效位来完成。在复位后,缓存的内容必须总是被无效化,因为其内容将不确定。如果缓存包含脏数据,通常不应该简单地无效化它。因为在可写回缓存区域中的写操作,缓存中更新的数据如果直接无效化会导致丢失。

  • 清理缓存或缓存行:意味着将脏缓存行的内容写回主存,并清除缓存行中的脏位。这使缓存行和主存保持一致。这仅适用于使用写回策略的数据缓存。缓存无效化和清理操作可以通过缓存集(set)、缓存路(way)或通过虚拟地址来执行。

将代码从一个位置复制到另一个位置(或进行其他形式的自修改代码操作)可能要求你清理和/或无效化缓存。内存复制代码将使用加载和存储指令来操作核心数据侧的存储。如果数据缓存使用写回策略,则需要在执行代码前清理将要写入的区域中的数据。这确保将指令存储的数据写回主存,并为指令提取逻辑提供有效数据。此外,如果要写入代码的区域先前用于其他程序,则指令缓存可能包含过时代码(主存重写之前的旧代码)。因此,在跳转到新代码之前,可能也需要无效化指令缓存。

清理或无效化缓存的指令是CP15操作。它们仅限于特权模式下使用,无法在用户模式下执行。在使用TrustZone安全扩展的系统中,非安全模式下的这些操作可能会受到硬件限制。

CP15指令可以清理、无效化或同时清理和无效化一级数据或指令缓存。仅在确保缓存不包含脏数据时,才可以进行不带清理的无效化操作,例如哈佛架构的指令缓存,或者当你不在意丢失之前的值且数据将被覆盖时。可以对整个缓存或单个缓存行执行这些操作。单个缓存行可以通过虚拟地址指定,或者通过指定特定集合中的某一行来执行这些操作。在硬件结构已知的情况下,也可以在L2或外部缓存上执行相同的操作。


当然,这些操作将通过内核代码访问——在Linux的GCC中,你可以使用实现于arch/arm/mm/cache-v7.S中的 __clear_cache() 函数:

void __clear_cache(char* beg, char* end);

起始地址(char* beg)是包含的,而结束地址(char* end)是不包含的。

在其他操作系统中也有类似的函数,例如,Google Android中有 cacheflush()

清理或无效化常见的场景是 DMA(直接内存访问)。当需要将核心所做的更改展示给外部内存,以便DMA控制器读取时,可能需要清理缓存。当DMA控制器写入外部内存并需要将这些更改展示给核心时,必须无效化缓存中受影响的地址。

8.9 Point of coherency and unification

对于基于集/路的清理和无效化操作,操作是在特定级别的缓存上执行的。对于使用虚拟地址的操作,架构定义了两个概念点:

  • 一致性点(Point of Coherency, PoC)
    对于特定地址,PoC 是所有可以访问内存的模块(例如,核心、DSP 或 DMA 引擎)能够保证看到相同内存位置副本的点。通常,这将是外部主系统内存。

    统一点(Point of Unification, PoU)
    对于一个核心来说,PoU 是指该核心的指令缓存和数据缓存能够保证看到相同内存位置副本的点。例如,在具有哈佛架构一级缓存和用于缓存转换表条目的TLB的系统中,统一的二级缓存将是统一点。如果不存在外部缓存,主内存将是统一点。

在 Cortex-A9 处理器中,PoC 和 PoU 实际上是同一个位置,位于 L2 接口处。

由于 Cortex-A8 处理器包含一个受 CP15 控制的 L2 缓存,因此 PoU 和 PoC 位于不同的地方,PoU 位于 L2 缓存中,PoC 位于 L2 接口之外。

不熟悉硬件转换表遍历或翻译后备缓冲区(Translation Lookaside Buffer, TLB)术语的读者可以在第9章找到相关描述。如果不存在外部缓存,主内存将是PoU。

在集群或 big.LITTLE 组合的情况下,PoU 是指集群中所有核心的指令缓存、数据缓存和转换表遍历能够保证看到相同内存位置副本的点。

了解 PoU 可以帮助自修改代码确保未来的指令获取来自代码的正确修改版本。它们可以通过两步过程来实现:

  • 清理相关的数据缓存条目(按地址)。

  • 无效化指令缓存条目(按地址)。

此外,还需要使用内存屏障。

8.9.1 Example code for cache maintenance operations

以下代码展示了一种将整个数据或统一缓存清理至一致性点的通用机制。

注意
在集群中,如果多个核心在一致性点之前共享缓存,在多个核心上运行此序列将导致这些操作在共享缓存上重复执行。


同样,你可以使用清理数据缓存条目和无效化 TLB 操作,以确保对转换表的所有写入对 MMU 可见。

8.10 Level 2 cache controller

在本章的开始,我们简要描述了内存系统的分区,并解释了许多系统如何具有多级缓存层次结构。然而,Cortex-A5 和 Cortex-A9 处理器没有集成的二级缓存。相反,系统设计师可以选择在处理器之外连接另一个缓存控制器,例如 ARM 的 L2 缓存控制器(L2C-310)。

L2C-310 缓存控制器可以支持高达 8MB 的缓存,组相联度在 4 到 16 路之间。缓存的大小和相联度由 SoC 设计师固定。二级缓存可以在多个核心之间共享,或者在核心与其他设备(如图形处理器)之间共享。可以基于每个主设备、每路的方式锁定缓存数据,从而实现对多个组件之间缓存共享的管理。

8.10.1 Level 2 cache maintenance

在缓存位于核心外部的情况下,这可以通过写入L2缓存控制器内存映射寄存器来实现;如果二级缓存是在核心内部实现的,则通过CP15完成。寄存器本身不被缓存,这使得这一操作可行。对于核心通过内存映射写操作执行这些操作时,核心必须有一种方式来确定操作何时完成。这可以通过轮询L2缓存控制器中的额外内存映射寄存器来实现。

ARM L2C-310二级缓存控制器仅对物理地址操作。因此,执行缓存维护操作时,程序可能需要执行虚拟地址到物理地址的转换。L2C-310提供了一个缓存同步操作,强制系统等待挂起的操作完成。

8.11 Parity and ECC in caches

所谓的软错误日益成为关注的焦点。更小的晶体管几何尺寸和更低的电压使电路对宇宙射线、其他背景辐射、硅封装中的α粒子或电噪声的干扰更加敏感。这对于依赖存储少量电荷且占据大量硅面积的存储设备尤其如此。在某些系统中,如果不采取适当的软错误保护措施,平均故障间隔时间可能以秒为单位衡量。

ARM架构在缓存中提供了对奇偶校验和错误校正码(ECC)的支持。奇偶校验意味着有一个额外的位,用于标记一个值为1的位数是偶数还是奇数,具体取决于所选择的方案。这提供了一个简单的单比特错误检测方法。

ECC方案能够检测多比特故障,并可能从软错误中恢复,但恢复计算可能需要多个周期。设计一个容忍一级缓存RAM访问占用多个时钟周期的核心会显著增加设计的复杂性。因此,ECC通常只用于核心外的内存块(例如二级缓存)。然而,Cortex-A15在核心内部支持ECC和奇偶校验。

奇偶校验在读写操作时进行检查,并可在标签和数据RAM上实现。奇偶校验不匹配会生成预取或数据中止异常,并且故障状态地址寄存器会相应更新。


(广告时间)

Arm架构类课程:


安全热销大课程:


安全类经典课程:


其它课程:


铂金VIP课程介绍


之最介绍

  • 招牌课程:Truszone标准版、Trustzone高配版

  • 销量前三课程:ARM三期、Secureboot、Android15安全架构

  • 持续更新的课程:ARM三期、铂金VIP

  • 非常好非常好但又被忽视的课程:CA/TA开发

  • 近期更新/力推的课程/重点课程:optee系统架构从入门到精通


说点心里话:

  • 1、不要再说课贵了,你看看咱这是啥课?别家的能比不?请不要拿通用的linux、android、python、C语言和咱这专业课比。

  • 2、咱们的VIP是数十门课程的集合。不要拿别人一门课程的价格对标咱这20门课程价格。

  • 3、这些知识很多人都会,但拿出来讲的有多少人? 愿意拿出来讲的有多少人?讲的又有多少人?

  • 4、价格都是认真计算的,并非随意定价。都是根据内容质量、核心知识点、时长和节数计算而来。从来不无缘无故涨价(涨价是需要理由的,如课程内容增加了....)。咱靠的是内容质量长期服务,而不是运营和营销(无脑涨价)。 

  • 5、如果你刷到此处,可能是老粉/铁粉,记得点赞、评论哦。感谢您的支持。



Arm精选
ARMv8/ARMv9架构、SOC架构、Trustzone/TEE安全、终端安全、SOC安全、ARM安全、ATF、OPTEE等
 最新文章