在Linux中如何清空特定内存区域的CPU缓存?

19

我对清除缓存(L1、L2 和 L3)只针对地址空间的某个区域感兴趣,例如从地址 A 到地址 B 的所有缓存条目。在 Linux 中,是否有一种机制可以从用户或内核空间进行此操作?


2
你的 CPU 是什么型号?你是想从用户空间还是内核空间运行“flush”命令? - osgx
2
用户空间最好,但内核空间也可以。 我正在进行一项研究,所以我需要关于x86或ARM的一些信息。我想它们没有相同的机制(至少底层实现/指令不会相同)。 - aminfar
5个回答

10

请查看此页面,以获取Linux内核中可用清除方法的列表:https://www.kernel.org/doc/Documentation/cachetlb.txt

在Linux下进行缓存和TLB清除。David S. Miller

这里有一组范围清除函数。

2) flush_cache_range(vma, start, end);
   change_range_of_page_tables(mm, start, end);
   flush_tlb_range(vma, start, end);

3) void flush_cache_range(struct vm_area_struct *vma, unsigned long start, unsigned long end)

Here we are flushing a specific range of (user) virtual
addresses from the cache.  After running, there will be no
entries in the cache for 'vma->vm_mm' for virtual addresses in
the range 'start' to 'end-1'.

您还可以检查函数的实现 - http://lxr.free-electrons.com/ident?a=sh;i=flush_cache_range

例如,在ARM中 - http://lxr.free-electrons.com/source/arch/arm/mm/flush.c?a=sh&v=3.13#L67

 67 void flush_cache_range(struct vm_area_struct *vma, unsigned long start, unsigned long end)
 68 {
 69         if (cache_is_vivt()) {
 70                 vivt_flush_cache_range(vma, start, end);
 71                 return;
 72         }
 73 
 74         if (cache_is_vipt_aliasing()) {
 75                 asm(    "mcr    p15, 0, %0, c7, c14, 0\n"
 76                 "       mcr     p15, 0, %0, c7, c10, 4"
 77                     :
 78                     : "r" (0)
 79                     : "cc");
 80         }
 81 
 82         if (vma->vm_flags & VM_EXEC)
 83                 __flush_icache_all();
 84 }

非常棒的信息,我很感激。 我在想如何估计flush_cache_range的执行时间,而不需要在实际硬件上运行它。例如,一个非常粗略的估计可以是:(要刷新的缓存行数*每个缓存行刷新所需的时间)。我知道这并不容易,但如果您能提供一些指导,那就太好了。 - aminfar
aminfar,这个估计取决于确切的CPU(其微架构),对于任何不是ARM内部人员的人来说都很困难。此外,我担心x86没有部分缓存刷新(只有TLB刷新,但不知道部分TLB刷新)。 - osgx
3
@aminfar,在x86上,您可能可以使用clflush在内联汇编中,并循环遍历地址范围。 - Leeor
(同时会带来稍微更高的内存成本,因为页面表的某些部分需要在核心之间进行复制) - Paul Stelian
@osgx 我不确定这是否允许您在不影响其他进程用户空间性能的情况下从根本上不缓存内核内存。 - Paul Stelian
显示剩余5条评论

8

这是针对ARM的。

GCC提供__builtin___clear_cache,它应该执行syscall cacheflush。但是它可能有注意事项

重要的是Linux提供了一个系统调用(特定于ARM)来刷新缓存。您可以查看Android/Bionic flushcache 以了解如何使用此系统调用。但我不确定在调用时Linux会给出什么样的保证或者它是如何通过内部工作实现的。

这篇博客文章缓存和自修改代码可能会有所帮助。


1
第一个链接说它只适用于指令缓存,不确定这是否是 OP 所需的。 - Leeor
@Leeor Linux 代码并没有明确说明这一点,这就是为什么我提供了链接的原因。 - auselen
1
如果你想要 cacheflush 的行为,你应该直接调用它。调用一个具有较弱行为保证的内置函数,因为它目前恰好是在你想要的更强函数之上实现的,似乎不是一个好主意。 - Peter Cordes

