Linux内核中的flush_write_buffers()在x86上是如何工作的?

5
以下代码来自include/asm-i386/io.h,并且被从dma_map_single()调用。我的理解是flush_write_buffers()在DMA映射内存之前会清空CPU内存缓存。但是这个汇编代码是如何清空CPU缓存的呢?
static inline void flush_write_buffers(void)
{
    __asm__ __volatile__ ("lock; addl $0,0(%%esp)": : :"memory");
}
2个回答

6
英特尔奔腾Pro处理器存在一个漏洞,其中对UC类型的存储到内存位置可能会与先前访问WC类型位置的内存访问重新排序,这违反了x86内存一致性模型。解决方法是,在UC存储之前使用正确实现的内存序列化指令。在奔腾Pro处理器上,以下任何一个都可以完成工作:(1)cpuid,(2)UC加载或(3)lock前缀指令。
Linux内核中的flush_write_buffers正是出于这个目的使用了lock前缀指令。cpuid是最昂贵和不必要的。 UC加载需要UC类型的内存位置,这通常有点不方便。因此,选择使用lock前缀指令。
正如函数名称所示,它的目的是等待写缓冲区(在此上下文中称为存储缓冲区)中的所有挂起写操作变得全局可观察。缓存不受影响。
这个错误只影响 Pentium Pro 处理器,内核必须编译使用 CONFIG_X86_PPRO_FENCE 才能启用解决方法。然而,很难确保解决方法在内核的所有应该使用的地方都得到了使用。此外,CONFIG_X86_PPRO_FENCE 不仅影响 flush_write_buffers 的操作,还影响其他结构,因此可能会导致显著的性能下降。最终,自内核 v4.16-rc7 开始,它被 删除

5
你看到的是一个内存栅栏。该指令的作用是确保所有前置的加载和存储指令对任何后续的加载或存储指令都变得全局可见。
栅栏就像一道屏障,其效果是清空CPU缓冲区(注意:缓冲区,不是缓存,那是另一回事),因为等待写入的数据需要立即变得全局可用,以确保后续指令将获取正确的数据。
这个函数是为了解决旧版英特尔CPU家族中的硬件问题而引入的,即奔腾Pro(1995-98),在特定情况下会导致内存访问操作按错误顺序执行。
现在,在x86中应用栅栏的规范方法是通过使用mfencelfencesfence指令(取决于所需的栅栏类型),但这些指令只是后来添加的(使用SSE和SSE2)。在Pentium Pro上,没有这样的指令可用。 lock指令实际上只是一个指令前缀,因此这个:
lock
addl $0,0(%esp)

实际上是一个“锁定的add”。

lock前缀用于执行读取-修改-写入操作的操作码,使它们具有原子性。应用lock add $0, 0(%esp)时,为了使指令具有原子性并且结果能够立即全局可见,隐式地应用了加载+存储栅栏。栈顶始终可读可写,添加0是无操作,因此无需向函数传递有效地址。因此,此解决方法允许正确序列化内存访问,并且是在Intel Pentium Pro上实现目标的最快类型的指令。


还可以查看以下其他帖子:


我认为问题实际上是关于函数flush_write_buffers的作用,而不是带锁前缀的指令,这两个问题本质上有着不同的答案。但除此之外,还有几个不准确的陈述。最大的问题是“选择锁加指令只是出于性能原因...” 不仅这一点太不准确了,而且也不相关,因为在需要flush_write_buffers的处理器上不支持mfence - Hadi Brais
@HadiBrais 感谢您的指正,您说得对,已经更正。 - Marco Bonelli
如果你要详细解释为什么lock add...,那么值得一提的是,x += 0不会修改x,而0(%esp)是“栈顶”,很可能已经在L1d缓存中独占所有权,并且不与任何其他核心共享。也许其中一个链接已经涵盖了这一点,但总结一下也无妨。 - Peter Cordes
@PeterCordes 我在之前的回答版本中有提到过,但实际上加0是一个无操作,这一点很明显,并没有为解释增加太多内容。我会加上括号。 - Marco Bonelli
1
操作的原子性并不是真正需要的,这可能不是真的:这可能是我们传递给另一个线程的本地地址。虽然不太可能,但有可能。关键是你想要在某个地方选择一些内存,而0(%esp)始终是可写的,因此选择它是安全的,通常也很有效。 - Peter Cordes

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