C语言中的volatile变量和缓存内存

39

缓存由硬件透明地控制,因此如果我们在C程序中使用易失性变量,如何保证程序每次从指定的实际内存地址而不是缓存中读取数据?

我的理解是:

  1. 易失性关键字告诉编译器变量引用不应该被优化,并且应根据代码中编程的方式进行读取。

  2. 缓存由缓存硬件透明地控制,因此当处理器发出地址时,它不知道数据是来自缓存还是内存。

所以,如果我有一个要求必须每次读取内存地址,怎样才能确保它不是从缓存而是从需要的地址引用的?这两个概念似乎没有很好地契合。请说明如何做到这一点。

(想象一下,如果需要分析问题,我们有写回策略在缓存中)

谢谢, 微内核:)

7个回答

38
固件开发人员在此。这是嵌入式编程中的标准问题,也是许多(甚至非常有经验的)开发人员容易犯的问题。
我假设你正在尝试访问硬件寄存器,并且该寄存器的值可以随时间改变(无论是中断状态、定时器、GPIO指示等)。 volatile关键字只是解决方案的一部分,在许多情况下可能并不需要。这会导致变量每次使用时从内存中重新读取(而不是被编译器优化掉或存储在处理器寄存器中跨多个用途使用),但是否读取的“内存”是实际硬件寄存器与缓存位置无关,不受volatile关键字影响。如果您的函数只读取寄存器一次,那么您可能可以省略volatile,但通常建议大多数硬件寄存器都应定义为volatile
更大的问题是缓存和缓存一致性。最简单的方法是确保您的寄存器位于未缓存的地址空间中。这意味着每次访问寄存器时,您都可以保证读取/写入的是实际的硬件寄存器而不是缓存内存。一个更复杂但潜在性能更好的方法是使用缓存地址空间,并让您的代码手动强制缓存更新以满足特定情况。对于这两种方法,如何实现是与体系结构相关的,超出了问题的范围。它可能涉及MTRR(对于x86),MMU、页表修改等。
希望这有所帮助。如果我漏掉了什么,请告诉我,我将扩展我的答案。

volatile 的目的,在使用好的编译器时,应该是确保生成的代码在某个特定点之前让处理器知道需要写入的所有内容,并且在此之后不要求处理器读取信息。程序员还可能需要使用内部函数或其他手段来强制硬件缓存刷新,但如果编译器以硬件不知道的方式缓存寄存器,则强制硬件缓存刷新将是无用的。 - supercat

9
从你的问题中可以看出你有一个误解。
Volatile这个关键字和你所描述的缓存没有关系。当一个变量被指定为volatile时,它向编译器提供了一个提示,即该变量可能会从程序的其他部分意外地改变,因此编译器不应进行某些优化。这里的意思是编译器不应重用寄存器中已经加载的值,而应再次访问内存,因为寄存器中的值不能保证与存储在内存中的值相同。其余相关缓存内存的内容与程序员无直接关系。我的意思是CPU任何缓存存储器与RAM的同步都是一个完全不同的主题。

那么,如果我取一个变量,该变量由另一个线程或从输入设备读取的驱动程序更新,那么怎么保证我读到的是正确的值而不是缓存的值?在代码中如何避免这种情况? - Microkernel
如果您使用volatile,则保证您始终会读取来自另一个线程在内存中进行的最新更新。但我感觉您更关心操作系统层面,即缓存与内存同步。 - Cratylus
如果您使用线程,那么在不同核心上运行的线程之间,“最新”、“过去”等概念并没有明确定义。 - curiousguy

7
我的建议是通过虚拟内存管理器将页面标记为非缓存。
在Windows中,这是通过在调用VirtualProtect时设置PAGE_NOCACHE来完成的。
出于稍微不同的目的,SSE 2指令具有_mm_stream_xyz指令以防止缓存污染,尽管我认为它们在这里不适用于您的情况。
在任何一种情况下,在C语言中没有可移植的方法来实现您想要的功能;您必须使用操作系统功能。

