21

我一直在为 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),我在这里吗?

4

2 回答 2

18

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

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 范式,最高优先级的任务将不受低级任务的影响(但会导致低级任务不得不做浪费的工作)。

于 2013-03-11T18:04:04.513 回答
11

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

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

于 2012-08-10T01:11:24.233 回答