0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

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

电子设计 来源:my.oschina 作者:鸿蒙内核源码分析 2021-04-25 14:18 次阅读

内核中哪些地方会用到自旋锁?看图:

pIYBAGCFCHaAKalfAAEbq5iNP4k545.png

自旋锁顾名思义,是一把自动旋转的锁,这很像厕所里的锁,进入前标记是绿色可用的,进入格子间后,手一带,里面的锁转个圈,外面标记变成了红色表示在使用,外面的只能等待.这是形象的比喻,但实际也是如此.

在多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_SpinLockLOS_SpinTrylockLOS_SpinLockSave申请指定的自旋锁,申请成功就继续往后执行锁保护的代码;申请失败在自旋锁申请中忙等,直到申请到自旋锁为止。

释放自旋锁:使用LOS_SpinUnlockLOS_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 spinlock
task2 got spinlock
task1 try to get spinlock
task2 release spinlock
task1 got spinlock
task1 release spinlock

总结

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

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

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

编辑:hfy

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • cpu
    cpu
    +关注

    关注

    68

    文章

    10824

    浏览量

    211126
  • 鸿蒙系统
    +关注

    关注

    183

    文章

    2634

    浏览量

    66211
  • 自旋锁
    +关注

    关注

    0

    文章

    11

    浏览量

    1579
收藏 人收藏

    评论

    相关推荐

    深度解析自旋自旋的实现方案

    入场券自旋和MCS自旋都属于排队自旋(queued spinlock),进程按照申请
    发表于 09-19 11:39 4368次阅读
    深度解析<b class='flag-5'>自旋</b><b class='flag-5'>锁</b>及<b class='flag-5'>自旋</b><b class='flag-5'>锁</b>的实现方案

    Linux驱动开发笔记-自旋和信号量

    (&lock, flags);.//关闭中断保存中断状态到flags,然后获取自旋www.arm8.net 嵌入式论坛释放:spin_unlock_irq(&lock);或者
    发表于 08-30 18:08

    Linux内核同步机制的自旋原理是什么?

    自旋是专为防止多处理器并发而引入的一种,它在内核中大量应用于中断处理等部分(对于单处理器来说,防止中断处理的并发可简单采用关闭中断的方
    发表于 03-31 08:06

    开源的鸿蒙系统其他手机厂商会用吗?

    转眼间就来到了2020年下半年,而备受大家关注的华为鸿蒙OS系统,发布距今也有一年多的时间了,华为鸿蒙OS系统迎来2.0版本,被应用到PC、
    发表于 09-24 10:42

    哪些地方用到差分线?

    什么是差分线差分线有什么用哪些地方用到差分线高速差分线设计的硬件要求
    发表于 02-25 06:26

    怎么在atmega128实现自旋

    什么是自旋?有哪些缺陷?怎么在atmega128实现自旋
    发表于 01-24 06:54

    Linux内核同步机制的自旋原理

    一、自旋 自旋是专为防止多处理器并发而引入的一种,它在内核中大量应用于中断处理等部分(对
    发表于 06-08 14:50 1302次阅读

    信号量和自旋

    量应用于中断处理等部分(对于单处理器来说,防止中断处理的并发可简单采用关闭中断的方式,不需要自旋)。    自旋最多只能被一个
    发表于 04-02 14:43 798次阅读

    Linux 自旋spinlock

    ,所以同一时刻只能有一个任务获取到内核当发生访问资源冲突的时候,通常有两种处理方式: 一个是原地等待 一个是挂起当前进程,调度其他进程执行(睡眠) 自旋 Spinlock 是
    的头像 发表于 09-11 14:36 2047次阅读

    自旋的发展历史与使用方法

    自旋是Linux内核里最常用的之一,自旋的概念很简单,就是如果加锁失败在等
    的头像 发表于 08-08 08:51 1677次阅读

    自旋和互斥的区别有哪些

    自旋 自旋与互斥很相似,在访问共享资源之前对自旋
    的头像 发表于 07-21 11:19 9417次阅读

    如何用C++11实现自旋

    )不同之处在于当自旋尝试获取时以忙等待(busy waiting)的形式不断地循环检查是否可用。 在多CPU的环境, 对持有
    的头像 发表于 11-11 16:48 1378次阅读
    如何用C++11实现<b class='flag-5'>自旋</b><b class='flag-5'>锁</b>

    互斥自旋的区别 自旋临界区可以被中断吗?

    互斥自旋的区别 自旋临界区可以被中断吗? 互斥
    的头像 发表于 11-22 17:41 774次阅读

    自旋和互斥的使用场景是什么

    制,它在等待的过程,线程会不断地检查的状态,直到被释放。自旋适用于以下场景: 1.1
    的头像 发表于 07-10 10:05 896次阅读

    互斥自旋的实现原理

    互斥自旋是操作系统中常用的同步机制,用于控制对共享资源的访问,以避免多个线程或进程同时访问同一资源,从而引发数据不一致或竞争条件等问题。 互斥
    的头像 发表于 07-10 10:07 406次阅读