4
在x86版本的Linux中,您还可以找到一个名为void clflush_cache_range(void *vaddr, unsigned int size)的函数,该函数用于清除缓存范围。此函数依赖于CLFLUSHCLFLUSHOPT指令。建议检查处理器是否实际支持它们,因为理论上它们是可选的。 CLFLUSHOPT是弱排序的。 CLFLUSH最初被指定为仅由MFENCE排序,但是所有实现它的CPU都具有与写入和其他CLFLUSH指令强排序相关的功能。英特尔决定添加一个新指令(CLFLUSHOPT),而不是更改CLFLUSH的行为,并更新手册以保证未来的CPU将强制执行CLFLUSH。出于这个目的,在使用任何一种指令之后,应该MFENCE,以确保在负载从基准测试中加载之前完成刷新(不仅是存储)。
实际上,x86提供了另一条可能有用的指令:CLWBCLWB将数据从缓存刷新到内存而不会(必须)使其失效,使其保持清洁但仍然被缓存。clwb在SKX上像clflushopt一样使其失效,但是 还要注意,这些指令是高速缓存一致的。它们的执行将影响系统中所有处理器(处理器核心)的所有高速缓存。
所有这三个指令都可以在用户模式下使用。因此,您可以使用汇编器(或类似_mm_clflushopt的内部函数)并在您的用户空间应用程序中创建自己的void clflush_cache_range(void *vaddr, unsigned int size)(但在实际使用之前不要忘记检查它们的可用性)。
如果我理解正确,在这方面理解ARM要困难得多。 ARM处理器系列比IA-32处理器系列不一致得多。您可以拥有一个具有完整功能的缓存的ARM处理器,另一个则完全没有缓存。此外,许多制造商可以使用定制的MMU和MPU。因此,最好考虑某个特定的ARM处理器型号。
很不幸,似乎几乎不可能对清除某些数据所需的时间进行合理估计。这个时间受太多因素影响,包括刷新缓存行的数量、指令无序执行、TLB的状态(因为指令使用虚拟地址作为参数,但缓存使用物理地址)、系统中的CPU数量、内存操作的实际负载以及处理器实际缓存了多少范围内的行等等,最后还受CPU、内存、内存控制器和内存总线的性能影响。因此,我认为在不同环境和不同负载下,执行时间会有显着差异。唯一合理的方法是在目标系统上测量刷新时间并采用类似的负载。


最后需要注意的是,不要混淆内存缓存和TLB。它们都是缓存,但组织方式和服务的目的不同。TLB缓存最近使用的虚拟和物理地址之间的转换,但不缓存由这些地址指向的数据。

而且,与内存缓存不同,TLB不是一致的。要小心,因为刷新TLB条目不会导致从内存缓存中刷新相应的数据。


CLFLUSH现在被定义为强顺序。felixcloutier.com上的英特尔手册版本描述了您所描述的方式(并且缺少CLFLUSHOPT的条目),但更新版本hjlebbink.github.io/x86doc/匹配英特尔官方PDF,表示它与其他CLFUSHes有序,以及写入等,并带有脚注此手册的早期版本...所有实现CLFLUSH指令的处理器也会相对于上述其他操作进行排序。 - Peter Cordes
这就是为什么CLFLUSHOPT存在,以及Linux在可用时使用它的原因。 - Peter Cordes

3

有几个人对clear_cache表示担忧。下面是一种手动清除缓存的方法,虽然效率不高,但在任何用户空间任务(在任何操作系统中)中都可以实现。


PLD/LDR

