访问共享内存而不使用易失性、std::atomic、信号量、互斥锁和自旋锁



我读了这篇文章。

答案有趣地指出:

实际上,您需要修改代码以不使用 C 库函数 在易失性缓冲液上。您的选项包括:

  1. 编写自己的 C 库函数替代方法,该函数适用于易失性缓冲区。
  2. 使用适当的内存屏障。

我很好奇#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_intvolatile中获得的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_getpsharedpthread_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()操作。

有关进程共享信号量的示例,请参阅如何使用共享内存在进程之间共享信号量。

最新更新