是否可以在不使用偏移量的情况下将指针存储在共享内存中?

时间:2010-03-22 03:39:11

标签: multithreading atomic shared-memory race-condition mmap

当使用共享内存时,每个进程可以将共享区域映射到其相应地址空间的不同区域。这意味着在共享区域中存储指针时,您需要store them as offsets共享区域的开头。不幸的是,这使得使用原子指令变得复杂(例如,如果你试图写一个lock free algorithm)。例如,假设您在共享内存中有一堆引用计数节点,由单个编写器创建。编写器定期原子地更新指针'p'以指向具有正引用计数的有效节点。读者希望原子地写入'p',因为它指向一个节点(结构)的开头,其第一个元素是引用计数。由于p始​​终指向有效节点,因此增加引用计数是安全的,并且可以安全地取消引用“p”并访问其他成员。但是,这一切只有在所有内容都在同一地址空间时才有效。如果节点和'p'指针存储在共享内存中,则客户端会遇到竞争条件:

  1. x =读p
  2. y = x + offset
  3. y
  4. 处的增量引用计数

    在步骤2期间,p可能会更改,x可能不再指向有效节点。我能想到的唯一解决方法是以某种方式迫使所有进程就共享内存映射的位置达成一致,以便在mmap'd区域中存储真实指针而不是偏移量。有没有办法做到这一点?我在mmap文档中看到了MAP_FIXED,但我不知道如何选择一个安全的地址。

    编辑:使用内联汇编和x86上的'lock'前缀也许可以建立一个“增量ptr X,偏移量Y乘以值Z”?其他架构的等价选项?没有写过很多装配,不知道是否存在所需的指令。

5 个答案:

答案 0 :(得分:3)

在低级别,x86原子结构可以立即执行所有这些树步骤:

  1. x =读p
  2. y = x +偏移量增量
  3. 处引用refcount
    //
          mov  edi, Destination
          mov  edx, DataOffset
          mov  ecx, NewData
     @Repeat:
          mov  eax, [edi + edx]    //load OldData
    //Here you can also increment eax and save to [edi + edx]          
          lock cmpxchg dword ptr [edi + edx], ecx
          jnz  @Repeat
    //
    

答案 1 :(得分:3)

这在UNIX系统上是微不足道的;只需使用共享内存函数:

shgmet,shmat,shmctl,shmdt

void * shmat(int shmid,const void * shmaddr,int shmflg);

  

shmat()附加共享内存   由shmid识别的段   调用进程的地址空间。   附加地址由。指定   shmaddr与以下之一   标准:

     

如果shmaddr为NULL,则系统选择   一个合适的(未使用的)地址   附上该段。

只需在此指定您自己的地址;例如的 0x20000000000

如果shmget()在每个进程中使用相同的键和大小,您将获得相同的共享内存段。如果shmat()位于同一地址,则虚拟地址在所有进程中都是相同的。内核不关心你使用的是什么地址范围,只要它不与通常分配内容的地方冲突。 (如果你省略了地址,你可以看到它喜欢放置的一般区域;同样,检查堆栈上的地址并从malloc()/ new []返回。)

在Linux上,确保root将/ proc / sys / kernel / shmmax中的SHMMAX设置为足够大的数字以容纳您的共享内存段(默认为32MB)。

至于原子操作,你可以从Linux内核源代码中获取它们,例如:

  

包括/ ASM-86 / atomic_64.h

/*
 * Make sure gcc doesn't try to be clever and move things around
 * on us. We need to use _exactly_ the address the user gave us,
 * not some alias that contains the same information.
 */
typedef struct {
        int counter;
} atomic_t;

/**
 * atomic_read - read atomic variable
 * @v: pointer of type atomic_t
 *
 * Atomically reads the value of @v.
 */
#define atomic_read(v)          ((v)->counter)

/**
 * atomic_set - set atomic variable
 * @v: pointer of type atomic_t
 * @i: required value
 *
 * Atomically sets the value of @v to @i.
 */
#define atomic_set(v, i)                (((v)->counter) = (i))


/**
 * atomic_add - add integer to atomic variable
 * @i: integer value to add
 * @v: pointer of type atomic_t
 *
 * Atomically adds @i to @v.
 */
static inline void atomic_add(int i, atomic_t *v)
{
        asm volatile(LOCK_PREFIX "addl %1,%0"
                     : "=m" (v->counter)
                     : "ir" (i), "m" (v->counter));
}

64位版本:

typedef struct {
        long counter;
} atomic64_t;

/**
 * atomic64_add - add integer to atomic64 variable
 * @i: integer value to add
 * @v: pointer to type atomic64_t
 *
 * Atomically adds @i to @v.
 */
static inline void atomic64_add(long i, atomic64_t *v)
{
        asm volatile(LOCK_PREFIX "addq %1,%0"
                     : "=m" (v->counter)
                     : "er" (i), "m" (v->counter));
}

答案 2 :(得分:2)

我们的代码与您的问题描述类似。我们使用内存映射文件,偏移量和文件锁定。我们还没有找到替代方案。

答案 3 :(得分:2)

你不应该害怕随意编造一个地址,因为内核只会拒绝它不喜欢的地址(那些冲突的地址)。使用 0x20000000000

查看上面的shmat()答案

使用mmap:

  

void * mmap(void * addr,size_t length,int prot,int flags,int fd,off_t offset);

     

如果addr不是NULL,那么内核   把它作为暗示在哪里   放置映射;在Linux上,   映射将在下一次创建   更高的页面边界。的地址   新映射作为返回   电话结果。

     

flags参数确定是否   可以看到对映射的更新   映射相同的其他进程   区域,以及是否更新   贯彻到底层   文件。此行为由确定   包括以下之一   标志中的值:

     

MAP_SHARED 分享此映射。   可以看到对映射的更新   映射此的其他进程   文件,并进行到   底层文件。该文件可能不会   实际上会更新,直到msync(2)或   调用munmap()。

     

<强>错误

     

EINVAL 我们不喜欢地址,长度或者   偏移(例如,它们太大,或   没有在页面边界上对齐)。

答案 4 :(得分:1)

向指针添加偏移不会产生竞争的可能性,它已经存在。由于至少ARM和x86都不能原子地读取指针然后访问它所引用的内存,无论是否添加偏移量,都需要使用锁保护指针访问。