自旋锁(Spinlock)的底层实现原理
源码参考自鸿蒙内核 v26.08的自旋锁实现源码。
源码参考自鸿蒙内核 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进入真正睡眠,核间竞争的代码.
本文部分内容参考于网络,如有侵权请私信联系我删除
更多推荐
所有评论(0)