【问题标题】:How does Linux kernel flush_write_buffers() work on x86?Linux 内核 flush_write_buffers() 如何在 x86 上工作?
【发布时间】:2020-05-16 07:40:15
【问题描述】:

以下代码来自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");
}

【问题讨论】:

    标签: c assembly linux-kernel x86 dma


    【解决方案1】:

    Intel Pentium Pro 处理器存在一个错误,其中存储到 UC 类型的内存位置可能会被重新排序,而早期的内存访问类型为 WC 的位置,这违反了 x86 内存一致性模型。作为一种解决方法,可以在 UC 存储之前使用正确实现的内存序列化指令。在 Pentium Pro 处理器上,以下任何一个都可以完成这项工作:(1) cpuid,(2) UC 负载,或 (3) lock-prefixed 指令。

    Linux 内核中的flush_write_buffers 正是为此目的使用了lock 前缀指令。 cpuid 是最昂贵且不必要的。 UC 加载需要 UC 类型的内存位置,这通常有点不方便。因此,选择使用lock-prefixed 指令。

    正如函数名称所示,它的目的是等待写入缓冲区(在此上下文中也称为存储缓冲区)中的所有待处理写入变为全局可观察的。缓存不受影响。

    此错误仅影响 Pentium Pro,并且必须使用 CONFIG_X86_PPRO_FENCE 编译内核才能启用解决方法。但是,要确保在内核中应该使用它的所有地方都使用了该变通方法是很困难的。而且,CONFIG_X86_PPRO_FENCE 不仅影响flush_write_buffers 的运行,还影响其他构造,因此会导致性能显着下降。最终,从 v4.16-rc7 开始,它是来自内核的 dropped

    【讨论】:

      【解决方案2】:

      您看到的是memory fence。该指令的作用是保证所有前面的加载和存储指令对任何后续的加载或存储指令都是全局可见的。

      栅栏充当屏障,具有刷新 CPU 缓冲区(注意:缓冲区,而不是缓存,这是另一回事)的效果,因为等待写入的数据需要在继续之前立即全局可用,在以确保连续的指令将获取正确的数据。

      引入此功能是为了解决旧系列 Intel CPU(即 Pentium 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 的作用而不是锁定前缀指令,这是两个完全不同的问题,具有本质不同的答案。但除此之外,还有一些不准确的说法。最大的一个是“出于性能原因,只是选择了加锁指令而不是 mfence ......” 这一点不仅太不准确,而且不相关,因为需要flush_write_buffers 的处理器不支持mfence
      • @HadiBrais 感谢您的指点,您确实是对的,已更正。
      • 如果你要详细说明为什么lock add...,值得一提的是x += 0不会修改x,而0(%esp)是“栈顶”它很可能已经在 L1d 缓存中独占,并且不与任何其他内核共享。可能其中一个链接已经涵盖了这一点,但总结不会有什么坏处。
      • @PeterCordes 我在以前的答案版本中就有这个问题,但实际上 add 0 是一个无操作的事实是显而易见的,并没有增加太多的解释。我将添加一个括号。
      • 操作的原子性并不是真正需要的可能不正确:这可能是我们传递给另一个线程的本地地址。不太可能但可能。关键是您需要在某处选择一些内存,而0(%esp) 始终是可写的,因此选择起来很安全,而且通常也很有效。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-01-06
      • 2020-08-09
      • 2013-04-11
      • 2014-11-27
      • 1970-01-01
      • 2021-10-31
      相关资源
      最近更新 更多