鸿蒙系统内核中哪些地方会用到自旋锁?

内核中哪些地方会用到自旋锁?看图:
自旋锁顾名思义,是一把自动旋转的锁,这很像厕所里的锁,进入前标记是绿色可用的,进入格子间后,手一带,里面的锁转个圈,外面标记变成了红色表示在使用,外面的只能等待.这是形象的比喻,但实际也是如此.
在多cpu核环境中,由于使用相同的内存空间,存在对同一资源进行访问的情况,所以需要互斥访问机制来保证同一时刻只有一个核进行操作,自旋锁就是这样的一种机制。
自旋锁是指当一个线程在获取锁时,如果锁已经被其它cpu中的线程获取,那么该线程将循环等待,并不断判断是否能够成功获取锁,直到其它cpu释放锁后,等锁cpu才会退出循环。
自旋锁的设计理念是它仅会被持有非常短的时间,锁只能被一个任务持有,而且持有自旋锁的cpu是不可以进入睡眠模式的,因为其他的cpu在等待锁,为了防止死锁上下文交换也是不允许的,是禁止发生调度的.
自旋锁与互斥锁比较类似,它们都是为了解决对共享资源的互斥使用问题。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个持有者。但是两者在调度机制上略有不同,对于互斥锁,如果锁已经被占用,锁申请者会被阻塞;但是自旋锁不会引起调用者阻塞,会一直循环检测自旋锁是否已经被释放。
虽然都是共享资源竞争,但自旋锁强调的是cpu核间的竞争,而互斥量强调的是任务(包括同一cpu核)之间的竞争.
自旋锁长什么样?
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语言代码中找不到变量的变化过程,而是通过一段汇编代码来实现.看完本篇会明白也只能通过汇编代码来实现自旋锁.
自旋锁使用流程
自旋锁用于多cpu核的情况,解决的是cpu之间竞争资源的问题.使用流程很简单,三步走。
创建自旋锁:使用los_spininit初始化自旋锁,或者使用spin_lock_init初始化静态内存的自旋锁。
申请自旋锁:使用接口los_spinlock los_spintrylock los_spinlocksave申请指定的自旋锁,申请成功就继续往后执行锁保护的代码;申请失败在自旋锁申请中忙等,直到申请到自旋锁为止。
释放自旋锁:使用los_spinunlock los_spinunlockrestore接口释放自旋锁。锁保护代码执行完毕后,释放对应的自旋锁,以便其他核申请自旋锁。
几个关键函数
自旋锁模块由内联函数实现,见于los_spinlock.h 代码不多,主要是三个函数.
archspinlock(&lock->rawlock);archspintrylock(&lock->rawlock)archspinunlock(&lock->rawlock); 可以说掌握了它们就掌握了自旋锁,但这三个函数全由汇编实现.见于los_dispatch.s文件 因为系列篇已有两篇讲过汇编代码,所以很容易理解这三段代码.函数的参数由r0记录,即r0保存了lock->rawlock的地址,拿锁/释放锁是让lock->rawlock在0,1切换 下面逐一说明自旋锁的汇编代码.
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 @跳回调用archspinlock函数 比较两段汇编代码可知,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 @跳回调用archspinlock函数 代码中涉及到几个不常用的汇编指令,一一说明:
汇编指令之 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的流程是:
开始之初资源空闲
cpu核1 访问资源,持有锁,获得资源
cpu核2 访问资源,此时资源不空闲,执行wfe指令,让core进入low-power state(睡眠)
cpu核1 释放资源,释放锁,释放资源,同时执行sev指令,唤醒cpu核2
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指令更新的时候,会发现独占标记已经被清空了,就不能再更新了,从而实现独占访问的机制。
编程实例
本实例实现如下流程。
任务example_taskentry初始化自旋锁,创建两个任务example_spintask1、example_spintask2,分别运行于两个核。
example_spintask1、example_spintask2中均执行申请自旋锁的操作,同时为了模拟实际操作,在持有自旋锁后进行延迟操作,最后释放自旋锁。
300tick后任务example_taskentry被调度运行,删除任务example_spintask1和example_spintask2。
#include los_spinlock.h#include los_task.h/* 自旋锁句柄id */spin_lock_s g_testspinlock;/* 任务id */uint32 g_testtaskid01;uint32 g_testtaskid02;void example_spintask1(void){ uint32 i; uintptr intsave; /* 申请自旋锁 */ dprintf(task1 try to get spinlock\n); los_spinlocksave(&g_testspinlock, &intsave); dprintf(task1 got spinlock\n); for(i = 0; i < 5000; i++) { asm volatile(nop); } /* 释放自旋锁 */ dprintf(task1 release spinlock\n); los_spinunlockrestore(&g_testspinlock, intsave); return;}void example_spintask2(void){ uint32 i; uintptr intsave; /* 申请自旋锁 */ dprintf(task2 try to get spinlock\n); los_spinlocksave(&g_testspinlock, &intsave); dprintf(task2 got spinlock\n); for(i = 0; i < 5000; i++) { asm volatile(nop); } /* 释放自旋锁 */ dprintf(task2 release spinlock\n); los_spinunlockrestore(&g_testspinlock, intsave); return;}uint32 example_taskentry(void){ uint32 ret; tsk_init_param_s sttask1; tsk_init_param_s sttask2; /* 初始化自旋锁 */ los_spininit(&g_testspinlock); /* 创建任务1 */ memset(&sttask1, 0, sizeof(tsk_init_param_s)); sttask1.pfntaskentry = (tsk_entry_func)example_spintask1; sttask1.pcname = spintsk1; sttask1.uwstacksize = loscfg_task_min_stack_size; sttask1.ustaskprio = 5;#ifdef loscfg_kernel_smp /* 绑定任务到cpu0运行 */ sttask1.uscpuaffimask = cpuid_to_affi_mask(0);#endif ret = los_taskcreate(&g_testtaskid01, &sttask1); if(ret != los_ok) { dprintf(task1 create failed .\n); return los_nok; } /* 创建任务2 */ memset(&sttask2, 0, sizeof(tsk_init_param_s)); sttask2.pfntaskentry = (tsk_entry_func)example_spintask2; sttask2.pcname = spintsk2; sttask2.uwstacksize = loscfg_task_min_stack_size; sttask2.ustaskprio = 5;#ifdef loscfg_kernel_smp /* 绑定任务到cpu1运行 */ sttask1.uscpuaffimask = cpuid_to_affi_mask(1);#endif ret = los_taskcreate(&g_testtaskid02, &sttask2); if(ret != los_ok) { dprintf(task2 create failed .\n); return los_nok; } /* 任务休眠300ticks */ los_taskdelay(300); /* 删除任务1 */ ret = los_taskdelete(g_testtaskid01); if(ret != los_ok) { dprintf(task1 delete failed .\n); return los_nok; } /* 删除任务2 */ ret = los_taskdelete(g_testtaskid02); if(ret != los_ok) { dprintf(task2 delete failed .\n); return los_nok; } return los_ok;}运行结果task2 try to get spinlocktask2 got spinlocktask1 try to get spinlocktask2 release spinlocktask1 got spinlocktask1 release spinlock 总结
自旋锁用于解决cpu核间竞争资源的问题
因为自旋锁会让cpu陷入睡眠状态,所以锁的代码不能太长,否则容易导致意外出现,也影响性能.
必须由汇编代码实现,因为c语言写不出让cpu进入真正睡眠,核间竞争的代码.


测量示波器底噪并理解示波器的三大关键指标
新思科技葛群:EDA让芯片赋能万物 深耕本土市场的全球化之路
中科创达发力物联网领域,同时布局无人机解决方案
通过嵌入式AI解决方案来深挖终端计算性能
盘点:2016年VR市场的发展现状
鸿蒙系统内核中哪些地方会用到自旋锁?
为什么虚拟机比容器更安全
传感器加载连接装置是决定电子秤性能的重要环节之一
英特尔卖掉闪存业务是明智的选择
如何使用波形分集雷达抑制风力涡轮机干扰
智能门锁上装载的dToF接近传感器作用
澳大利亚电信将与爱立信合作在墨尔本板球场部署5G网络
诚邀加入 | 润和软件联合牵头成立全国智能物联行业产教融合共同体
铅酸电池和锂电池可以共用充电器吗?
首款搭载华为鸿蒙系统的第三方设备开卖
什么是激光雷达?LiDAR系统的工作原理及解决方案
汽车OBU介绍
台工研院积极推动CIMS平台
纳芯微增强型数字隔离芯片上市,融合Adaptive OOK技术
Galaxy S9+拆解:独立模块化设计+高度集成高通骁龙845芯片