在没有易失性、std::atomic、信号量、互斥量和自旋锁的情况下访问共享内存?

Accessing Shared Memory Without Volatile, std::atomic, semaphore, mutex, and spinlock?

我读了

答案有趣地指出:

You do in fact need to modify your code to not use C library functions on volatile buffers. Your options include:

  1. Write your own alternative to the C library function that works with volatile buffers.
  2. Use a proper memory barrier.

我很好奇#2 怎么可能。假设 2 个(单线程)进程在 CentOS 7 上使用 shm_open() + memcpy() 到 create/open 相同的共享内存。 我正在使用 gcc/g++ 7 和 x86-64。

您可以使用 a process-shared mutex. 或信号量。

NAME

pthread_mutexattr_getpshared, pthread_mutexattr_setpshared - get and set the process-shared attribute

SYNOPSIS

#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]

DESCRIPTION

The pthread_mutexattr_getpshared() function shall obtain the value of the process-shared attribute from the attributes object referenced by attr. The pthread_mutexattr_setpshared() function shall set the process-shared attribute in an initialized attributes object referenced by attr.

The process-shared attribute is set to PTHREAD_PROCESS_SHARED to permit a mutex to be operated upon by any thread that has access to the memory where the mutex is allocated, even if the mutex is allocated in memory that is shared by multiple processes. If the process-shared attribute is PTHREAD_PROCESS_PRIVATE, the mutex shall only be operated upon by threads created within the same process as the thread that initialized the mutex; if threads of differing processes attempt to operate on such a mutex, the behavior is undefined. The default value of the attribute shall be PTHREAD_PROCESS_PRIVATE.

有关进程共享互斥体的示例,请参阅 Condition Variable in Shared Memory - is this code POSIX-conformant?

对于a process-shared semaphore,

NAME

sem_init - initialize an unnamed semaphore (REALTIME)

SYNOPSIS

#include <semaphore.h>

int sem_init(sem_t *sem, int pshared, unsigned value); [Option End]

DESCRIPTION

The sem_init() function shall initialize the unnamed semaphore referred to by sem. The value of the initialized semaphore shall be value. Following a successful call to sem_init(), the semaphore may be used in subsequent calls to sem_wait(), sem_timedwait(), sem_trywait(), sem_post(), and sem_destroy(). This semaphore shall remain usable until the semaphore is destroyed.

If the pshared argument has a non-zero value, then the semaphore is shared between processes; in this case, any process that can access the semaphore sem can use sem for performing sem_wait(), sem_timedwait(), sem_trywait(), sem_post(), and sem_destroy() operations.

有关进程共享信号量的示例,请参阅 How to share semaphores between processes using shared memory

直接回答您的直接问题:使用标准内存屏障 - 将 while 循环更改为:

while (strncmp((char *) mem, "exit", 4) != 0)
    atomic_thread_fence(memory_order_acquire);

(请注意,这是 C。您已将您的问题标记为 C++,而您所指的原始 post 是 C。但是,等效的 C++ 看起来非常相似)。

粗略地说,memory_order_acquire 意味着您想查看其他线程(或在本例中为其他进程)所做的更改。这似乎已经足够了,在我进行的一些简单实验中使用当前的编译器,但是 技术上可能还不够 如果没有原子操作。完整的解决方案将使用原子加载重新实现 strncmp 函数。

严格来说,你不应该在易失性缓冲区上使用 strncmp 之类的东西(即使有内存屏障,这几乎肯定会引发未定义的行为,尽管我想你永远不会遇到当前的问题编译器)。

还有很多更好的方法可以解决您链接的 post 中描述的问题。特别是,对于这种情况,首先使用共享内存意义不大;一个简单的管道将是一个更好的解决方案。

滚动你自己的编译器内存屏障,告诉编译器所有全局变量可能已经被异步修改。

在 C++11 及更高版本中,该语言定义了一个内存模型,该模型指定非原子变量上的数据竞争是未定义的行为。因此,尽管这在现代编译器上仍然有效,但我们应该只讨论 C++03 及更早版本。在 C++11 之前,您必须自己动手,或使用 pthreads 库函数或任何其他库。

相关:


在 GNU C 中 asm("" ::: "memory") 是一个编译器内存屏障。 在 x86 上,一个强有序的架构,这本身就给了你 acq_rel 语义,因为只有一种 运行x86 可以做的时间重新排序是 StoreLoad。

优化器将其视为对非内联函数的函数调用:假定此函数外部 任何东西 可能有指针指向的任何内存都被修改。参见 。 (没有输出的 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 (on the Godbolt compiler explorer) 将其编译为看起来像源代码的 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() 循环来避免这种情况。 ().

除此之外,它与我们使用 std::atomic_intvolatile 得到的 asm 相同。 (参见 Godbolt link)。

没有障碍物,它确实能吊起负载:

# 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

没有任何特定于编译器的东西,您实际上可以 使用 一个非内联函数来击败优化器,但您可能必须将它放在库中才能击败 link-时间优化。仅仅另一个源文件是不够的。所以你最终需要一个特定于系统的 Makefile 或其他什么。 (而且它有 运行 时间开销)。