我读了这篇文章。
答案有趣地指出:
实际上,您需要修改代码以不使用 C 库函数 在易失性缓冲液上。您的选项包括:
- 编写自己的 C 库函数替代方法,该函数适用于易失性缓冲区。
- 使用适当的内存屏障。
我很好奇#2怎么可能。 假设 2 个(单线程)进程使用shm_open()
+memcpy()
在 CentOS 7 上创建/打开相同的共享内存。 我正在使用 gcc/g++ 7 和 x86-64。
滚动自己的编译器内存屏障,告诉编译器所有全局变量可能已被异步修改。
在 C++11 及更高版本中,该语言定义了一个内存模型,该模型指定非原子变量上的数据竞赛是未定义的行为。 因此,尽管这在实践中仍然适用于现代编译器,但我们可能只应该谈论 C++03 及更早版本。 在 C++11 之前,您必须自己滚动,或使用 pthreads 库函数或任何其他库。
相关:互斥锁和解锁功能如何防止 CPU 重新排序?
在GNU C中,asm("" ::: "memory")
是一个编译器内存屏障。在 x86(一种强序架构)上,仅此一项就为您提供了acq_rel语义,因为 x86 唯一可以做的运行时重新排序就是 StoreLoad。
优化器将其视为对非内联函数的函数调用:此函数之外的任何内容都可以指向的任何内存都被假定为被修改。 请参阅了解易失性 asm 与易失性变量。 (没有输出的GNU C扩展asm语句是隐式volatile
,所以asm volatile("" ::: "memory")
更明确但等效。
另请参阅 http://preshing.com/20120625/memory-ordering-at-compile-time/,了解有关编译器屏障的更多信息。 但请注意,这不仅仅是阻止重新排序,而是阻止优化,例如将值保留在循环中的寄存器中。
例如,像while(shared_var) {}
这样的自旋循环可以编译为if(shared_var) infinite_loop;
,但通过屏障我们可以防止这种情况:
void spinwait(int *ptr_to_shmem) {
while(shared_var) {
asm("" ::: "memory");
}
}
gcc -O3 for x86-64(在 Godbolt 编译器资源管理器上)将其编译为 asm,看起来像源代码,而不会将负载提升到循环之外:
# gcc's output
spinwait(int*):
jmp .L5 # gcc doesn't check or know that the asm statement is empty
.L3:
#APP
# 3 "/tmp/compiler-explorer-compiler118610-54-z1284x.occil/example.cpp" 1
#asm comment: barrier here
# 0 "" 2
#NO_APP
.L5:
mov eax, DWORD PTR [rdi]
test eax, eax
jne .L3
ret
asm
语句仍然是一个易失性的 asm 语句,它必须运行与循环体在 C 抽象机器中运行的次数完全相同。 GCC 跳过空的 asm 语句以到达循环底部的条件,以确保在运行(空)asm 语句之前检查条件。 我在 asm 模板中放置了一个 asm 注释,以查看它在编译器生成的整个函数的 asm 中的最终位置。 我们本可以通过在 C 源代码中编写一个do{}while()
循环来避免这种情况。(为什么循环总是编译成"do...而"风格(尾跳)?
除此之外,它与我们从使用std::atomic_int
或volatile
中获得的asm相同。 (参见 Godbolt 链接)。
没有屏障,它确实可以提升负载:
# clang6.0 -O3
spinwait_nobarrier(int*): # @spinwait_nobarrier(int*)
cmp dword ptr [rdi], 0
je .LBB1_2
.LBB1_1: #infinite loop
jmp .LBB1_1
.LBB1_2: # jump target for 0 on entry
ret
没有任何特定于编译器的内容,您实际上可以使用非内联函数来击败优化器,但您可能必须将其放入库中以击败链接时优化。 仅使用另一个源文件是不够的。 因此,您最终需要一个特定于系统的Makefile或其他任何东西。 (并且它具有运行时开销)。
要直接回答您的直接问题: 使用标准内存屏障 - 将 while 循环更改为:
while (strncmp((char *) mem, "exit", 4) != 0)
atomic_thread_fence(memory_order_acquire);
(请注意,这是C。您已将问题标记为C++,而您引用的原始帖子是 C。但是,等效C++看起来非常相似)。
粗略地说,memory_order_acquire
意味着您希望看到其他线程(或在本例中为其他进程)所做的更改。这似乎已经足够了,在我进行的一些简单实验中,当前的编译器,但如果没有原子操作的存在,技术上可能还不够。完整的解决方案将使用原子负载重新实现strncmp
函数。
严格来说,你不应该在易失性缓冲区上使用strncmp
等(即使有内存屏障,这几乎肯定会引发未定义的行为,尽管我想你永远不会遇到当前的编译器问题)。
还有更好的方法来解决您链接的帖子中描述的问题。特别是,对于这种情况,首先使用共享内存几乎没有意义;一个简单的管道将是一个更好的解决方案。
您可以使用进程共享互斥锁或信号量。
名字
pthread_mutexattr_getpshared
,pthread_mutexattr_setpshared
- 获取 并设置进程共享属性概要
#include <pthread.h> int pthread_mutexattr_getpshared(const pthread_mutexattr_t * restrict attr, int *restrict pshared); int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared); [Option End]
描述
pthread_mutexattr_getpshared()
函数应获得值 引用的属性对象中的进程共享属性 由attr
.pthread_mutexattr_setpshared()
函数应设置 初始化属性对象中的进程共享属性attr
引用 .进程共享属性设置为
PTHREAD_PROCESS_SHARED
允许任何有权访问的线程对互斥体进行操作 分配互斥锁的内存,即使互斥锁 在由多个进程共享的内存中分配。如果 进程共享属性PTHREAD_PROCESS_PRIVATE
,互斥锁应 仅由在同一进程中创建的线程操作 初始化互斥锁的线程;如果线程不同 进程尝试对此类互斥锁进行操作,行为是 定义。属性的默认值应为PTHREAD_PROCESS_PRIVATE
.
有关进程共享互斥锁的示例,请参阅共享内存中的条件变量 - 此代码是否符合 POSIX 标准。
对于进程共享信号量,
名字
sem_init
- 初始化未命名的信号量(实时)概要
#include <semaphore.h> int sem_init(sem_t *sem, int pshared, unsigned value); [Option End]
描述
sem_init()
函数应初始化未命名的信号量sem
.初始化信号量的值应为 价值。在成功调用sem_init()
后,信号量可以 用于后续调用sem_wait()
、sem_timedwait()
、sem_trywait()
、sem_post()
和sem_destroy()
。此信号量 在信号量被销毁之前,应保持可用状态。如果
pshared
参数具有非零值,则信号量为 在进程之间共享;在这种情况下,任何可以访问的进程 信号量sem
可以使用sem
来执行sem_wait()
,sem_timedwait()
、sem_trywait()
、sem_post()
和sem_destroy()
操作。
有关进程共享信号量的示例,请参阅如何使用共享内存在进程之间共享信号量。