自旋锁(Spinlock)的底层实现原理

源码参考自鸿蒙内核 v26.08的自旋锁实现源码。

spinlock概念

  • 自旋锁是指当一个线程在获取锁时,如果锁已经被其它CPU中的线程获取,那么该线程将循环等待,并不断判断是否能够成功获取锁,直到其它CPU释放锁后,等锁CPU才会退出循环。

  • 自旋锁的设计理念是它仅会被持有非常短的时间,锁只能被一个任务持有,而且持有自旋锁的CPU是不可以进入睡眠模式的,因为其他的CPU在等待锁,为了防止死锁上下文交换也是不允许的,是禁止发生调度的.

  • 自旋锁与互斥锁比较类似,它们都是为了解决对共享资源的互斥使用问题。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个持有者。但是两者在调度机制上略有不同,对于互斥锁,如果锁已经被占用,锁申请者会被阻塞;但是自旋锁不会引起调用者阻塞,会一直循环检测自旋锁是否已经被释放。

虽然都是共享资源竞争,但自旋锁强调的是CPU核间(即多核)的竞争,而互斥量强调的是任务(包括同一CPU核)之间的竞争.

spinlock结构体

    typedef struct Spinlock {//自旋锁结构体
        size_t      rawLock;//原始锁
    #if (LOSCFG_KERNEL_SMP_LOCKDEP == YES) // 死锁检测模块开关
        UINT32      cpuid; //持有锁的CPU
        VOID        *owner; //持有锁任务
        const CHAR  *name; //锁名称
    #endif
    } SPIN_LOCK_S;

结构体很简单,里面有个宏,用于死锁检测,默认情况下是关闭的.所以真正的被使用的变量只有rawLock一个.自旋锁通常通过汇编代码来实现自旋锁,因为其他语言都无法真正做到让CPU休眠,可能只是让CPU再循环里等待,也有一定的资源消耗。

几个关键函数

这三个函数全由汇编实现,函数的参数由r0记录,即r0保存了lock->rawLock的地址,拿锁/释放锁是让lock->rawLock在0,1切换。

ArchSpinLock(&lock->rawLock);
 FUNCTION(ArchSpinLock)  @死守,非要拿到锁
        mov     r1, #1      @r1=1
    1:                      @循环的作用,因SEV是广播事件.不一定lock->rawLock的值已经改变了
        ldrex   r2, [r0]    @r0 = &lock->rawLock, 即 r2 = lock->rawLock,并标记独占访问
        cmp     r2, #0      @r2和0比较
        wfene               @不相等时,说明资源被占用,也就是被其他资源标记为1,CPU核进入睡眠状态
        strexeq r2, r1, [r0]@此时CPU被重新唤醒,尝试令lock->rawLock=1(加锁),成功写入则r2=0
        cmpeq   r2, #0      @再来比较r2是否等于0,如果相等则获取到了锁
        bne     1b          @如果不相等,继续进入循环
        dmb                 @用DMB指令来隔离,以保证缓冲中的数据已经落实到RAM中
        bx      lr          @此时是一定拿到锁了,跳回调用ArchSpinLock函数

ArchSpinLock会在循环里一直等待lock->rawLock = 0的广播事件发生。能让CPU进入睡眠的只能通过汇编实现.C语言根本就写不出让CPU真正睡眠的代码.

LDREX用来读取内存中的值,并标记对该段内存的独占访问:
LDREX Rx, [Ry]
上面的指令意味着,读取寄存器Ry指向的4字节内存值,将其保存到Rx寄存器中,同时标记对Ry指向内存区域的独占访问。
如果执行LDREX指令的时候发现已经被标记为独占访问了,并不会对指令的执行产生影响。
而STREX在更新内存数值时,会检查该段内存是否已经被标记为独占访问,并以此来决定是否更新内存中的值:
STREX Rx, Ry, [Rz]
如果执行这条指令的时候发现已经被标记为独占访问了,则将寄存器Ry中的值更新到寄存器Rz指向的内存,并将寄存器Rx设置成0。指令执行成功后,会将独占访问标记位清除。
而如果执行这条指令的时候发现没有设置独占标记,则不会更新内存,且将寄存器Rx的值设置成1。
一旦某条STREX指令执行成功后,以后再对同一段内存尝试使用STREX指令更新的时候,会发现独占标记已经被清空了,就不能再更新了,从而实现独占访问的机制。

ArchSpinTrylock(&lock->rawLock);
    FUNCTION(ArchSpinTrylock)   @尝试拿锁,拿不到就撤
        mov     r1, #1          @r1=1
        mov     r2, r0          @r2 = r0       
        ldrex   r0, [r2]        @r2 = &lock->rawLock, 即 r0 = lock->rawLock
        cmp     r0, #0          @r0和0比较
        strexeq r0, r1, [r2]    @尝试令lock->rawLock=1,成功写入则r0=0,否则 r0 =1
        dmb                     @数据存储隔离,以保证缓冲中的数据已经落实到RAM中
        bx      lr              @跳回调用ArchSpinLock函数

ArchSpinTrylock即没有循环也不会让CPU进入睡眠,直接返回了

ArchSpinUnlock(&lock->rawLock);
    FUNCTION(ArchSpinUnlock)    @释放锁
        mov     r1, #0          @r1=0               
        dmb                     @数据存储隔离,以保证缓冲中的数据已经落实到RAM中
        str     r1, [r0]        @令lock->rawLock = 0
        dsb                     @数据同步隔离
        sev                     @给各CPU广播事件,唤醒沉睡的CPU们
        bx      lr              @跳回调用ArchSpinLock函数

总结

  • 自旋锁用于解决CPU核间竞争资源的问题

  • 因为自旋锁会让CPU陷入睡眠状态,所以锁的代码不能太长,否则容易导致意外出现,也影响性能.

  • 必须由汇编代码实现,因为C语言写不出让CPU进入真正睡眠,核间竞争的代码.


    本文部分内容参考于网络,如有侵权请私信联系我删除

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值