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

memory mapped files and pointers to volatile objects

我对C和C++中volatile的语义的理解是在volatile的语义上把内存访问变成了(observable) side effects. Whenever reading or writing to a memory mapped file (or shared memory) I would expect the the pointer to be volatile qualified, to indicate that this is in fact I/O. (John Regehr wrote a very good article

此外,我希望使用像 memcpy() 这样的函数来访问共享内存是不正确的,因为签名表明 volatile 限定被丢弃,并且内存访问不被视为 I/O .

在我看来,这是一个支持 std::copy() 的论点,其中 volatile 限定符不会被丢弃,并且内存访问被正确地视为 I/O。

但是,我使用指向易失性对象的指针和 std::copy() 访问内存映射文件的经验是,它比仅使用 memcpy() 慢几个数量级。我很想得出结论,也许 clang 和 GCC 在处理 volatile 时过于保守。是这样吗?

关于 volatile 访问共享内存有什么指导,如果我想遵循标准的字母并让它恢复我所依赖的语义?


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

Reading an object designated by a volatile glvalue, modifying an object, calling a library I/O function, or calling a function that does any of those operations are all side effects, which are changes in the state of the execution environment. Evaluation of an expression (or a subexpression) in general includes both value computations (including determining the identity of an object for glvalue evaluation and fetching a value previously assigned to an object for prvalue evaluation) and initiation of side effects. When a call to a library I/O function returns or an access through a volatile glvalue is evaluated the side effect is considered complete, even though some external actions implied by the call (such as the I/O itself) or by the volatile access may not have completed yet.

My understanding of the semantics of volatile in C and C++ is that it turns memory access into I/O

你的理解有误。 Volatile object 是副作用 volatile - 它的值可能会在编译期间被编译器不可见的东西改变

所以 volatile object 必须有一个永久的(当然在其范围内)内存存储位置,必须在使用前从中读取,并在每次更改后保存

看例子: https://godbolt.org/g/1vm1pq

顺便说一句,我认为那篇文章是垃圾 - 它假设程序员认为 volatile 也意味着原子性和一致性,但事实并非如此。那篇文章应该有一个标题 - "Why my understanding of volatile was wrong, and why I still live in the world of myths"

My understanding of the semantics of volatile in C and C++ is that it turns memory access into I/O

不,它不会那样做。 volatile 所做的只是通过 "something else" 从程序员向编译器传达某个内存区域可以随时更改的信息。

"Something else" 可能有很多不同的东西。示例:

  • 内存映射硬件寄存器
  • 与 ISR 共享的变量
  • 从回调函数更新的变量
  • 变量与另一个线程或进程共享
  • 内存区域通过DMA更新

由于标准 (5.1.2.3) 保证对 volatile 对象的访问 (read/write) 可能不会被优化掉,volatile 也可用于阻止某些编译器优化,这在与硬件相关的编程中最有用。

Whenever reading or writing to a memory mapped file (or shared memory) I would expect the the pointer to be volatile qualified

不一定,没有。数据的性质并不重要,重要的是它如何更新。

I would expect using functions like memcpy() to access shared memory to be incorrect

总体而言,这取决于您对 "shared memory" 的定义。这是你整个问题的问题,因为你一直在谈论 "shared memory" 这不是一个正式的、定义明确的术语。与另一个 ISR/thread/process 共享内存?

是的,与另一个 ISR/thread/process 共享的内存可能 必须声明为 volatile,具体取决于编译器。但这只是 因为 volatile 可以防止编译器做出不正确的假设并优化代码以错误的方式访问此类 "shared" 变量。在旧的嵌入式系统编译器上特别容易发生的事情。在现代托管系统编译器上应该没有必要。

volatile 不会导致内存屏障行为。它不会(必然)强制表达式以特定顺序执行。

volatile 当然不保证任何形式的原子性。这就是 _Atomic 类型限定符被添加到 C 语言的原因。

所以回到复制问题——如果内存区域"shared"介于几个ISRs/threads/processes之间,那么volatile根本没有帮助。相反,您需要一些同步方式,例如互斥量、信号量或临界区。

In my mind, this is an argument in favor of std::copy(), where the volatile qualifier won't be cast away, and memory accesses being correctly treated as I/O.

不,由于已经提到的原因,这是错误的。

What guidance is there for accessing shared memory with regards to volatile, if I want to follow the letter of the standard and have it back the semantics I rely on?

使用系统特定的 API:s 来保护内存访问,通过 mutex/semaphore/critical 部分。

我认为你想多了。我看不出 mmap 或等效项(我将在此处使用 POSIX 术语)内存不稳定的任何原因。

从编译器的角度来看 mmap returns 一个对象被修改然后给 msyncmunmap 或者在 _Exit。这些功能需要被视为 I/O,没有别的。

你几乎可以将 mmap 替换为 malloc+read 并将 munmap 替换为 write+free 并且你会得到最多I/O 何时以及如何完成的保证。

请注意,这甚至不需要将数据反馈给 munmap,只是更容易以这种方式进行演示。您可以拥有 mmap return 一块内存,并将其内部保存在列表中,然后是一个没有任何参数的函数(我们称之为 msyncall)内存所有调用 mmap 之前 returned。然后我们可以以此为基础构建,假设任何执行 I/O 的函数都有一个隐含的 msyncall。我们不需要走那么远。从编译器的角度来看,libc 是一个黑盒子,其中一些函数 return 编辑了一些内存,该内存必须在任何其他调用 libc 之前同步,因为编译器无法知道哪些内存位以前 return 从 libc 编辑的仍然被引用并在内部积极使用。

上面这段是实际操作的,但是我们如何从标准的角度来处理呢?我们先来看一个类似的问题。对于线程,共享内存仅在某些 very specific function calls 处同步。这非常重要,因为现代 CPU 重新排序读写和内存屏障是昂贵的,而旧 CPU 可能需要在写入数据被其他人(无论是其他线程、进程还是 I/O)可见之前显式缓存刷新。 mmap 的规范说:

The application must ensure correct synchronization when using mmap() in conjunction with any other file access method

但它没有指定同步是如何完成的。我知道在实践中同步几乎必须是 msync 因为仍然有系统 read/write 没有使用与 mmap 相同的页面缓存。