ARM Cortex M3上的原子int64_t

7

由于我的编译器仍不支持c++11和std::atomic,我被迫通过ldrex-strex对其进行手动实现。

我的问题是:使用ldrex和strex以“原子方式”读取修改写入int64_t的正确方法是什么?

这样的简单解决方案似乎行不通(其中一个STREXW始终返回1):

volatile int64_t value;
int64_t temp;

do
{
    int32_t low = __LDREXW( (uint32_t *)&value );
    int32_t high = __LDREXW( ((uint32_t *)&value)+1 );

    temp = (int64_t)low | ( (int64_t)high<<32);
    temp++;    

} while( __STREXW( temp, (uint32_t *)&value) |  __STREXW( temp>>32, ((uint32_t *)&value)+1) );

我在手册中找不到关于多个连续的LDREX或STREX指令指向不同地址的内容,但我认为应该是允许的。

否则,在某些情况下,多个线程将无法更改两个不同的原子变量。


1
它是GCC吗?考虑GCC内置的原子操作? - user3528438
GCC现在支持std :: atomic。不,这不是GCC,而是Keil armcc。 - Amomum
请参阅:ARM AN321 - Cortex-M memory barriers。"原子"是一个多义词。在宇宙中,事物不可能以原子方式发生。它们是原子的,相对于某些电子设备;主线/中断、SMP、总线上的设备等等。你到底有什么问题?你必须更好地描述它。 - artless noise
此外,您可以使用ldrdstrd与自旋锁和/或结构,例如环形缓冲区,在单个读取器/写入器方面固有安全性,并且已经多年用于主线/中断。请参见:Donald Knuth,《计算机编程的艺术》和 Stefani Seibold等人的Linux kfifo.h。我认为她在这个实现上做得很好,但它依赖于一些狂热者可能不喜欢的溢出条件。 - artless noise
@artlessnoise,恐怕我不明白你所说的“单个ldrex和strex可以用于多个地址位置”的意思。ldrex和strex都只接受一个地址作为参数。或者你是指我可以通过ldrex读取uint64的一半,然后通过ldr读取另一半,因为任何strex都会导致独占式保留失败? - Amomum
显示剩余2条评论
2个回答

6
这是行不通的,因为您不能以这种方式嵌套排他性。就实现而言,Cortex-M3本地独占监视器甚至不会跟踪地址 - 独占保留颗粒是整个地址空间 - 因此追踪每个单词的假设已经无效。但是,您甚至不需要考虑任何实现细节,因为架构已经明确排除了连续的strex

如果执行两个STREX指令而没有介于它们之间的LDREX,则第二个STREX返回值为1。这意味着:

  • 在给定的执行线程中,每个STREX必须有一个先前的关联LDREX。
  • 并不是每个LDREX都需要有后续的STREX。

由于Cortex-M3(以及ARMv7-M)没有类似ARMv7-A的ldrexd,因此您将不得不使用单独的锁来控制对变量的所有访问,或者只需在读取-修改-写入期间禁用中断。如果可能的话,最好重新设计事物,以不需要首先具有原子64位类型,因为您仍然只能针对同一核心上的其他线程实现原子性 - 您根本无法从外部代理(如DMA控制器)的角度使任何64位操作成为原子操作。


我本来就担心这个问题。所以现在如果我理解正确,任何一个ldrex都会为其他所有的ldrex重置独占监视器? - Amomum
谢谢,现在我明白了! - Amomum
从外观上看,v7-M 甚至没有本地/全局监视器的区别 - 它读起来像是 v7-A 本地监视器状态机的简化版本。我猜在实际应用中,硬件可能甚至不会费心跟踪地址,并只在任何内存访问时清除排他状态。 - Notlikethat
不,划掉那个,我只是在错误的地方查找。可以为可共享区域设置全局监视器,但我怀疑大多数微处理器都没有实现这一点。 - Notlikethat
如果我使用ldrex来获取uint64的高32位,然后使用简单的ldr来获取低32位,总是按照这个顺序进行,那会怎样呢?高32位不会像整个uint64的自旋锁一样吗?虽然在32位模式下无法用于DMA,但这是一种方法。 - Amomum
显示剩余4条评论

3
我只需查看gcc如何执行,然后使用相同的指令序列。
gcc 4.8.2声称使用is_lock_free()实现std::atomic,即使使用-mcpu=cortex-m3也返回true。但不幸的是,它并不能正常工作。这会导致代码无法链接或无法工作,因为它试图使用的辅助函数没有实现。(感谢@Notlikethat尝试了一下)。
这是我尝试过的测试代码。如果该链接已失效,请参阅此答案的旧版本。我将保留此答案,以防该想法对任何在gcc实际上生成有用代码的相关情况有用。

我尝试了你建议的使用gcc 5.2.1编译ARM的方法,但是出现了链接错误:"e:\gcc-arm-none-eabi-5_2-2015q4-20151219-win32\arm-none-eabi\include\c++\5.2.1\bits/atomic_base.h:514: undefined reference to `__atomic_fetch_add_8'"。当创建std::atomic<long long>时,std::atomic<int>按预期工作。看起来很合理。 - Amomum
@Amomum:当需要从libgcc.a中获取帮助函数时,您的gcc安装通常是否正常工作?这可能是gcc的错误或安装不正确的编译器问题。64位除法很可能会调用类似的辅助函数。 - Peter Cordes
uint64_t的除法调用__aeabi_uldivmod如预期那样。我认为那不是一个bug,也许真的没有办法实现原子uint64_t?例如,某些架构没有uint8_t是完全正常的(如果CHAR_BITS大于8);因此,我认为没有__atomic_fetch_add_8并不是一个很大的问题。 - Amomum
额,这不是很明显吗?我也遇到了atomic<double>的链接错误,这与Notlikethat的答案非常一致——cortex m3没有ldrexd,因此独占式加载存储的最大大小为4字节。 - Amomum
1
很遗憾,GCC生成调用并不意味着太多。我试图在几个完整的ARM工具链上测试了一下你的示例 - 裸机的newlib(arm-none-eabi)根本没有__atomic_fetch_add_8实现,因此无法链接;glibc(arm-linux-gnueabihf)实现使用__sync_fetch_and_add_8,而这又依赖于Linux提供的cmpxchg64助手(在缺少ldrexd的情况下触发一些魔法来重新启动用户空间路径,以防它被中断),但可惜这对Cortex-M3也不适用。 - Notlikethat
显示剩余2条评论

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