spinlock 及原子操作实现详解

 

文章转自: http://m.blog.csdn.net/arm7star/article/details/77092650

 

 

 

1、自旋锁结构

typedef struct {
union {
u32 slock;
struct __raw_tickets {
#ifdef __ARMEB__
u16 next; ------ 下一个可以获取自旋锁的处理器,处理器请求自旋锁的时候会保存该值并对该值加1,然后与owner比较,检查是否可以获取到自旋锁,每请求一次next都加1
u16 owner; ------ 当前获取到/可以获取自旋锁的处理器,没释放一次都加1,这样next与owner就保存一致
#else
u16 owner;
u16 next;
#endif
} tickets;
};
} arch_spinlock_t;

2、获取自旋锁

static inline void arch_spin_lock(arch_spinlock_t *lock)
{
unsigned long tmp;
u32 newval;
arch_spinlock_t lockval;


prefetchw(&lock->slock);
__asm__ __volatile__(
"1: ldrex  %0, [%3]\n" ------ lockval = lock->slock (如果lock->slock没有被其他处理器独占,则标记当前执行处理器对lock->slock地址的独占访问;否则不影响)
" add %1, %0, %4\n"------ newval = lockval + (1 << TICKET_SHIFT)
" strex  %2, %1, [%3]\n" ------ strex tmp, newval, [&lock->slock] (如果当前执行处理器没有独占lock->slock地址的访问,不进行存储,返回1;如果当前处理器已经独占lock->slock内存访问,则对内存进行写,返回0,清除独占标记) lock->tickets.next = lock->tickets.next + 1
" teq %2, #0\n"------ 检查是否写入成功lockval.tickets.next
" bne 1b"
: "=&r" (lockval), "=&r" (newval), "=&r" (tmp)
: "r" (&lock->slock), "I" (1 << TICKET_SHIFT)
: "cc");


while (lockval.tickets.next != lockval.tickets.owner) {------ 初始化时lock->tickets.owner、lock->tickets.next都为0,假设第一次执行arch_spin_lock,lockval = *lock,lock->tickets.next++,lockval.tickets.next等于lockval.tickets.owner,获取到自旋锁;自旋锁未释放,第二次执行的时候,lock->tickets.owner = 0, lock->tickets.next = 1,拷贝到lockval后,lockval.tickets.next != lockval.tickets.owner,会执行wfe等待被自旋锁释放被唤醒,自旋锁释放时会执行lock->tickets.owner++,lockval.tickets.owner重新赋值
wfe(); ------ 暂时中断挂起执行,使处理器进入a low-power state等待状态
lockval.tickets.owner = ACCESS_ONCE(lock->tickets.owner);------ 重新读取lock->tickets.owner
}


smp_mb();
}

3、释放自旋锁

static inline void arch_spin_unlock(arch_spinlock_t *lock)
{
smp_mb();
lock->tickets.owner++; ------ lock->tickets.owner增加1,下一个被唤醒的处理器会检查该值是否与自己的lockval.tickets.next相等,lock->tickets.owner代表可以获取的自旋锁的处理器,lock->tickets.next你一个可以获取的自旋锁的owner;处理器获取自旋锁时,会先读取lock->tickets.next用于与lock->tickets.owner比较并且对lock->tickets.next加1,下一个处理器获取到的lock->tickets.next就与当前处理器不一致了,两个处理器都与lock->tickets.owner比较,肯定只有一个处理器会相等,自旋锁释放时时对lock->tickets.owner加1计算,因此,先申请自旋锁多处理器lock->tickets.next值更新,自然先获取到自旋锁
dsb_sev(); ------ 执行sev指令,唤醒wfe等待的处理器
}

 

 

 

 

========================

WFE:

     Wait For Event is a hint instruction that permits the processor to enter a low-power state until one of a number of
events occurs,

     Encoding A1 ARMv6K, ARMv7 (executes as NOP in ARMv6T2)
     WFE <c>

========================

LDREX

     Load Register Exclusive calculates an address from a base register value and an immediate offset, loads a word from

memory, writes it to a register and:

•     if the address has the Shared Memory attribute, marks the physical address as exclusive access for the

      executing processor in a global monitor

•     causes the executing processor to indicate an active exclusive access in the local monitor.

==========================

STREX

     Store Register Exclusive calculates an address from a base register value and an immediate offset, and stores a word

from a register to memory if the executing processor has exclusive access to the memory addressed.

 

 

 

