如何在 Linux 中刷新地址空间区域的 CPU 缓存?
Posted
技术标签:
【中文标题】如何在 Linux 中刷新地址空间区域的 CPU 缓存?【英文标题】:How to flush the CPU cache for a region of address space in Linux? 【发布时间】:2014-05-07 05:41:43 【问题描述】:我对仅刷新地址空间区域的缓存(L1、L2 和 L3)感兴趣,例如从地址 A 到地址 B 的所有缓存条目。在 Linux 中是否有这样做的机制,无论是来自用户还是内核空间?
【问题讨论】:
你的 CPU 是什么?你想从用户空间还是从内核空间运行“flush”? 用户空间会很好,但内核空间也可以。我正在研究,所以我需要一些关于 x86 或 ARM 的信息。我想他们没有相同的机制(至少底层实现/指令不一样)。 【参考方案1】:查看此页面以获取 linux 内核中可用的刷新方法列表:https://www.kernel.org/doc/Documentation/cachetlb.txt
Linux 下的缓存和 TLB 刷新。大卫·米勒
有一组范围刷新函数
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, 无符号长开始,无符号长结束)
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 的执行时间。例如,一个非常粗略的估计可能是:(number_cache_lines_to_flush * time_to_flush_each_cache_line)。我知道这不会那么容易,但如果你能点亮一些灯,那就太好了。 aminfar,这个估计取决于确切的 cpu(它的微架构),对于任何不是 ARM 内部人员的人来说都很难。另外,我担心 x86 没有部分缓存刷新(只有 tlb 刷新,但不知道部分 tlb 刷新)。 @aminfar ,在 x86 上,您可能可以在内联汇编中使用 clflush 并遍历地址范围 @aminfar,由于 DMA 和/或 GPU 的活动,很难估计。 (个人研究)flush_tlb_range
是否像名称所宣传的那样工作,只在需要时刷新一小部分虚拟内存(而不是需要刷新整个 TLB)?与此处的其他所有内容不完全相关,但更多的是假设更高的 I 性能 Meltdown 解决方法:p【参考方案2】:
这是给 ARM 的。
GCC 提供了__builtin___clear_cache
,确实应该这样做syscall cacheflush
。但是它可能有它的caveats。
这里重要的是 Linux 提供了一个系统调用(ARM 特定)来刷新缓存。您可以查看 android/Bionic flushcache 了解如何使用此系统调用。但是我不确定 Linux 在您调用它时会提供什么样的保证,或者它是如何通过其内部工作实现的。
这篇博文Caches and Self-Modifying Code 可能会有所帮助。
【讨论】:
第一个链接说它只用于指令缓存,不确定它是否是OP需要的 @Leeor Linux 代码没有明确说明,这就是我链接它的原因。 如果你想要cacheflush
的行为,你绝对应该直接调用它。调用具有较弱行为保证的内置函数,因为它目前恰好是在您想要的更强大的函数之上实现的,这似乎是个坏主意。【参考方案3】:
有几个人对clear_cache
表达了疑虑。下面是一个手动清除缓存的过程,它效率低下,但可以从任何用户空间任务(在任何操作系统中)。
PLD/LDR
可以通过mis-使用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 循环缓存,pld
位于 0x13340 em>、0x14340、0x15340 和 0x16340 会以这种方式驱逐任何形式的值。相同的主体可以应用于驱逐 L2(通常是统一的)。遍历所有缓存大小将驱逐整个缓存。您需要分配一个缓存大小的未使用内存来驱逐整个缓存。这对于 L2 来说可能相当大。 pld
不需要使用,但需要完整的内存访问 (ldr/ldm
)。对于多个 CPU(线程缓存驱逐),您需要在每个 CPU 上运行驱逐。通常 L2 对所有 CPU 都是全局的,因此只需要运行一次。
注意:此方法仅适用于 LRU(最近最少使用)或 round-robin 缓存。对于伪随机替换,您将必须写入/读取更多数据以确保驱逐,确切的数量是高度特定于 CPU 的。 ARM 随机替换基于 8-33 位的 LFSR,具体取决于 CPU。对于某些 CPU,它默认为 round-robin,而其他 CPU 默认为 pseudo-random 模式。对于少数 CPU,Linux 内核配置将选择该模式。 ref:CPU_CACHE_ROUND_ROBIN 但是,对于较新的 CPU,Linux 将使用引导加载程序和/或芯片中的默认值。换句话说,如果您需要完全通用或者您将不得不花费大量时间来可靠地清除缓存,那么尝试让clear_cache
OS 调用工作(请参阅其他答案)是值得的。
上下文切换
可以通过在某些 ARM CPU 和特定操作系统上使用 MMU 欺骗操作系统来绕过缓存。在 *nix 系统上,您需要多个进程。您需要在进程之间切换,并且操作系统应该刷新缓存。通常,这仅适用于较旧的 ARM CPU(不支持 pld
的 CPU),其中操作系统应刷新缓存以确保进程之间不会发生信息泄漏。它不可移植,需要您对自己的操作系统有很多了解。
大多数显式缓存刷新寄存器仅限于系统模式,以防止进程之间的拒绝服务类型的攻击。一些漏洞可以尝试通过查看哪些行已被其他进程驱逐来获取信息(这可以提供有关其他进程正在访问哪些地址的信息)。这些攻击使用伪随机替换更加困难。
【讨论】:
【参考方案4】:在 x86 版本的 Linux 中,您还可以找到用于刷新缓存范围的函数 void clflush_cache_range(void *vaddr, unsigned int size)
。此函数依赖于CLFLUSH
或CLFLUSHOPT
指令。我建议检查您的处理器是否确实支持它们,因为理论上它们是可选的。
CLFLUSHOPT
是弱排序的。 CLFLUSH
最初指定为仅由 MFENCE
排序,但所有实现它的 CPU 都使用强排序 wrt。写和其他CLFLUSH
指令。 Intel 决定添加一条新指令 (CLFLUSHOPT
),而不是更改 CLFLUSH
的行为,并更新手册以保证未来的 CPU 将按照强指令执行 CLFLUSH
。对于此用途,您应该在使用任何一种后MFENCE
,以确保在您的基准(不仅仅是存储)的任何加载之前完成刷新。
实际上 x86 提供了另外一条可能有用的指令:CLWB
。 CLWB
将数据从缓存刷新到内存而不(必然)驱逐它,使其保持干净但仍被缓存。 clwb
on SKX does evict like clflushopt
, though
还要注意,这些指令是缓存一致的。它们的执行将影响系统中所有处理器(处理器内核)的所有缓存。
所有这三个指令都可以在用户模式下使用。因此,您可以使用汇编程序(或像 _mm_clflushopt
这样的内部函数)并在您的用户空间应用程序中创建自己的 void clflush_cache_range(void *vaddr, unsigned int size)
(但不要忘记在实际使用之前检查它们的可用性)。
如果我理解正确的话,在这方面对 ARM 进行推理要困难得多。 ARM 处理器系列的一致性远不如 IA-32 处理器系列。您可以拥有一个具有全功能缓存的 ARM,而另一个完全没有缓存的 ARM。此外,许多制造商可以使用定制的 MMU 和 MPU。所以最好推理一些特定的 ARM 处理器型号。
不幸的是,似乎几乎不可能对刷新某些数据所需的时间进行任何合理的估计。这个时间受太多因素影响,包括刷新的缓存行数,指令执行无序,TLB的状态(因为指令以虚拟地址为参数,而缓存使用物理地址),系统中的CPU数量,实际负载取决于系统中其他处理器上的内存操作,以及该范围内有多少行实际被处理器缓存,最后取决于 CPU、内存、内存控制器和内存总线的性能。因此,我认为执行时间在不同的环境和不同的负载下会有很大的不同。唯一合理的方法是测量系统上的刷新时间,并且负载与目标系统相似。
最后一点,不要混淆内存缓存和 TLB。它们都是缓存,但以不同的方式组织并服务于不同的目的。 TLB 仅缓存最近使用的虚拟地址和物理地址之间的转换,但不缓存该地址指向的数据。
与内存缓存相比,TLB 不是连贯的。请小心,因为刷新 TLB 条目不会导致从内存缓存中刷新适当的数据。
【讨论】:
CLFLUSH 现在被定义为强排序。 felixcloutier.com 上的英特尔手册版本以您的方式描述它(并且缺少 CLFLUSHOPT 条目),但更新版本 on hjlebbink.github.io/x86doc/ matches Intel's official PDF 说它是按其他 CLFUSH 排序的,并写道,等等,脚注是本手册的早期版本...所有实现 CLFLUSH 指令的处理器也相对于上面列举的其他操作对其进行排序。 这就是 CLFLUSHOPT 存在的原因,也是 Linux 在可用时使用它的原因。【参考方案5】:在 x86 中,你可以使用这个来刷新整个缓存层次结构
native_wbinvd()
在 arch/x86/include/asm/special_insns.h 中定义。如果你看它的实现,它只是调用了 WBINVD 指令
static inline void native_wbinvd(void)
asm volatile("wbinvd": : :"memory");
请注意,您需要处于特权模式才能执行 WBINVD X86 指令。这与 CLFLUSH x86 指令形成对比,后者清除单个缓存行并且不需要调用者处于特权模式。
如果您查看 x86 Linux 内核代码,您只会看到该指令的少数(我编写此指令时为 6 个位置)。这是因为它减慢了在该系统上运行的所有实体。想象一下在具有 100MB LLC 的服务器上运行它。该指令意味着将整个 100+ MB 从缓存移动到 RAM。此外,我注意到该指令是不可中断的。因此,它的使用可能会显着影响 RT 系统的确定性,例如
(虽然最初的问题是关于如何清除特定地址范围,但我认为清除整个缓存层次结构的信息对某些读者也很有用)
【讨论】:
更糟糕的是,wbinvd
本身不可中断,因此对中断延迟非常不利。这几乎总是错误的解决方案,除了性能实验或其他实验或玩具使用。此外,它还会刷新所有内核上的所有缓存。
伟大的点@PeterCordes w.r.t 该指令的不可中断性质。我将更新答案以反映这一点。以上是关于如何在 Linux 中刷新地址空间区域的 CPU 缓存?的主要内容,如果未能解决你的问题,请参考以下文章