可以通过错误地使用pld指令来清除缓存。 pld将获取一个缓存行。为了清除特定的内存地址,您需要了解缓存的结构。例如,cortex-a9具有4路数据缓存,每行8个字。缓存大小可配置为16KB、32KB或64KB。因此,它有512、1024或2048个行。路始终不重要于较低的地址位(因此顺序地址不会冲突)。因此,您将通过访问memory offset + cache size / ways来填充新的路径。对于cortex-a9,这是每4KB、8KB和16KB。

在“C”或“C ++”中使用ldr很简单。您只需要适当地调整数组大小并访问即可。

请参见:Programmatically get the cache line size?

例如,如果要清除0x12345,则行从0x12340开始,并且对于16KB循环缓存,对0x133400x143400x153400x16340pld将清除该路径中的任何值。相同的原理可以应用于清除L2(通常是统一的)。迭代所有缓存大小将清除整个缓存。您需要分配一个未使用的内存来清除整个缓存。对于L2而言,这可能非常大。pld不需要使用,但需要完全访问内存(ldr/ldm)。对于多个CPU(线程化缓存驱逐),您需要在每个CPU上运行驱逐。通常,L2对所有CPU都是全局的,因此只需运行一次即可。

NB: 此方法仅适用于LRU(最近最少使用)或轮询缓存。 对于伪随机替换,您将需要写入/读取更多数据以确保清除,具体数量高度与CPU相关。 ARM随机替换基于一个LFSR,其位数取决于CPU,范围在8-33位之间。 对于一些CPU,默认为轮询模式,而对于其他CPU,则默认为伪随机模式。 对于一些CPU,Linux内核配置将选择模式。ref: CPU_CACHE_ROUND_ROBIN 然而,对于更新的CPU,Linux将使用启动加载程序和/或硅片的默认值。 换句话说,如果您需要完全通用,或者您必须花费大量时间可靠地清除缓存,则值得尝试并使clear_cache操作系统调用起作用(请参见其他答案)。

上下文切换

在某些ARM CPU和特定操作系统上,可以通过欺骗使用MMU的操作系统来绕过缓存。在*nix系统上,您需要多个进程。您需要在进程之间切换,并且操作系统应刷新缓存。通常,这只适用于旧的ARM CPU(不支持pld),操作系统应刷新缓存以确保进程之间没有信息泄漏。它不可移植,并且需要您对操作系统有很多了解。

大多数显式缓存清除寄存器都受限于系统模式,以防止进程之间的拒绝服务类型攻击。一些漏洞利用可以尝试通过查看其他进程已经清除的行来获得信息(这可以提供关于其他进程正在访问的地址的信息)。使用伪随机替换会使这些攻击更加困难。


1
在x86架构中,如果要刷新整个缓存层次结构,可以使用以下方法。
native_wbinvd()

这在 arch/x86/include/asm/special_insns.h 中有定义。如果你查看它的实现,它只是调用了 WBINVD 指令。

static inline void native_wbinvd(void)
{
        asm volatile("wbinvd": : :"memory");
}

请注意,执行X86指令WBINVD需要处于特权模式。这与清除单个高速缓存行的X86指令CLFLUSH不同,后者不需要调用方处于特权模式。
如果您查看x86 Linux内核代码,您只会看到少数几个(在我撰写本文时只有6个)此指令的位置。这是因为它会减慢运行在该系统上的所有实体。想象一下在具有100MB LLC的服务器上运行此操作。这条指令将意味着将整个100+ MB从缓存移动到RAM中。此外,我注意到此指令无法中断。因此,它的使用可能会显著影响实时系统的确定性。
(虽然原始问题询问如何清除特定地址范围,但我认为有关清除整个缓存层次结构的信息对某些读者也会有用)

更糟糕的是,wbinvd本身不可中断,因此它对中断延迟非常不利。这几乎总是错误的解决方案,除了性能实验或其他实验性或玩具用途。此外,它会刷新所有核心上的所有缓存。 - Peter Cordes
非常好的观点@PeterCordes,关于这个指令不可中断的性质。我会更新答案以反映这一点。 - jithu83

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接