1
所以,这取决于平台?因此,缓存不受缓存硬件控制?(如果硬件完全管理缓存,则不会检查PAGE_NOCACHE标志,对吗?) - Microkernel
1
@微内核:它由硬件管理。但操作系统告诉硬件要做什么(毕竟,硬件不知道操作系统想如何管理内存),您正在请求操作系统按照您的意愿执行操作。而所有这些信息都存储在——猜猜在哪里?——内存本身中。虽然这是一种被动过程——只有在出现问题(例如页面错误)时,操作系统才会介入。除此之外,硬件只是继续按照操作系统要求的方式进行操作,没有操作系统的干预。 - user541686
嗯,好的...看来我的理解有些错误,我一直以为CPU缓存对除了缓存硬件之外的所有人都是透明的!有什么参考资料可以让我弄清楚我的概念吗?非常感谢澄清 :) - Microkernel
4
@Microkernel:好的! :) 基本上,操作系统会在内存中的“页表”中存储所有内存管理信息,并告诉CPU去哪里查找这些信息。然后CPU负责管理一切,并在无法决定该怎么做时向操作系统寻求“帮助”。你可以在这里阅读关于分页的内容(http://wiki.osdev.org/Paging),关于缓存的内容可以在这里找到(http://wiki.osdev.org/CPU_Caches);如果您仍有任何问题,请告诉我。(这就是为什么他们说操作系统处于硬件和软件之间的原因--它确实是这样!) - user541686

2

维基百科有一篇关于MTRR(内存类型范围寄存器)的很好的文章,它适用于x86系列的CPU。

简单来说,从Pentium Pro Intel开始(AMD也复制了),就有了这些MTR寄存器,可以在内存范围上设置未缓存、写通、写组合、写保护或写回属性。

从Pentium III开始,但据我所知,只有64位处理器才真正有用,它们会尊重MTRR,但可以被页面属性表覆盖,让CPU为每个内存页面设置内存类型。

我所知道的MTRR的一个主要用途是图形RAM。将其标记为写组合更有效率。这使得缓存可以存储写入,并放松所有内存写入顺序规则,以允许向图形卡进行非常高速的突发写入。

但对于您的目的,您需要使用未缓存或写通的MTRR或PAT设置。


0

正如你所说,缓存对程序员是透明的。系统保证如果你通过地址访问对象,你总是看到最后一次写入的值。唯一可能发生的事情是,如果你的缓存中存在过时的值,你可能会遭受运行时的惩罚。


4
只有当机器只有一个CPU时,才会出现这种情况。 - JeremyP
1
@JeremyP,我认为这里的问题已经超出了并发访问共享内存的范围。如果你有额外的需求,那么一切都会变得更加复杂。你需要应用适当的工具来确保数据的一致性。但是,这是一个更加普遍的问题,从缓存的角度来看可能不是正确的视角。 - Jens Gustedt
2
我认为并发访问内存并不超出范围。问题的前提是存在对内存的并发访问,否则,正如你所指出的那样,缓存是透明的。 - JeremyP
机器不需要有多个CPU。内存映射设备控制寄存器可以产生相同的效果(对于硬MCU,设计师可能会注意不要缓存该地址空间,对于FPGA / PLD上的软核心,不一定)。请参见https://www.altera.com/ja_JP/pdfs/literature/hb/nios2/n2sw_nii52007.pdf第4页。 - Dmitri
@JeremyP "只有当机器只有一个CPU时" 这并不总是错误的,但是极其误导。应该这样写:只有当机器没有多个处理单元且这些单元不支持线程时才是如此。如果CPU被设计为支持线程,则可以保证。 - curiousguy

0

volatile 确保每次需要读取数据时都会从 CPU 和内存之间的缓存中获取,但如果您需要从内存中读取实际数据而不是缓存数据,则有两个选项:

  • 制作一个不缓存所需数据的板子。如果您访问某些 I/O 设备,则可能已经是这种情况了。
  • 使用特定的 CPU 指令来绕过缓存。当您需要擦除内存以激活可能的 SEU 错误时,就会使用此选项。

第二个选项的详细信息取决于操作系统和/或 CPU。


7
我不同意这篇帖子。 volatile 关键字只是防止 C 编译器对变量进行某些优化。它并不会影响缓存。一些编译器可能会让你篡改该关键字的含义(例如 ARC 编译器),但对于大多数编译器来说,情况并非如此。 - Jimbo

0

使用 _Uncached 关键字可能有助于嵌入式操作系统,如 MQX。

#define MEM_READ(addr)       (*((volatile _Uncached unsigned int *)(addr)))
#define MEM_WRITE(addr,data) (*((volatile _Uncached unsigned int *)(addr)) = data)

代码按钮是有原因的。请不要滥用格式。 - A--C
3
哪个编译器支持"_Uncached"关键字?在谷歌搜索"_Uncached",第一个结果即为答案。 - Manuel Jacob
@ManuelJacob GCC现在针对ARC架构具有__attribute__((uncached))。请参阅https://gcc.gnu.org/onlinedocs/gcc-13.2.0/gcc/ARC-Type-Attributes.html(我知道我是在回复旧帖)。 - Anonymous Guy

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