转载自   http://blog.chinaunix.net/uid-20543672-id-3262230.html

ldrex/strex原子操作

前段时间重新研究了一下Linux的并发控制机制,对于内核的自旋锁、互斥锁、信号量等机制及其变体做了底层代码上的研究。因为只有从原理上理解了这些机制,在编写驱动的时候才会记得应该注意什么。这些机制基本都从代码上理解了,但是唯有一个不是非常理解的是内核对于ARM构架中原子变量的底层支持,这个机制其实在自旋锁、互斥锁以及读写锁等内核机制中都有类似的使用。这里将学习的结果写出,请大家指正。

    假设原子变量的底层实现是由一个汇编指令实现的,这个原子性必然有保障。但是如果原子变量的实现是由多条指令组合而成的,那么对于SMP和中断的介入会不会有什么影响呢?我在看ARM的原子变量操作实现的时候,发现其是由多条汇编指令(ldrex/strex)实现的。在参考了别的书籍和资料后,发现大部分书中对这两条指令的描诉都是说他们是支持在SMP系统中实现多核共享内存的互斥访问。但在UP系统中使用,如果ldrex/strex和之间发生了中断,并在中断中也用ldrex/strex操作了同一个原子变量会不会有问题呢?就这个问题,我认真看了一下内核的ARM原子变量源码和ARM官方对于ldrex/strex的功能解释,总结如下:

 

一、ARM构架的原子变量实现结构

    对于ARM构架的原子变量实现源码位于:arch/arm/include/asm/atomic.h

    其主要的实现代码分为ARMv6以上(含v6)构架的实现和ARMv6版本以下的实现。

该文件的主要结构如下:

  1. #if __LINUX_ARM_ARCH__ >= 6
  2.  
  3. ......(通过ldrex/strex指令的汇编实现)
  4.  
  5. #else /* ARM_ARCH_6 */
  6.  
  7. #ifdef CONFIG_SMP
  8. #error SMP not supported on pre-ARMv6 CPUs
  9. #endif
  10.  
  11. ......(通过关闭CPU中断的C语言实现)
  12.  
  13. #endif /* __LINUX_ARM_ARCH__ */
  14. ...... 
  15.  
  16.  #ifndef CONFIG_GENERIC_ATOMIC64
  17.  
  18. ......(通过ldrexd/strexd指令的汇编实现的64bit原子变量的访问)
  19.  
  20. #else /* !CONFIG_GENERIC_ATOMIC64 */
  21.  
  22. #include <asm-generic/atomic64.h>
  23.  
  24. #endif
  25.  
  26. #include <asm-generic/atomic-long.h>

 

      这样的安排是依据ARM核心指令集版本的实现来做的:

(1)在ARMv6以上(含v6)构架有了多核的CPU,为了在多核之间同步数据和控制并发,ARM在内存访问上增加了独占监测(Exclusive monitors)机制(一种简单的状态机),并增加了相关的ldrex/strex指令。请先阅读以下参考资料(关键在于理解local monitor和Global monitor):

 

1.2.2. Exclusive monitors

4.2.12. LDREX  STREX

(2)对于ARMv6以前的构架不可能有多核CPU,所以对于变量的原子访问只需要关闭本CPU中断即可保证原子性。 

 

对于(2),非常好理解。

但是(1)情况,我还是要通过源码的分析才认同这种代码,以下我仅仅分析最具有代表性的atomic_add源码,其他的API原理都一样。如果读者还不熟悉C内嵌汇编的格式,请参考ARM GCC 内嵌汇编手册》

 

二、内核对于ARM构架的atomic_add源码分析

 

  1. /*
  2. * ARMv6 UP 和 SMP 安全原子操作。 我们是用独占载入和
  3. * 独占存储来保证这些操作的原子性。我们可能会通过循环
  4. * 来保证成功更新变量。
  5. */
  6.  
  7. static inline void atomic_add(int i, atomic_t *v)
  8. {
  9. unsigned long tmp;
  10. int result;
  11. __asm__ __volatile__("@ atomic_add\n"
  12. "1: ldrex %0, [%3]\n"
  13. " add %0, %0, %4\n"
  14. " strex %1, %0, [%3]\n"
  15. " teq %1, #0\n"
  16. " bne 1b"
  17. : "=&r" (result), "=&r" (tmp), "+Qo" (v->counter)
  18. : "r" (&v->counter), "Ir" (i)
  19. : "cc");
  20. }

 

