【问题标题】:Is it possible to store pointers in shared memory without using offsets?是否可以在不使用偏移量的情况下将指针存储在共享内存中?
【发布时间】:2011-01-30 04:48:59
【问题描述】:

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

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

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

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

【问题讨论】:

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


    【解决方案1】:

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

    【讨论】:

      【解决方案2】:

      在底层,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
      //
      

      【讨论】:

      • 如果 cmpxchg 已经进行了原子读取和原子写入,是否需要“锁定”?或者这是否确保 edi + edx 以原子方式完成?我只真正使用过 MIPS 程序集。
      • 锁定保证对内存总线的原子访问,因此锁定指令是必要的。您可能还可以使用 API InterlockedCompareExchange(查看 MSDN 以获得解释)。首先将内存 32 位指针加载为 OldValue,然后将其递增以获取 NewValue,然后尝试执行 InterlockedCompareExchange。 InterlockedCompareExchange(Destination + Offset, NewValue, OldValue) 将返回比较值,如果与 OldValue 不同,其他线程正在交换它,因此没有进行交换,您必须重复该过程。
      【解决方案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));
      }
      

      【讨论】:

        【解决方案4】:

        您不应该害怕随机编造一个地址,因为内核只会拒绝它不喜欢的地址(有冲突的地址)。使用 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 我们不喜欢 addr、length 或 偏移量(例如,它们太大,或 未在页面边界上对齐)。

        【讨论】:

        • 有趣,我以为你只能在编写设备驱动程序或其他低级黑客时使用它。这是一个潜在的有吸引力的解决方案,但它需要让所有进程尝试对不同的区域进行映射,直到找到一个他们都可以同意的区域,并且如果启动了一个不喜欢现有映射的新进程,所有旧的映射都会可能必须将其数据复制到新位置。尽管如此,很酷的想法,赞成。
        • @shm skywalker,我知道这是一个旧线程,但这很酷。感谢分享:) 我现在正试图理解,为什么内核会拒绝?有什么方法可以防止这种拒绝 - 也许通过配置链接器来创建一个未使用的虚拟部分?
        【解决方案5】:

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2012-10-09
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2023-03-23
          • 2014-08-13
          • 1970-01-01
          相关资源
          最近更新 更多