-
Notifications
You must be signed in to change notification settings - Fork 68
42_自旋锁篇
本篇关键词:、、、
下载 >> 离线文档.鸿蒙内核源码分析(百篇博客分析.挖透鸿蒙内核).pdf
通讯机制相关篇为:
- v41.04 鸿蒙内核源码分析(通讯总览) | 内核跟人一样都喜欢八卦
- v42.08 鸿蒙内核源码分析(自旋锁) | 死等丈夫归来的贞洁烈女
- v43.05 鸿蒙内核源码分析(互斥锁) | 有你没她 相安无事
- v44.02 鸿蒙内核源码分析(快锁使用) | 用户态负责快锁逻辑
- v45.02 鸿蒙内核源码分析(快锁实现) | 内核态负责快锁调度
- v46.01 鸿蒙内核源码分析(读写锁) | 内核如何实现多读单写
- v47.05 鸿蒙内核源码分析(信号量) | 谁在解决任务间的同步
- v48.07 鸿蒙内核源码分析(事件机制) | 多对多任务如何同步
- v49.05 鸿蒙内核源码分析(信号生产) | 年过半百 活力十足
- v50.03 鸿蒙内核源码分析(信号消费) | 谁让CPU连续四次换栈运行
- v51.03 鸿蒙内核源码分析(消息队列) | 进程间如何异步传递大数据
- v52.02 鸿蒙内核源码分析(消息封装) | 剖析LiteIpc(上)进程通讯内容
- v53.01 鸿蒙内核源码分析(消息映射) | 剖析LiteIpc(下)进程通讯机制
- v54.01 鸿蒙内核源码分析(共享内存) | 进程间最快通讯方式
读本篇之前建议先读系列篇 进程/线程篇。
内核中哪些地方会用到自旋锁?看图:
自旋锁
顾名思义,是一把自动旋转的锁,这很像厕所里的锁,进入前标记是绿色可用的,进入格子间后,手一带,里面的锁转个圈,外面标记变成了红色表示在使用,外面的只能等待。这是形象的比喻,但实际也是如此。
在多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切换
下面逐一说明自旋锁的汇编代码。
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
和 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
立一个贞节牌坊!
FUNCTION(ArchSpinUnlock) @释放锁
mov r1, #0 @r1=0
dmb @数据存储隔离,以保证缓冲中的数据已经落实到RAM中
str r1, [r0] @令lock->rawLock = 0
dsb @数据同步隔离
sev @给各CPU广播事件,唤醒沉睡的CPU们
bx lr @跳回调用ArchSpinLock函数
代码中涉及到几个不常用的汇编指令,一一说明:
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
用来读取内存中的值,并标记对该段内存的独占访问:
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 spinlock
task2 got spinlock
task1 try to get spinlock
task2 release spinlock
task1 got spinlock
task1 release spinlock
-
自旋锁用于解决CPU核间竞争资源的问题
-
因为自旋锁会让CPU陷入睡眠状态,所以锁的代码不能太长,否则容易导致意外出现,也影响性能。
-
必须由汇编代码实现,因为C语言写不出让CPU进入真正睡眠,核间竞争的代码。
- 百文相当于摸出内核的肌肉和器官系统,让人开始丰满有立体感,因是直接从注释源码起步,在加注释过程中,每每有心得处就整理,慢慢形成了以下文章。内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感,容易理解记忆。说别人能听得懂的话很重要! 百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思。更希望让内核变得栩栩如生,倍感亲切。
- 与代码需不断
debug
一样,文章内容会存在不少错漏之处,请多包涵,但会反复修正,持续更新,v**.xx
代表文章序号和修改的次数,精雕细琢,言简意赅,力求打造精品内容。 - 百文在 < 鸿蒙研究站 | 开源中国 | 博客园 | 51cto | csdn | 知乎 | 掘金 > 站点发布,百篇博客系列目录如下。
按功能模块:
基础知识 | 进程管理 | 任务管理 | 内存管理 |
---|---|---|---|
双向链表 内核概念 源码结构 地址空间 计时单位 优雅的宏 钩子框架 位图管理 POSIX main函数 | 调度故事 进程控制块 进程空间 线性区 红黑树 进程管理 Fork进程 进程回收 Shell编辑 Shell解析 | 任务控制块 并发并行 就绪队列 调度机制 任务管理 用栈方式 软件定时器 控制台 远程登录 协议栈 | 内存规则 物理内存 内存概念 虚实映射 页表管理 静态分配 TLFS算法 内存池管理 原子操作 圆整对齐 |
通讯机制 | 文件系统 | 硬件架构 | 内核汇编 |
通讯总览 自旋锁 互斥锁 快锁使用 快锁实现 读写锁 信号量 事件机制 信号生产 信号消费 消息队列 消息封装 消息映射 共享内存 | 文件概念 文件故事 索引节点 VFS 文件句柄 根文件系统 挂载机制 管道文件 文件映射 写时拷贝 | 芯片模式 ARM架构 指令集 协处理器 工作模式 寄存器 多核管理 中断概念 中断管理 | 编码方式 汇编基础 汇编传参 链接脚本 内核启动 进程切换 任务切换 中断切换 异常接管 缺页中断 |
编译运行 | 调测工具 | ||
编译过程 编译构建 GN语法 忍者无敌 ELF格式 ELF解析 静态链接 重定位 动态链接 进程映像 应用启动 系统调用 VDSO | 模块监控 日志跟踪 系统安全 测试用例 |
-
百万汉字注解内核目的是要看清楚其毛细血管,细胞结构,等于在拿放大镜看内核。内核并不神秘,带着问题去源码中找答案是很容易上瘾的,你会发现很多文章对一些问题的解读是错误的,或者说不深刻难以自圆其说,你会慢慢形成自己新的解读,而新的解读又会碰到新的问题,如此层层递进,滚滚向前,拿着放大镜根本不愿意放手。
期间不断得到小伙伴的支持,有学生,有职场新人,也有老江湖,在此一并感谢,大家的支持是前进的动力。尤其每次收到学生的赞助很感慨,后生可敬。 >> 查看捐助名单
据说喜欢 点赞 + 分享 的,后来都成了大神。:)