源码分析: 

注意:根据内联汇编的语法,result、tmp、&v->counter对应的数据都放在了寄存器中操作。如果出现上下文切换,切换机制会做寄存器上下文保护。

(1)ldrex %0, [%3]

意思是将&v->counter指向的数据放入result中,并且(分别在Local monitor和Global monitor中)设置独占标志。

(2)add %0, %0, %4

result = result + i

(3)strex %1, %0, [%3]

意思是将result保存到&v->counter指向的内存中,此时 Exclusive monitors会发挥作用,将保存是否成功的标志放入tmp中。

(4) teq %1, #0

测试strex是否成功(tmp == 0 ??)

(5)bne 1b

如果发现strex失败,从(1)再次执行。

通过上面的分析,可知关键在于strex的操作是否成功的判断上。而这个就归功于ARM的Exclusive monitors和ldrex/strex指令的机制。以下通过可能的情况分析ldrex/strex指令机制。(请阅读时参考4.2.12. LDREX  STREX

 

1、UP系统或SMP系统中变量为非CPU间共享访问的情况 

    此情况下,仅有一个CPU可能访问变量,此时仅有Local monitor需要关注。

    假设CPU执行到(2)的时候,来了一个中断,并在中断里使用ldrex/strex操作了同一个原子变量。则情况如下图所示:

 

  • A:处理器标记一个物理地址,但访问尚未完毕
  • B:再次标记此物理地址访问尚未完毕(与A重复)
  • C:进行存储操作,清除以上标记,返回0(操作成功)
  • D:不会进行存储操作,并返回1(操作失败) 

 

也就是说,中断例程里的操作会成功,被中断的操作会失败重试。 

 

2、SMP系统中变量为CPU间共享访问的情况

 

    此情况下,需要两个CPU间的互斥访问,此时ldrex/strex指令会同时关注Local monitor和Global monitor。

(i)两个CPU同时访问同个原子变量(ldrex/strex指令会关注Global monitor。)

 

  • A:将该物理地址标记为CPU0独占访问,并清除CPU0对其他任何物理地址的任何独占访问标记。
  • B:标记此物理地址为CPU1独占访问,并清除CPU1对其他任何物理地址的任何独占访问标记。
  • C:没有标记为CPU0独占访问,不会进行存储,并返回1(操作失败)。
  • D:已被标记为CPU1独占访问,进行存储并清除独占访问标记,并返回0(操作成功)。

 

也就是说,后执行ldrex操作的CPU会成功。

 

(ii)同一个CPU因为中断,“嵌套”访问同个原子变量(ldrex/strex指令会关注Local monito)

 

  • A:将该物理地址标记为CPU0独占访问,并清除CPU0对其他任何物理地址的任何独占访问标记。
  • B:再次标记此物理地址为CPU0独占访问,并清除CPU0对其他任何物理地址的任何独占访问标记。
  • C:已被标记为CPU0独占访问,进行存储并清除独占访问标记,并返回0(操作成功)。
  • D:没有标记为CPU0独占访问,不会进行存储,并返回1(操作失败)。

 

也就是说,中断例程里的操作会成功,被中断的操作会失败重试。

 

(iii)两个CPU同时访问同个原子变量,并同时有CPU因中断“嵌套”访问改原子变量(ldrex/strex指令会同时关注Local monitor和Global monitor)

虽然对于人来说,这种情况比较BT。但是在飞速运行的CPU来说,BT的事情随时都可能发生。

 

  • A:将该物理地址标记为CPU0独占访问,并清除CPU0对其他任何物理地址的任何独占访问标记。
  • B:标记此物理地址为CPU1独占访问,并清除CPU1对其他任何物理地址的任何独占访问标记。
  • C:再次标记此物理地址为CPU0独占访问,并清除CPU0对其他任何物理地址的任何独占访问标记。
  • D:已被标记为CPU0独占访问,进行存储并清除独占访问标记,并返回0(操作成功)。
  • E:没有标记为CPU1独占访问,不会进行存储,并返回1(操作失败)。
  • F:没有标记为CPU0独占访问,不会进行存储,并返回1(操作失败)。

 

 

    当然还有其他许多复杂的可能,也可以通过ldrex/strex指令的机制分析出来。从上面列举的分析中,我们可以看出:ldrex/strex可以保证在任何情况下(包括被中断)的访问原子性。所以内核中ARM构架中的原子操作是可以信任的。

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值