【问题标题】:C volatile, and issues with hardware cachingC volatile,以及硬件缓存问题
【发布时间】:2016-01-26 05:38:21
【问题描述】:

我在本网站和其他地方阅读过类似的答案,但在某些情况下仍然感到困惑。

我知道标准实际上向我们保证了什么,我了解关键字的预期用途,并且我很清楚编译器缓存和 L1/L2/ect 之间的区别。缓存;出于好奇,我了解其他情况。

假设我在 C 中声明了一个变量 volatile。四种情况:

  1. 信号处理程序,单线程(按预期):这是关键字旨在解决的问题。我的进程从操作系统获得信号回调,并且我在进程的正常执行之外修改了一些 volatile 变量。由于它被声明为volatile,因此正常进程不会将此值存储在 CPU 寄存器中,而是始终从内存中加载。即使信号处理程序写入volatile 变量,因为信号处理程序与正常进程共享相同的地址空间,即使volatile 变量之前缓存在硬件中(即L1, L2),我们保证主进程将加载正确的、更新的变量。太好了,大家都很开心。
  2. DMA 传输,单线程:假设volatile 变量映射到正在进行 DMA 写入的内存区域。和以前一样,编译器不会将volatile 变量保存在CPU 寄存器中,而是总是从内存中加载;但是,如果该变量存在于 hardware 缓存中,则加载请求将永远不会到达主内存。如果 DMA 控制器在我们背后更新 MM,我们将永远无法获得最新值。在抢占式操作系统中,最终我们可能会被上下文切换出来,并且下一次我们的进程恢复时,缓存将是冷的,我们实际上必须从主内存重新加载。我们将获得正确的功能......最终(我们自己的进程也可能会交换该缓存线 - 但同样,我们可能会在此之前浪费宝贵的周期)。当通过 DMA 控制器更新主内存时,是否有标准化的硬件支持或操作系统支持通知硬件缓存?或者我们是否必须显式刷新缓存以确保我们不会读取错误值? (这在列出的架构中是否可行?)
  3. 内存映射寄存器,单线程: 与 #2 相同,除了 volatile 变量映射到内存映射寄存器(或显式 IO 端口)。我想这是一个比 #3 更困难的问题,因为至少 DMA 控制器会在传输完成时向 CPU 发出信号,这让操作系统或硬件有机会做某事。
  4. Mutilthreaded:如果我有一个volatile 变量,是否可以保证在不同物理内核上运行的多个线程之间的缓存一致性?可以肯定的是,编译器仍在从内存中发出加载指令,但是如果该值缓存在一个内核的缓存中,是否可以保证相同的值必须存在于另一个内核的缓存中? (我想这对于同一物理核心上不同逻辑核心上的超线程线程来说根本不是问题,因为它们共享物理缓存)。我压倒性的直觉说不,但我想我还是会在这里列出这个案例。

如果可能,区分 x64 和 ARMv6/7/8 架构,以及内核与用户空间解决方案。

【问题讨论】:

  • c/c++ volatile 与硬件无关。它只是为编译器标记变量。编译器不会重新排序对标记变量的访问,也不会优化对它们的访问。
  • @MichaelNastenko 我想清楚了我知道。也许我应该清楚我正在寻找是否有可用的操作系统/架构支持可以在 C 标准范围之外被编译器利用
  • volatile 表示“不重新排序,不优化”。不多也不少。您提到的#1-#3 场景由硬件解决。 #4 通常由 MESI 或任何其他缓存一致性协议解决,c/c++ volatile 在这种情况下是无用的。

标签: c multithreading caching


【解决方案1】:

对于 2 和 3,没有标准化的工作方式。

通常在进行 DMA 传输时,会根据平台的方式刷新缓存。通常有非常直接的指令可以做到这一点(因为现在缓存已集成在 CPU 中)。

另一方面,当访问内存映射寄存器时,行为通常取决于写入的顺序。例如,假设您有一个 UART 端口并向其写入字符——您需要确保每次从 C 写入该端口时都有实际写入。

虽然它可能会在每次写入之间刷新缓存,但这不是通常所做的。正常的方法(至少对于 ARM)是设置 MMU,以便对地址空间的某些区域的写入以正确的顺序进行非缓存。

这种方法也可用于用于 DMA 传输的内存;例如,可以设置专用区域用作 DMA 缓冲区并设置 MMU,以便对该区域的读取和写入不会发生缓存。

另一方面,该语言保证所有内存(以及您从声明变量或使用new 分配内存中获得的内容)将以特定方式运行。它是多线程还是涉及信号应该没有区别。请注意,C90 和 C99 标准没有提到线程(C11 有),但它们应该以这种方式工作。实现必须确保以与此一致的方式使用 CPU 和缓存(因此,如果无法实现,操作系统可能无法在不同的内核上调度不同的线程)。因此,您不需要刷新缓存以在线程之间共享数据,但您确实需要同步线程,当然还需要使用volatile 限定数据。信号处理程序也是如此,即使实现碰巧将它们安排在不同的内核上。

【讨论】:

  • 感谢您的回答 - 特别是关于缓存刷新和 MMU 映射的不可兑现 DMA 区域。只是为了清楚最后一段,我不相信任何 C 标准可以保证从写入到volatile 内存的全局可见性。我认为我们在 x86 上运气不错,因为它同步缓存的方式。
  • @jammie 是的,C 标准似乎没有涵盖线程。然而,应该可以假设线程的任何实现都以这种方式运行(有人可能会认为这在线程的概念内)。如果平台具有单独的内核和单独的缓存,则在设计线程功能时应考虑到这一点 - 否则它可能会变得非常不可用。
  • C11 有 <threads.h>,它定义了标准 C 线程。并非所有 C11 的实现都支持 <threads.h> 并且它不是强制性标头(因此线程支持在 C11 中不是强制性的——但编译器应该通过定义 __STDC_NO_THREADS__ 来说明这一点,但 GCC 5.1.0 没有定义即使在 Mac OS X 上不支持<threads.h>)。
  • @JonathanLeffler 出于某种奇怪的原因,我正在阅读 C99 标准。但是,我仍然认为,在(线程的)同步发生之后,任何名副其实的线程实现都应该具有 volatile 变量的全局可见性。
  • 我没有资格讨论线程下 volatile 变量同步的细节。我所提请注意的是您的评论“(请注意,C 标准没有提及线程,……”,我将其编辑为“请注意,C90 和 C99 标准没有提及线程(C11 有),……”。我还注意到,并非每个支持 C11 的 C 编译器通常都支持 C11 线程(支持线程是可选的),并给出了一个不支持它们的具体示例(但是 - 错误地? - 没有声明它不支持支持他们)。如果您需要编辑更多,请成为我的客人。
猜你喜欢
  • 1970-01-01
  • 2011-11-19
  • 2011-12-13
  • 2017-11-25
  • 2017-08-09
  • 2011-07-29
  • 1970-01-01
相关资源
最近更新 更多