ARM平台下独占访问指令LDREX和STREX的原理

2023-01-05 18:26:04 浏览数 (2)

作者简介:Roland_Sun 职业旅程:Engineer->Senior Engineer->QA Manager->Project Manager->Senior Staff Engineer

为了实现线程间同步,一般都要在执行关键代码段之前加互斥(Mutex)锁,且在执行完关键代码段之后解锁。为了实现所谓的互斥锁的概念,一般都需要所在平台提供支持。

在计算机领域里,如果要在多线程的情况下要保持数据的同步,需要引入称作Load-Link(LL)和Store-Conditional(SC)的操作,通常简称为LL/SC。LL操作返回一个内存地址上当前存储的值,后面的SC操作,会向这个内存地址写入一个新值,但是只有在这个内存地址上存储的值,从上个LL操作开始直到现在都没有发生改变的情况下,写入操作才能成功,否则都会失败。这个操作非常重要,是很多平台实现基本原子操作的基础。 对于ARM平台来说,也在硬件层面上提供了对LL/SC的支持,LL操作用的是LDREX指令,SC操作用的是STREX指令。

本文主要用来说明ARM平台上特有的独占访问指令LDREX和STREX的工作原理,以及如何使用。而它们也是ARM平台上,实现互斥锁等线程同步工具的基础。

我们先来看看LDREX和STREX两条指令的语义。其实LDREX和STREX指令,是将单纯的更新内存的原子操作分成了两个独立的步骤。

  1. LDREX用来读取内存中的值,并标记对该段内存的独占访问:

LDREX Rx, [Ry]

上面的指令意味着,读取寄存器Ry指向的4字节内存值,将其保存到Rx寄存器中,同时标记对Ry指向内存区域的独占访问。如果执行LDREX指令的时候发现已经被标记为独占访问了,并不会对指令的执行产生影响。

  1. STREX在更新内存数值时,会检查该段内存是否已经被标记为独占访问,并以此来决定是否更新内存中的值:

STREX Rx, Ry, [Rz]

如果执行这条指令的时候发现已经被标记为独占访问了,则将寄存器Ry中的值更新到寄存器Rz指向的内存,并将寄存器Rx设置成0。指令执行成功后,会将独占访问标记位清除。而如果执行这条指令的时候发现没有设置独占标记,则不会更新内存,且将寄存器Rx的值设置成1。一旦某条STREX指令执行成功后,以后再对同一段内存尝试使用STREX指令更新的时候,会发现独占标记已经被清空了,就不能再更新了,从而实现独占访问的机制。

大致的流程就是这样,但是ARM内部为了实现这个功能,还有不少复杂的情况要处理。

在ARM系统中,内存有两种不同且对立的属性,即共享(Shareable)和非共享(Non-shareable)。共享意味着该段内存可以被系统中不同处理器访问到,这些处理器可以是同构的也可以是异构的。而非共享,则相反,意味着该段内存只能被系统中的一个处理器所访问到,对别的处理器来说不可见。

为了实现独占访问,ARM系统中还特别提供了所谓独占监视器(Exclusive Monitor)的东西,其结构大致如下:

可以看出来,一共有两种类型的独占监视器。每一个处理器内部都有一个本地监视器(Local Monitor),且在整个系统范围内还有一个全局监视器(Global Monitor)。

如果要对非共享内存区中的值进行独占访问,只需要涉及本处理器内部的本地监视器就可以了;而如果要对共享内存区中的内存进行独占访问,除了要涉及到本处理器内部的本地监视器外,由于该内存区域可以被系统中所有处理器访问到,因此还必须要由全局监视器来协调。

对于本地监视器来说,它只标记了本处理器对某段内存的独占访问,在调用LDREX指令时设置独占访问标志,在调用STREX指令时清除独占访问标志。而对于全局监视器来说,它可以标记每个处理器对某段内存的独占访问。也就是说,当一个处理器调用LDREX访问某段共享内存时,全局监视器只会设置针对该处理器的独占访问标记,不会影响到其它的处理器。

当在以下两种情况下,会清除某个处理器的独占访问标记:

  1. 当该处理器调用LDREX指令,申请独占访问另一段内存时;
  2. 当别的处理器成功更新了该段独占访问内存值时。

对于第二种情况,也就是说,当独占内存访问内存的值在任何情况下,被任何一个处理器更改过之后,所有申请独占该段内存的处理器的独占标记都会被清空。

现在的处理器基本上都是多核的,一个芯片上集成了多个处理器。而且对于一般的操作系统,系统内存基本上都被设置上了共享属性,也就是说对系统中所有处理器可见。因此,我们这里主要分析多核系统中对共享内存的独占访问的情况。

为了更加清楚的说明,我们可以举一个例子。假设系统中有两个处理器内核,而一个程序由三个线程组成,其中两个线程被分配到了第一个处理器上,另外一个线程被分配到了第二个处理器上。且他们的执行序列如下:

大致经历的步骤如下:

  1. CPU2上的线程3最早执行LDREX,锁定某段共享内存区域。它会相应更新本地监视器和全局监视器。
  2. 然后,CPU1上的线程1执行LDREX,它也会更新本地监视器和全局监视器。这时在全局监视器上,CPU1和CPU2都对该段内存做了独占标记。
  3. 接着,CPU1上的线程2执行LDREX指令,它会发现本处理器的本地监视器对该段内存有了独占标记,同时全局监视器上CPU1也对该段内存做了独占标记,但这并不会影响这条指令的操作。
  4. 再下来,CPU1上的线程1最先执行了STREX指令,尝试更新该段内存的值。它会发现本地监视器对该段内存是有独占标记的,而全局监视器上CPU1也有该段内存的独占标记,则更新内存值成功。同时,清除本地监视器对该段内存的独占标记,还有全局监视器所有处理器对该段内存的独占标记。
  5. 下面,CPU2上的线程3执行STREX指令,也想更新该段内存值。它会发现本地监视器拥有对该段内存的独占标记,但是在全局监视器上CPU1没有了该段内存的独占标记(前面一步清空了),则更新不成功。
  6. 最后,CPU1上的线程2执行STREX指令,试着更新该段内存值。它会发现本地监视器已经没有了对该段内存的独占标记(第4步清除了),则直接更新失败,不需要再查全局监视器了。

所以,可以看出来,这套机制的精髓就是,无论有多少个处理器,有多少个地方会申请对同一个内存段进行操作,保证只有最早的更新可以成功,这之后的更新都会失败。失败了就证明对该段内存有访问冲突了。实际的使用中,可以重新用LDREX读取该段内存中保存的最新值,再处理一次,再尝试保存,直到成功为止。

还有一点需要说明,LDREX和STREX是对内存中的一个字(Word,32 bit)进行独占访问的指令。如果想独占访问的内存区域不是一个字,还有其它的指令: 1)LDREXB和STREXB:对内存中的一个字节(Byte,8 bit)进行独占访问; 2)LDREXH和STREXH:中的一个半字(Half Word,16 bit)进行独占访问; 3)LDREXD和STREXD:中的一个双字(Double Word,64 bit)进行独占访问。

在ARMv8指令集下,LDREX指令被改名成了LDXR指令,而STREX指令被改名成了STXR指令,功能基本上是一样的,除了添加了一个新的特性。当全局监视器标记的对某段内存的独占访问被清空后,将向所有标记了对该段内存独占访问的CPU核都发送事件,将它们从WFE指令中唤醒,继续执行。

0 人点赞