【问题标题】:Atomic operations in ARMARM 中的原子操作
【发布时间】:2012-08-10 00:53:55
【问题描述】:

我一直在为 ARM 开发嵌入式操作系统,但是即使在参考了 ARMARM 和 linux 源代码之后,我仍然对架构有一些不了解的地方。

原子操作。

ARM ARM 说加载和存储指令是原子的,它的执行保证在中断处理程序执行之前完成。通过查看验证

arch/arm/include/asm/atomic.h :
    #define atomic_read(v)  (*(volatile int *)&(v)->counter)
    #define atomic_set(v,i) (((v)->counter) = (i))

但是,当我想使用 cpu 指令(atomic_inc、atomic_dec、atomic_cmpxchg 等)以原子方式操作此值时,问题就出现了,这些指令将 LDREX 和 STREX 用于 ARMv7(我的目标)。

ARMARM 在本节中没有说明中断被阻止的任何内容,因此我假设 LDREX 和 STREX 之间可能发生中断。它确实提到的是关于锁定内存总线,我猜这仅对 MP 系统有帮助,因为 MP 系统可能有更多的 CPU 试图同时访问相同的位置。但是对于 UP(可能还有 MP),如果在 LDREX 和 STREX 的这个小窗口中触发了定时器中断(或 SMP 的 IPI),异常处理程序执行可能会更改 cpu 上下文并返回新任务,但是令人震惊的部分现在出现了,它执行 'CLREX' 并因此删除前一个线程持有的任何独占锁。那么在 UP 系统上使用 LDREX 和 STREX 比使用 LDR 和 STR 的原子性好多少?

我确实读过一些关于独占锁监视器的内容,所以我有一个可能的理论,即当线程恢复并执行 STREX 时,操作系统监视器会导致此调用失败,可以检测到并且可以重新执行循环使用过程中的新值(分支回到 LDREX),我在这里吗?

