ARM下的自旋锁spinlock

本文讲述的是linux 4.7.1下ARM architecture的自旋锁spinlock的实现。

先来看定义:

include/linux/spinlock_types.h

我们不考虑CONFIG_DEBUG_LOCK_ALLOC, spinlock_t结构体中只有一个成员struct raw_spinlock rlock. struct raw_spinlock也只有一个成员arch_spinlock_t raw_lock.

arch/arm/include/asm/spinlock_types.h

arch_spinlock_t是一个联合体union,slock和ticks占据相同内存。

spinlock_t    lock;       // lock.rlock.raw_lock.slock

我们接着来看spinlock相关的API:

1. spin_lock_init()

这个API用于初始化spinlock变量。

include/linux/spinlock.h

__ARCH_SPIN_LOCK_UNLOCKED依赖于architecture, 对于ARM而言,定义如如下:

arch/arm/include/asm/spinlock_types.h

所以, spin_lock_init(&lock),就相当于lock.rlock.raw_lock.slock = 0, 也即lock.rlock.raw_lock.tickets.owner = 0, lock.rlock.raw_lock.tickets.next = 0.

 

2. spin_lock()

include/linux/spinlock.h

kernel/lockding/spinlock.c

include/linux/spinlock_api_smp.h  (smp系统)

143行,禁内核抢占。为什么要禁这个? 自旋锁的定义就是不能lock时,不断在原地打转,不释放CPU。如果不禁内核抢占,那么当硬件中断(可屏蔽和不可屏蔽中断)发生时,中断返回会触发调度schedule重新调度进程,如果当前进程被调度出去了,就不叫“自旋”了。

include/linux/lockdep.h

故__raw_spin_lock()在145行,先调用do_raw_spin_trylock(),如果返回0,则再调用do_raw_spin_lock().

arch/arm/include/asm/spinlock.h

arch_spin_trylock()中的那段嵌入式汇编:

91-93行,判断lock->tickets.next和lock->tickets.owner是否相等(ror循环右移)。若相同,则表明当前没有cpu进入临界区,然后就是94-95行,将lock->tickets.next+1,否则就退出,返回0(trylock failed).

93行减法的结果保存再contened变量中。所以arch_spin_trylock()成功时,返回1(103行),失败返回0(105行).

arch_spin_trylock()使用了ldrex/strexeq,这个操作和原子变量操作是一样的,95行如果strexeq失败了,res为1,表明操作失败,可能有其他进程或者中断ldrex了这个lock,那么trylock就while(1)继续尝试。

如果在trylock之前,该锁就已经被其他进程给lock了,那么93行的减法结果就不是eq了(即lock->tickets.next != lock->tickets.owner),同时contended为非0值,即addeq和strexeq都不会执行,res为0, while(0)只循环一次,就返回了(return 0, trylock failed)。

arch_spin_trylock()失败时,do_raw_spin_trylock()返回0,则LOCK_CONTENDED()再调用do_raw_spin_lock()。

include/linux/spinlock.h

arch/arm/include/asm/spinlock.h

66-70行,arch_spin_lock() 将lock->tickets.next += 1;

75-78行,这里lockval是个局部变量,它只记录当前cpu的lock->tickets的值 。如果之前已经有其他cpu在使用锁,那么lockval.tickets.next != lockval.tickets.owner,然后wfe,等待占有的cpu去unlock。当占有的cpu unlock时(返回时,会将lock->tickets.owner++),会发送SEV, 当前CPU会从wfe返回,返回后更新lock->tickets.owner到本地lockval.ticket.owner。最后判断是否轮到自己lock了(75行, lockval.tickets.next != lockval.tickets.owner, lock的过程是个FIFO的原则)。

 

3. spin_unlock()

include/linux/spinlock.h

kernel/lockding/spinlock.c

include/linux/spinlock_api_smp.h  (smp系统)

__raw_spin_unlock()在153行调用do_raw_spin_unlock()之后,调用preempt_enable()使能了内核抢占。我们接着看do_raw_spin_unlock().

include/linux/spinlock.h

arch/arm/include/asm/spinlock.h

arch_spin_unlock()和我们之前描述的一样,将lock->tickets.owner++,并发送SEV事件,以唤醒等待lock的cpu.

 

这里,我们提出一个疑问,即当spin_lock()/spin_unlock()也用于中断时会发生?

假设当前进程拥有了spin lock锁,此时lock->tickets.next != lock->tickets.owner, 如果在当前进程unlock之前发生了中断,而中断中也尝试获取spin lock锁,于是中断程序就等待lock->tickets.next == lock->tickets.owner,而当前进程由于被中断了,所以无法unlock这个spin lock锁,这就会造成死锁。

该死锁同样会发生在不同中断源的中断程序共享同一把spin lock锁。

所以,spin_lock()/spin_unlock()用于中断中会有死锁风险(共享spin lock时),为了安全起见,中断中使用spin lock时,应disable irq,可使用中断版本的API: spin_lock_irq() 和spin_unlock_irq().

另外,当我们占有了spinlock后,在unlock之前,这中间的处理代码不能有引发调度程序schedule执行的行为,否则也会引起死锁。

 

还有一个问题,spin_lock()/spin_unlock()是排他性的,当锁被占有时,其他后来者都得排队等候。对于读多写少的临界区来说,显然有些浪费cpu,因为读者可以同时多个,只有写者才需要排队,这样才能最大限度地减少CPU地浪费。

对于这个问题,可以使用读写锁: rwlock_t, read_lock()/write_lock().

读写锁地实现,我们将在读写锁的章节介绍。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值