访问共享内存而又不易失,std :: atomic,semaphore,mutex和spinlock?

时间:2018-07-10 11:59:36

标签: c++ gcc x86 shared-memory volatile

我读了this post

有趣的是,答案指出:

  

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

     
      
  1. 编写与易失性缓冲区一起使用的C库函数的替代方法。
  2.   
  3. 使用适当的内存屏障。
  4.   

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

3 个答案:

答案 0 :(得分:2)

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

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

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

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

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

还有很多更好的方法可以解决您链接的文章中描述的问题。尤其是在这种情况下,首先使用共享内存几乎没有意义。简单的管道将是更好的解决方案。

答案 1 :(得分:2)

滚动您自己的编译器内存障碍,以告知编译器所有全局变量可能已被异步修改。

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

相关:How does a mutex lock and unlock functions prevents CPU reordering?


在GNU C asm("" ::: "memory")中是编译器的内存障碍。在x86(一种强序体系结构)上,这本身就为您提供了acq_rel语义,因为x86唯一的运行时重排序方法是StoreLoad。

优化器将其视为对非内联函数的函数调用:该函数之外的任何东西可能具有指针的任何内存都被假定为已修改。参见Understanding volatile asm vs volatile variable。 (没有输出的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");
    }
}

用于x86-64(on the Godbolt compiler explorer)的gcc -O3将此编译为看起来像源代码的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()循环来避免这种情况。 (Why are loops always compiled into "do...while" style (tail jump)?)。

除此之外,它与我们使用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或其他任何文件。 (并且有运行时开销)。

答案 2 :(得分:-1)

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

  

NAME

     

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()函数应获取该值   引用的属性对象中的流程共享属性   由attrpthread_mutexattr_setpshared()函数应设置   初始化属性对象中的进程共享属性   由attr引用。

     

进程共享属性设置为PTHREAD_PROCESS_SHARED   允许任何有权访问的线程对互斥量进行操作   分配互斥锁的内存,即使互斥锁为   分配在由多个进程共享的内存中。如果   进程共享属性为PTHREAD_PROCESS_PRIVATE,互斥体应为   只能由与   初始化互斥量的线程;如果线程不同   进程尝试在这种互斥锁上进行操作,其行为是   未定义。该属性的默认值应为   PTHREAD_PROCESS_PRIVATE

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

对于a process-shared semaphore

  

NAME

     

sem_init-初始化未命名的信号灯(REALTIME)

     

简介

#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()   操作。

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