鸿蒙内核源码分析(自旋锁篇) | 汇编到令人心碎的自旋锁 | 中文注解HarmonyOS源码 | v26.01
鸿蒙内核源码中文注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆, 四大源码仓每日同步更新< Gitee| Github| CSDN| Coding >
鸿蒙内核源码分析博客 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点每日同步更新< CSDN| 开源中国| WeHarmony >
本篇说清楚自旋锁
读本篇之前建议先读鸿蒙内核源码分析(总目录)进程+线程篇.
概述
自旋锁顾名思义,是一把自动旋转的锁,这很像厕所里锁,进入前标记是绿色可用的,进入格子间后,手一带,里面的锁转个圈,外面标记变成了红色表示在使用,外面的只能等待.这是形象的比喻,但实际也是如此.
在多CPU核环境中,由于使用相同的内存空间,存在对同一资源进行访问的情况,所以需要互斥访问机制来保证同一时刻只有一个核进行操作,自旋锁就是这样的一种机制。
自旋锁是指当一个线程在获取锁时,如果锁已经被其它线程获取,那么该线程将循环等待,并不断判断是否能够成功获取锁,直到获取到锁才会退出循环。因此建议保护耗时较短的操作,防止对系统整体性能有显的影响。
自旋锁与互斥锁比较类似,它们都是为了解决对共享资源的互斥使用问题。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个持有者。但是两者在调度机制上略有不同,对于互斥锁,如果锁已经被占用,锁申请者会被阻塞;但是自旋锁不会引起调用者阻塞,会一直循环检测自旋锁是否已经被释放。
虽然都是共享资源竞争,但自旋锁强调的是CPU核间的竞争,而互斥量强调的是任务(包括同一CPU核)之间的竞争.
自旋锁使用流程
自旋锁用于多CPU核的情况,解决的是CPU之间竞争资源的问题.使用流程很简单,三步走。
创建自旋锁:使用LOS_SpinInit初始化自旋锁,或者使用SPIN_LOCK_INIT初始化静态内存的自旋锁。
申请自旋锁:使用接口LOS_SpinLock/LOS_SpinTrylock/LOS_SpinLockSave申请指定的自旋锁,申请成功就继续往后执行锁保护的代码;申请失败在自旋锁申请中忙等,直到申请到自旋锁为止。
释放自旋锁:使用LOS_SpinUnlock/LOS_SpinUnlockRestore接口释放自旋锁。锁保护代码执行完毕后,释放对应的自旋锁,以便其他核申请自旋锁。
自旋锁长什么样?
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一个.但C语言代码中找不到变量的变化过程,而是通过一段汇编代码来实现.看完本篇会明白也只能通过汇编代码来实现自旋锁.
几个关键C函数
#if (LOSCFG_KERNEL_SMP == YES) //多CPU核情况下讨论自旋锁才有意义
//申请指定的自旋锁,如果无法获取锁,会一直循环等待
LITE_OS_SEC_ALW_INLINE STATIC INLINE VOID LOS_SpinLock(SPIN_LOCK_S *lock)
{
LOS_TaskLock();//1.先告诉CPU记录有个任务上了锁
LOCKDEP_CHECK_IN(lock);//2.检查自旋锁
ArchSpinLock(&lock->rawLock);//3.自旋锁工作主体,一段汇编代码
LOCKDEP_RECORD(lock);//4.记录自旋锁
}
//尝试申请指定的自旋锁,如果无法获取锁,直接返回失败,而不会一直循环等待
LITE_OS_SEC_ALW_INLINE STATIC INLINE INT32 LOS_SpinTrylock(SPIN_LOCK_S *lock)
{
LOS_TaskLock();
INT32 ret = ArchSpinTrylock(&lock->rawLock);
if (ret == LOS_OK) {
LOCKDEP_CHECK_IN(lock);
LOCKDEP_RECORD(lock);
} else {
LOS_TaskUnlock();
}
return ret;
}
//释放指定的自旋锁
LITE_OS_SEC_ALW_INLINE STATIC INLINE VOID LOS_SpinUnlock(SPIN_LOCK_S *lock)
{
LOCKDEP_CHECK_OUT(lock);
ArchSpinUnlock(&lock->rawLock);//注意ArchSpinUnlock是一个汇编函数 见于 los_dispatch.s
/* restore the scheduler flag */ //恢复调度标签
LOS_TaskUnlock();
}
#endif
代码也是很简单,真正涉及自旋锁的部分就是这三个函数.
ArchSpinLock(&lock->rawLock);
ArchSpinTrylock(&lock->rawLock)
ArchSpinUnlock(&lock->rawLock);
可以说掌握了这三个函数就掌握了自旋锁,但这三个函数全由汇编实现.见于los_dispatch.S文件 因为系列篇已有两篇讲过汇编代码,所以很容易理解这三段代码.函数的参数此时由 r0记录,即r0保存了lock->rawLock的地址. 下面逐一说明自旋锁的汇编代码.
ArchSpinLock 汇编代码
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 @不相等时,说明资源被占用,CPU核进入睡眠状态
strexeq r2, r1, [r0]@此时CPU被重新唤醒,尝试令lock->rawLock=1,成功写入则r2=0
cmpeq r2, #0 @再来比较r2是否等于0,如果相等则获取到了锁
bne 1b @如果不相等,继续进入循环
dmb @用DMB指令来隔离,以保证缓冲中的数据已经落实到RAM中
bx lr @此时是一定拿到锁了,跳回调用ArchSpinLock函数
看懂了这段汇编代码就理解了自旋锁实现的真正机制,为什么一定要用汇编来实现. 因为CPU宁愿睡眠也非拿要到锁不可的, 注意这里可不是让线程睡眠,而是让CPU进入睡眠状态,能让CPU进入睡眠的只能通过汇编实现.C语言根本就写不出让CPU真正睡眠的代码.
ArchSpinTrylock 汇编代码
如果不看下面这段汇编代码,你根本不可能知道 ArchSpinTrylock 和 ArchSpinLock的真正区别是什么.
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 @跳回调用ArchSpinTrylock函数
比较两段汇编代码可知,ArchSpinTrylock即没有循环也不会让CPU进入睡眠,直接返回了,而ArchSpinLock会睡了醒, 醒了睡,一直守到丈夫( lock->rawLock = 0的广播事件发生)回来才肯罢休. 笔者注释到这段代码时,那真是心潮澎湃, 心碎了老一地, 真想给 ArchSpinLock 立一个贞节牌坊 !
ArchSpinUnlock 汇编代码
FUNCTION(ArchSpinUnlock) @释放锁
mov r1, #0 @r1=0
dmb @数据存储隔离,以保证缓冲中的数据已经落实到RAM中
str r1, [r0] @令lock->rawLock = 0
dsb @数据同步隔离
sev @给各CPU广播事件,唤醒沉睡的CPU们
bx lr @跳回调用ArchSpinUnlock函数
}
代码中涉及到几个不常用的汇编指令,一一说明:
汇编指令之 WFI / WFE / SEV
WFI(Wait for interrupt):等待中断到来指令. WFI一般用于cpuidle,WFI 指令是在处理器发生中断或类似异常之前不需要做任何事情。
在鸿蒙源码分析系列篇(总目录)线程篇中已说过,每个CPU都有自己的idle任务,CPU没事干的时候就待在里面,就一个死循环守着WFI指令,有中断来了就触发CPU起床干活. 中断分硬中断和软中断,系统调用就是通过软中断实现的,而设备类的就属于硬中断,都能触发CPU干活. 具体看下CPU空闲的时候在干嘛,代码超级简单:
LITE_OS_SEC_TEXT WEAK VOID OsIdleTask(VOID) //CPU没事干的时候待在这里
{
while (1) {//只有一个死循环
Wfi();//WFI指令:arm core 立即进入low-power standby state,等待中断,进入休眠模式。
}
}
WFE(Wait for event):等待事件的到来指令.WFE 指令是在SEV指令生成事件之前不需要执行任何操作,所以用WFE的地方,后续一定会对应一个SEV的指令去唤醒它. WFE的一个典型使用场景,是用在自旋锁中,spinlock的功能,是在不同CPU core之间,保护共享资源。使用WFE的流程是:
1.开始之初资源空闲
2.CPU核1 访问资源,持有锁,获得资源
3.CPU核2 访问资源,此时资源不空闲,执行WFE指令,让core进入low-power state(睡眠)
4.CPU核1 释放资源,释放锁,释放资源,同时执行SEV指令,唤醒CPU核2
5.CPU核2 获得资源
另外说一下 以往的自旋锁,在获得不到资源时,让CPU核进入死循环,而通过插入WFE指令,则大大节省功耗.
SEV(send event):发送事件指令,SEV是一条广播指令,它会将事件发送到多处理器系统中的所有处理器,以唤醒沉睡的CPU.
SEV和WFE的实现很像设计模式的观察者模式.
汇编指令之 LDREX / STREX
LDREX用来读取内存中的值,并标记对该段内存的独占访问:
LDREX Rx, [Ry] 上面的指令意味着,读取寄存器Ry指向的4字节内存值,将其保存到Rx寄存器中,同时标记对Ry指向内存区域的独占访问。
如果执行LDREX指令的时候发现已经被标记为独占访问了,并不会对指令的执行产生影响。
而STREX在更新内存数值时,会检查该段内存是否已经被标记为独占访问,并以此来决定是否更新内存中的值:
STREX Rx, Ry, [Rz] 如果执行这条指令的时候发现已经被标记为独占访问了,则将寄存器Ry中的值更新到寄存器Rz指向的内存,并将寄存器Rx设置成0。指令执行成功后,会将独占访问标记位清除。
而如果执行这条指令的时候发现没有设置独占标记,则不会更新内存,且将寄存器Rx的值设置成1。
一旦某条STREX指令执行成功后,以后再对同一段内存尝试使用STREX指令更新的时候,会发现独占标记已经被清空了,就不能再更新了,从而实现独占访问的机制。
总结
-
自旋锁用于解决CPU核间竞争资源的问题
-
因为自旋锁会让CPU陷入睡眠状态,所以锁的代码不能太长,否则容易导致意外出现,也影响性能.
-
必须由汇编代码实现,因为C语言写不出让CPU进入真正睡眠,核间竞争的代码.
喜欢就请收藏吧
各大站点搜 "鸿蒙内核源码分析" ,快速找到组织.
鸿蒙内核源码中文注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆, 四大源码仓每日同步更新< Gitee| Github| CSDN| Coding >
鸿蒙内核源码分析博客 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点每日同步更新< CSDN| 开源中国| WeHarmony >

