ARM中的原子操作

21

我一直在为ARM嵌入式操作系统工作,但是即使参考了ARM ARM和Linux源代码,我仍然对架构中的一些事情不太理解。

原子操作。

ARM ARM表示Load和Store指令是原子的,并且它的执行在中断处理程序执行之前保证完成。通过查看

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等)以原子方式操纵该值时(在ARMv7上使用LDREX和STREX),这时不知道中断是否会被阻塞。ARMARM并没有在这个部分说明中断是否会被阻塞,所以我假设在LDREX和STREX之间可能会发生中断。它提到的是关于锁定内存总线,我想这只有在多处理器系统中有更多的CPU尝试同时访问同一位置时才有用。但对于单处理器(以及可能的多处理器系统),如果计时器中断(或SMP的IPI)在LDREX和STREX的这个小窗口期间触发,异常处理程序执行后可能会更改CPU上下文并返回新任务,但震惊的事情现在来了,它执行'CLREX',因此删除由先前线程持有的任何独占锁。那么,在UP系统上使用LDREX和STREX相比于LDR和STR如何更好地保证原子性呢?

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

2个回答

19

加载-链接/存储-排他范式的想法是,如果在加载之后很快跟着存储,并且没有任何中间的内存操作,并且如果没有其他东西触摸到该位置,则存储有可能成功,但如果其他东西已经触碰了该位置,则存储肯定失败。不能保证存储不会因为无法解释的原因而失败;然而,如果在负载和存储之间的时间尽量缩短,并且它们之间没有内存访问,那么像以下循环一样:

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循环是有道理的,因为这样独占监视器就不会被上下文切换清除,但重新进行重要计算需要很多开销,正如你在帖子中提到的那样,我观察到街道失败了,没有明显的原因(在PSR中屏蔽IRQ的UP)。 - sgupta
@user1075375:请查看附录。 - supercat
这些(__LDREXW & __STREXW)是Keil编译器为Cortex-M系列微控制器级处理器支持的内置函数,一般不适用于主流ARM目标(例如AArch64)和编译器(例如gcc、llvm),对吗?http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0552a/BABDEEJC.html - ahcox
@ahcox:ARM建议所有针对Cortex-M3系列的编译器供应商支持这些内置函数,但不能强制执行;我希望GCC应该支持它们,但我不太确定。至于其他处理器,我知道Cortex-M0不支持这些操作,但我预计更高级的处理器会支持。通常我建议将它们限制在像“原子递增”等小方法中使用,如果需要,可以轻松地重写它们以使用其他方法(例如,在Cortex-M0上,它们将通过暂时禁用中断来实现)。 - supercat
1
谢谢@supercat。刚刚看了一些AArch64反汇编代码,发现GCC内置的__sync_add_and_fetch(address, 1)使用这些操作生成了最优代码。就像这样:top: ldaxr w1,[x0] add w1,w1,#0x1 stlxr w2,w1,[x0] cbnz w2,top (对于混乱的格式表示抱歉)。 - ahcox

12

好的,我从他们的网站上得到了答案。

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


1
只要您的操作系统在上下文切换中使用clrex或虚拟的strex,或者在返回用户空间之前自己使用LDREX / STREX,这就是安全的。否则,如果您从一个LL / SC重试循环切换到另一个循环的中间,则可能会出现错误的正面。CPU可以在一个循环中看到LDREX,在另一个循环中看到STREX,如果它们之间没有无效化,则在简单实现上可能会成功。 - Peter Cordes
1
非常好的答案,尽管链接现在并不是很有用。那个特定的页面可能已经被移动了。也许这个链接更有用?它似乎包含了相关信息。 - wovano
@PeterCordes,您能详细解释一下吗?文档说明如果发生异常,则会删除独占访问标记。因此(由于任务切换通常使用异常实现),这只应导致假阴性,而不是假阳性,对吗? - wovano
1
@wovano: 当ARM Cortex M7实际上需要CLREX?说在Cortex-M上可能是必要的。我自己没有读过ARM手册;也许在某些型号上有所不同。或者“异常”意味着同步,比如页面故障,不包括外部中断,比如定时器中断。我不知道在ARM上下文中术语的确切含义。 - Peter Cordes
@PeterCordes,感谢提供链接,这很有趣。显然,这些指令存在很多混淆。不同的ARM架构之间可能确实存在差异。对于Cortex-M7,我非常确定中断(以及旨在进行上下文切换的PendSV)都是异常类型,因此我认为LDREX / STREX模式保证在该架构上正常工作。 - wovano

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接