8

当使用共享内存时,每个进程可以将共享区域映射到其各自地址空间的不同区域。这意味着当在共享区域中存储指针时,您需要将它们存储为共享区域开始的偏移量。不幸的是,这使原子指令的使用变得复杂(例如,如果您正在尝试编写无锁算法)。例如,假设您在共享内存中有一堆引用计数节点,由单个作者创建。编写器定期自动更新指针“p”以指向具有正引用计数的有效节点。读者希望以原子方式写入“p”,因为它指向一个节点(结构)的开头,该节点的第一个元素是引用计数。由于 p 总是指向一个有效的节点,增加 ref 计数是安全的,并且可以安全地取消引用 'p' 并访问其他成员。然而,这一切只有在所有东西都在同一个地址空间中时才有效。如果节点和“p”指针存储在共享内存中,则客户端会遇到竞争条件:

  1. x = 读取 p
  2. y = x + 偏移量
  3. 在 y 处增加引用计数

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

编辑:在 x86 上使用内联汇编和“锁定”前缀也许可以构建一个“增量 ptr X,偏移量 Y 值 Z”?其他架构上的等效选项?没有写很多汇编,不知道有没有需要的指令。

4

5 回答 5

3

在底层,x86 原子指令可以一次完成所有这些树步骤:

  1. x = 读取 p
  2. y = x + 偏移量增量
  3. y 的引用计数
//
      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
//
于 2010-03-22T10:44:19.233 回答
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-x86/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));
}
于 2010-04-20T01:48:30.620 回答
2

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

于 2010-03-22T03:59:14.010 回答
2

你不应该害怕随机组成一个地址,因为内核只会拒绝它不喜欢的地址(那些有冲突的地址)。请参阅shmat()上面的答案,使用0x20000000000

使用地图:

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我们不喜欢 addr、length 或 offset(例如,它们太大,或未在页面边界上对齐)。

于 2010-04-20T02:00:10.890 回答
1

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

于 2016-08-18T16:23:00.117 回答