ldrex简介
在Armv6开始支持多核,通过ldrex与strex指令来保证数据操作的原子性,比如自旋锁的上锁操作、原子变量操作等。在Armv6之前,都是单核,为保证数据的原子性,需要进行关中断操作。对于多核平台,关中断操作只能关闭本核中断,要想对数据进行原子操作,必须借助ldrex指令与strex。对于ldrex指令与strex指令原理这里不在介绍,网上资料很多,下面简单介绍SylixOS的队列式自旋锁实现。
static VOID armSpinLock (SPINLOCKTYPE *psld, VOIDFUNCPTR pfuncPoll, PVOID pvArg)
{
#if __SYLIXOS_ARM_ARCH__ >= 6
UINT32 uiTemp;
UINT32 uiNewVal;
SPINLOCKTYPE sldVal;
ARM_PREFETCH(&psld->SLD_uiLock);
__asm__ __volatile__(
"1: ldrex %[oldvalue], [%[slock]] \n"
" add %[newvalue], %[oldvalue], %[tshift] \n"
" strex %[temp], %[newvalue], [%[slock]] \n"
" teq %[temp], #0 \n"
" bne 1b"
: [oldvalue] "=&r" (sldVal), [newvalue] "=&r" (uiNewVal), [temp] "=&r" (uiTemp)
: [slock] "r" (&psld->SLD_uiLock), [tshift] "I" (1 << LW_SPINLOCK_TICKET_SHIFT)
: "cc");
while (sldVal.SLD_usTicket != sldVal.SLD_usSvcNow) {
if (pfuncPoll) {
pfuncPoll(pvArg);
} else {
__asm__ __volatile__(ARM_WFE(""));
}
sldVal.SLD_usSvcNow = LW_ACCESS_ONCE(UINT16, psld->SLD_usSvcNow);
}
#endif /* __SYLIXOS_ARM_ARCH__ >= 6 */
}
为避免线程在获取自旋锁时出现饥饿状态,在实现自旋锁时按照先到先得的机制。主要实现思路就是每个线程在获取自旋锁时,会对SLD_usTicket加1,释放时对SLD_usSvcNow加1,当两个值相等时,代表获取到锁。假设有三个线程A、B、C,访问锁的顺序依次为A -> B -> C,起初SLD_usTicket和SLD_usSvcNow都为0,线程A首先获取到锁的值,存储在sldVal中,此时sldVal中SLD_usTicket和SLD_usSvcNow都为0。注意,这里是先将锁的值保存到sldVal中,然后对锁的SLD_usTicket加1。这样,当线程B获取锁时,sldVal中的SLD_usTicket为1,然后也对锁的SLD_usTicket加1,当线程C获取锁时,sldVal中的SLD_usTicket为2。当线程A释放锁时,SLD_usSvcNow的值才会加1,此时线程B才可以获取锁,这样就保证了按队列方式获取锁。因为在自旋的过程中,不会发生任务切换,所以至多有CPU核数数量的线程同时获取锁,不会导致SLD_usTicket加1溢出时会和先到者冲突。
对于自旋锁的释放则比较简单:
static VOID armSpinUnlock (SPINLOCKTYPE *psld)
{
#if __SYLIXOS_ARM_ARCH__ >= 6
psld->SLD_usSvcNow++;
armDsb();
__asm__ __volatile__(ARM_SEV);
#endif /* __SYLIXOS_ARM_ARCH__ >= 6 */
}
ldrex使用问题
ldrex与strex指令的结合确实强大,对于一些古老的多核处理器,需要锁总线来保证数据原子操作(如x86),这样会导致访问效率降低。而ldrex和strex没有进行锁总线操作,并且在两条指令之间可以对变量进行复杂的操作,不仅仅是加1减1操作。但是也需要注意ldrex与strex指令要想正常工作,也是有前提的,笔者在开发ti 66AH2H(4核cortex-A15)平台BSP时,就遇到了这个问题,先看下ARM手册对于ldrex指令的相关说明:
上述内容来源于ARM官方文档(DDI0438I_cortex_a15_r4p0_trm.pdf),主要表达意思就是对于ldrex和strex指令的支持需要global monitor,而global monitor的实现由两种,一种是内部实现,需要开启cache。另一种是外部实现(和芯片有关系,有的芯片没有实现),通过总线监听的方式。当内部和外部都实现时,优先使用内部global monitor。笔者在开发ti 66AH2H(4核cortex-A15)平台BSP时,就在没有开启cache时使用了ldrex指令,系统工作异常,这也说明了66AH2H没有实现外部global monitor。同样是ti cortex-A15(AM5728),就实现了外部global monitor,zynq 7000双核cortex-A9(Armv7-A)也可以在不开启cache时使用ldrex指令,外部global monitor是否实现与芯片有关系,为安全起见,尽量不在开启cache前使用ldrex和strex指令。