内存映射文件和指向易失对象的指针

10
我对C和C++中volatile的语义理解是,它将内存访问转换为(observable)副作用。每当读取或写入内存映射文件(或共享内存)时,我希望指针被标记为volatile限定符,以表明这实际上是I/O操作。(John Regehr在volatile的语义方面写了一篇非常好的文章)。
此外,我认为使用像memcpy()这样的函数来访问共享内存是不正确的,因为函数签名表明volatile限定符被强制转换掉了,内存访问没有被视为I/O。
在我看来,这是支持使用std::copy()的论据,其中volatile限定符不会被强制转换掉,内存访问会被正确地视为I/O。
然而,我使用指向易失对象的指针和 std::copy() 访问内存映射文件的经验表明,它比仅使用 memcpy() 慢几个数量级。我倾向于得出结论,也许clang和GCC在处理volatile时过于保守。是否是这种情况?
在访问共享内存方面,有哪些关于 volatile 的指导方针,如果我想遵循标准并使其支持我所依赖的语义?

标准中相关引用 [intro.execution] §14:

读取由volatile glvalue指定的对象、修改对象、调用库I/O函数或调用执行这些操作的函数都是副作用,即更改执行环境状态的变化。一般情况下,表达式(或子表达式)的求值包括值计算(包括为glvalue评估确定对象的标识和为prvalue评估获取先前分配给对象的值)和启动副作用。当库I/O函数的调用返回或评估通过volatile glvalue的访问时,副作用被视为完成,即使一些由调用隐含的外部操作(例如I/O本身)或由volatile访问可能尚未完成。


在处理volatile时,您可能需要在memcpy之前和之后添加内存屏障。 - knivil
请澄清一下,您是指“volatile指针”还是“指向易变数据的非volatile指针”? - Richard Critten
1
你同时标记了C和C ++,但是谈到“标准”。 - Antti Haapala -- Слава Україні
1
@Arvid:你混淆了线程、进程、内存访问和信念。 - knivil
1
@knivil 我只想通过 mmap 映射的内存访问磁盘上的文件。就这样。我不认为“内存屏障”是 C++ 的概念(有 launder 和 std::experimental::barrier)。我标记了“语言律师”,以表明我主要关心的不是一些流行架构上的一些现代编译器如何生成代码,而是如何以正确捕获和执行 C++(或 C)抽象机器中的意图。对于通过 mmap() 读写文件的目的,多次加载或存储并不是真正的问题(给定单个 std::copy)。 - Arvid
显示剩余7条评论
3个回答

4
我理解C和C++中volatile的语义是将内存访问转换为I/O。但实际上,volatile只是将程序员对编译器的某个内存区域可以随时被“其他东西”更改的通知传达给编译器。这里的“其他东西”可能是很多不同的事情,例如:内存映射的硬件寄存器、与ISR共享的变量、从回调函数更新的变量、与另一个线程或进程共享的变量以及通过DMA更新的内存区域等。由于标准(5.1.2.3)保证了对volatile对象的访问(读/写)不会被优化掉,因此volatile也可以用于阻止某些编译器优化,这在硬件相关的编程中非常有用。
无论何时读取或写入内存映射文件(或共享内存),我都希望指针被声明为volatile限定符。但实际上,数据的性质并不重要,只要它被更新就行。
使用像memcpy()这样的函数来访问共享内存是否正确,整体而言,这取决于您对“共享内存”的定义。问题在于,您一直在谈论“共享内存”,这不是一个正式的、明确定义的术语。与另一个ISR/thread/process共享的内存?如果内存与另一个ISR/thread/process共享,那么根据编译器的不同,可能必须将其声明为volatile。但这仅是因为volatile可以防止编译器做出错误的假设并优化访问此类“共享”变量的代码。这种情况在旧的嵌入式系统编译器上特别容易发生。在现代托管系统编译器上不需要这样做。volatile不会导致内存屏障行为,也不会(必然)强制执行表达式以特定顺序执行。volatile当然不保证任何形式的原子性。这就是为什么C语言添加了_Atomic类型限定符的原因。
因此,回到复制问题,如果内存区域在几个ISR/thread/process之间“共享”,则volatile根本没有帮助。相反,您需要一些同步手段,例如互斥锁、信号量或临界区。
std::copy()不会保留volatile限定符,也不会正确地将内存访问视为I/O。因此,这种想法是错误的。
如果我想遵循标准的字面意思,并依赖它支持我所依赖的语义来访问共享内存,有哪些指导方针?

使用系统特定的API来保护内存访问,通过互斥/信号量/临界区。