【问题讨论】:

    标签: thread-safety kernel arm atomic


    【解决方案1】:

    load-linked/store-exclusive 范例背后的想法是,如果存储在加载后很快跟随,没有干预内存操作,并且如果没有其他任何内容触及该位置,商店可能成功,但如果有其他东西触及该位置,商店肯定会失败。不能保证商店有时不会无缘无故倒闭;但是,如果加载和存储之间的时间保持在最短,并且它们之间没有内存访问,则循环如下:

    do
    {
      new_value = __LDREXW(dest) + 1;
    } while (__STREXW(new_value, dest));
    

    通常可以依靠几次尝试就成功。如果根据旧值计算新值需要进行大量计算,则应将循环重写为:

    do
    {
      old_value = *dest;
    
      new_value = complicated_function(old_value);
    } while (CompareAndStore(dest, new_value, old_value) != 0);
    
    ... Assuming CompareAndStore is something like:
    
    uint32_t CompareAndStore(uint32_t *dest, uint32_t new_value, uint_32 old_value)
    {
      do
      {
        if (__LDREXW(dest) != old_value) return 1; // Failure
      } while(__STREXW(new_value, dest);
      return 0;
    }
    

    如果在计算新值时发生了某些更改 *dest,则此代码将不得不重新运行其主循环,但如果 __STREXW 由于某些其他原因失败,则只有小循环需要重新运行 [希望不太可能,假设在 __LDREXW 和 __STREXW 之间只有大约两条指令]

    附录 “基于旧值计算新值”可能很复杂的情况的一个示例是“值”实际上是对复杂数据结构的引用。代码可以获取旧的引用,从旧的数据结构派生新的数据结构,然后更新引用。与“裸机”编程相比,这种模式在垃圾收集框架中出现的频率要高得多,但即使在对裸机进行编程时,也可以通过多种方式出现。普通的 malloc/calloc 分配器通常不是线程安全/中断安全的,但用于固定大小结构的分配器通常是。如果一个人有一个由 2 次方的数据结构组成的“池”(比如 255 个),则可以使用以下内容:

    #define FOO_POOL_SIZE_SHIFT 8
    #define FOO_POOL_SIZE (1 << FOO_POOL_SIZE_SHIFT)
    #define FOO_POOL_SIZE_MASK (FOO_POOL_SIZE-1)
    
    void do_update(void)
    {
      // The foo_pool_alloc() method should return a slot number in the lower bits and
      // some sort of counter value in the upper bits so that once some particular
      // uint32_t value is returned, that same value will not be returned again unless
      // there are at least (UINT_MAX)/(FOO_POOL_SIZE) intervening allocations (to avoid
      // the possibility that while one task is performing its update, a second task
      // changes the thing to a new one and releases the old one, and a third task gets
      // given the newly-freed item and changes the thing to that, such that from the
      // point of view of the first task, the thing never changed.)
    
      uint32_t new_thing = foo_pool_alloc();
      uint32_t old_thing;
      do
      {
        // Capture old reference
        old_thing = foo_current_thing;
    
        // Compute new thing based on old one
        update_thing(&foo_pool[new_thing & FOO_POOL_SIZE_MASK],
          &foo_pool[old_thing & FOO_POOL_SIZE_MASK);
      } while(CompareAndSwap(&foo_current_thing, new_thing, old_thing) != 0);
      foo_pool_free(old_thing);
    }
    

    如果不经常有多个线程/中断/任何试图同时更新同一事物的事物,这种方法应该允许安全地执行更新。如果可能尝试更新同一项目的事物之间存在优先级关系,则最高优先级的事物在第一次尝试时保证成功,次高优先级的事物将在任何未被抢占的尝试中成功最高优先级的任务,等等。如果一个正在使用锁定,则想要执行更新的最高优先级任务将不得不等待较低优先级的更新完成;使用 CompareAndSwap 范式,最高优先级的任务将不受低级任务的影响(但会导致低级任务不得不做浪费的工作)。

    【讨论】:

    • 我一直在做完全相同的事情,但新值需要大量计算的部分仍然让我感到困惑。使用 cmxchg 循环是有意义的,因为这样独占监视器不会被上下文切换清除,但重新进行重要计算需要大量开销,因为我观察到 street 没有明显原因而失败(UP with IRQs masked in PSR ) 如您的帖子中所述。
    • @user1075375:见附录
    • 这些 (__LDREXW & __STREXW) 是 Cortex-M 系列微控制器级处理器的 Keil 编译器支持的内在函数,通常不适用于主流 ARM 目标(例如 AArch64)和编译器(例如 gcc、 llvm)对吗? infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0552a/…
    • @ahcox:ARM 建议所有面向 Cortex-M3 系列的编译器供应商都支持这些内在函数,但不能强制它们;我希望 GCC 应该支持他们,但我真的不知道。至于其他处理器,我知道 Cortex-M0 不支持这些操作,但我希望更高级的处理器能够支持。一般来说,我建议将它们的使用限制在诸如“原子增量”之类的小方法上,如果需要使用其他方法可以很容易地重写(例如在 Cortex-M0 上,它们将通过暂时禁用中断来实现) .
    • 谢谢@supercat。刚才看一些 AArch64 反汇编,发现 GCC 内置 __sync_add_and_fetch(address, 1) 使用这些操作生成最佳代码。像这样:top:ldaxr w1, [x0]add w1, w1, #0x1stlxr w2, w1, [x0]cbnz w2, top(抱歉格式混乱)。
    【解决方案2】:

    好的,从他们的website得到答案。

    如果上下文切换在进程执行 Load-Exclusive 之后但在执行 Store-Exclusive 之前调度进程,则 Store-Exclusive 在进程恢复时返回错误否定结果,并且不会更新内存。这不会影响程序功能,因为进程可以立即重试操作。

    【讨论】:

    • 这是安全的,只要你的操作系统在上下文切换中使用clrex 或虚拟strex,或者它本身在返回用户空间之前使用 LDREX / STREX。否则,如果您从一个 LL/SC 重试循环切换到另一个循环的中间,您可能会得到一个错误的肯定。 CPU 可以在一个循环中看到 LDREX,在另一个循环中看到 STREX,如果它们之间没有失效,那么它可以在简单的实现中成功。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-02-13
    • 2010-09-21
    • 1970-01-01
    • 1970-01-01
    • 2014-06-30
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多