你说“不,它不会这样做”,接着描述了各种I/O(即程序的副作用)。访问不允许被优化掉是由于它是一个副作用。我的问题与同步无关。 - Arvid
也许我的mmap()模型是不正确的,但从程序的角度来看,它看起来像是可以在程序本身不写入的情况下改变的内存,对吧? - Arvid
@Arvid 在计算机中,“I/O”一词具有非常特定的含义,即进出计算机的事物。在我的例子中,只有硬件寄存器和DMA可以成为I/O的形式。虽然副作用在C标准中的正式定义只是“执行环境的变化”,但这是一个非常模糊的术语。 - Lundin
如果我在没有使用volatile限定符的情况下将值复制到内存映射范围内,有什么防止编译器将其优化为死代码的措施吗?一个答案可能是该内存被返回给munmap(),但这意味着对基础文件对象的更新不会反映在映射中,而实际上它确实会反映。 - Arvid
我明白了。我认识到我使用了I/O这个术语,但我的意思是可观察的副作用,基本上就是C++承诺要做的事情。 - Arvid
我认为"I/O"这个术语非常贴切。你提到的所有例子都是程序与“外部世界”即抽象机器之外进行通信的方式。就像printf()一样,它们必须被视为执行程序时可观察到的副作用。你说数据的“本质”并不重要,只有它如何被更新。从程序的角度来看,内存映射文件中的内存确实是按照你所描述的方式在程序背后被更新的。 - Arvid

3
我认为你在过度思考。我不认为需要将mmap或等效函数(这里我会使用POSIX术语)中的内存设置为易失性。
从编译器的角度来看,mmap返回一个被修改后传递给msyncmunmap或隐含在_Exit期间取消映射的对象。这些函数需要视为I/O处理,而不是其他任何内容。
您可以将mmap基本上替换为malloc+read,并将munmap替换为write+free,然后您将获得大多数关于何时以及如何进行I/O的保证。
请注意,这甚至不需要将数据反馈给munmap,只是以这种方式更容易演示它。您可以让mmap返回一块内存,并在列表中内部保存它,然后有一个不带任何参数的函数(我们称之为msyncall),它写出所有之前调用mmap返回的内存。然后我们可以从那里开始构建,说执行I/O的任何函数都具有隐含的msyncall。虽然我们不需要走那么远。从编译器libc的角度来看,libc是一个黑盒子,在其中某些函数返回了一些内存,这些内存在调用libc之前必须同步,因为编译器无法知道从libc先前返回的哪些内存位仍被引用并在活动使用中。
上面的段落是实际操作中的工作原理,但我们如何从标准的角度来处理它呢? 首先让我们看一个类似的问题。对于线程而言,共享内存只在某些非常特定的函数调用处进行同步。这非常重要,因为现代CPU会重新排序读写,而内存屏障很昂贵,旧的CPU可能需要在其他线程、进程或I/O可见之前显式缓存刷新已写入的数据。 mmap的规范说明:
应用程序在使用mmap()与任何其他文件访问方法结合使用时必须确保正确的同步
但它没有指定如何进行同步。我知道在实践中,同步基本上必须是msync,因为仍然有一些系统不使用相同的页面缓存作为mmap的读/写。

有趣。从程序的角度来看,“mmap()”和“munmap()”执行I/O操作,由于“mmap”返回的内存被反馈到“munmap”,因此存在数据依赖性以进行“写”操作。然而,内存映射文件也可以实时更新,这是一种期望的属性。在这种情况下,内存访问本身就是副作用。当另一个进程更新文件内容时,是吗? - Arvid
@Arvid 更新了答案,因为我无法在字符限制内回答。 - Art

0
我的理解是,C和C++中volatile的语义是将内存访问转换为I/O。
您的理解是错误的。Volatile对象是副作用易变的 - 它的值可能会被编译器在编译期间看不到的某些东西更改。
因此,volatile对象必须具有永久(当然是在其范围内)的内存存储位置,在任何使用之前都必须从中读取,并在每次更改后保存。
请参见示例: https://godbolt.org/g/1vm1pq 顺便说一句,我认为那篇文章是垃圾 - 它假设程序员认为volatile意味着原子性和一致性,这不是真实情况。那篇文章应该有一个标题 - “为什么我的volatile理解是错误的,以及为什么我仍然生活在神话世界中”。

3
内存访问与输入/输出不同,二者没有共同之处。输入/输出寄存器可能在内存中有一个地址映射,因此可以通过内存写入和读取来访问它。但通常情况下,输入/输出并不等同于内存。 - 0___________
1
将'volatile'误解为类似于'atomic'或'synchronized'的东西是很普遍的。它只是“请不要在寄存器中进行优化”。 - Jacek Cz
1
@PeterJ 我不知道那篇文章的哪一部分让你觉得他建议使用volatile来进行线程同步。第8点是专门为打击这种误解而设立的。 - Arvid
3
因对链接文章的不公正和误导性批评而被投票下降。许多人仍然相信volatile可以用于线程同步。文章的作者说这是无效的,因此他的陈述既有效又相关。我不同意某些措辞,但你现在做的只是纯粹的指责而没有价值。 - Arne Vogel
3
这个回答的标题应该是“我对链接文章的理解为什么是错误的以及我为什么一开始没有阅读它”。 - Lundin
显示剩余18条评论

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