v25.05 鸿蒙内核源码分析(并发并行篇) | 内核如何管理多个CPU? | 百篇博客分析OpenHarmony源码

原创
2021/02/01 14:28
阅读数 8.7K

子曰:“好勇疾贫,乱也。人而不仁,疾之已甚,乱也。” 《论语》:泰伯篇

在这里插入图片描述

百篇博客系列篇.本篇为: v25.xx 鸿蒙内核源码分析(并发并行篇) | 听过无数遍的两个概念

任务管理相关篇为:

在这里插入图片描述

本篇说清楚并发并行

读本篇之前建议先读系列进程/线程篇,会对并行并发更深的理解.

理解并发概念

  • 并发(Concurrent):多个线程在单个核心运行,同一时间只能一个线程运行,内核不停切换线程,看起来像同时运行,实际上是线程被高速的切换.

  • 通俗好理解的比喻就是高速单行道,单行道指的是CPU的核数,跑的车就是线程(任务),进程就是管理车的公司,一个公司可以有很多台车.并发和并行跟CPU的核数有关.车道上同时只能跑一辆车,但因为指挥系统很牛,够快,在毫秒级内就能换车跑,人根本感知不到切换.所以外部的感知会是同时在进行,实现了微观上的串行,宏观上的并行.

  • 线程切换的本质是CPU要换场地上班,去哪里上班由哪里提供场地,那个场地就是任务栈,每个任务栈中保存了上班的各种材料,来了就行立马干活.那些材料就是任务上下文.简单的说就是上次活干到那里了,回来继续接着干.上下文由任务栈自己保存,CPU不管的,它来了只负责任务交过来的材料,材料显示去哪里搬砖它就去哪里搬砖.

记住一个单词就能记住并行并发的区别, 发单,发单(并发单行).

理解并行概念

并行(Parallel)每个线程分配给独立的CPU核心,线程真正的同时运行.

通俗好理解的比喻就是高速多行道,实现了微观和宏观上同时进行. 并行当然是快,人多了干活就不那么累,但干活人多了必然会带来人多的管理问题,会把问题变复杂,请想想会出现哪些问题?

理解协程概念

这里说下协程,例如go语言是有协程支持的,其实协程跟内核层没有关系,是应用层的概念.是在线程之上更高层的封装,用通俗的比喻来说就是在车内另外搞了几条车道玩.其对内核来说没有新东西,内核只负责车的调度,至于车内你想怎么弄那是应用程序自己的事.本质的区别是CPU根本没有换地方上班(没有被调度),而并发/并行都是换地方上班了.

内核如何描述CPU

    typedef struct {
        SortLinkAttribute taskSortLink;             /* task sort link */ //每个CPU core 都有一个task排序链表
        SortLinkAttribute swtmrSortLink;            /* swtmr sort link */ //每个CPU core 都有一个定时器排序链表

        UINT32 idleTaskID;                          /* idle task id */  //空闲任务ID 见于 OsIdleTaskCreate
        UINT32 taskLockCnt;                         /* task lock flag */ //任务锁的数量,当 > 0 的时候,需要重新调度了
        UINT32 swtmrHandlerQueue;                   /* software timer timeout queue id */ //软时钟超时队列句柄
        UINT32 swtmrTaskID;                         /* software timer task id */ //软时钟任务ID

        UINT32 schedFlag;                           /* pending scheduler flag */ //调度标识 INT_NO_RESCH INT_PEND_RESCH
    #if (LOSCFG_KERNEL_SMP == YES)
        UINT32 excFlag;                             /* cpu halt or exc flag */ //CPU处于停止或运行的标识
    #endif
    } Percpu;

    Percpu g_percpu[LOSCFG_KERNEL_CORE_NUM];//全局CPU数组

这是内核对CPU的描述,主要是两个排序链表,一个是任务的排序,一个是定时器的排序.什么意思? 在系列篇中多次提过,任务是内核的调度单元,注意可不是进程,虽然调度也需要进程参与,也需要切换进程,切换用户空间.但调度的核心是切换任务,每个任务的代码指令才是CPU的粮食,它吃的是一条条的指令.每个任务都必须指定取粮地址(即入口函数).

另外还有一个东西能提供入口函数,就是定时任务.很重要也很常用,没它某宝每晚9点的准时秒杀实现不了.在内核每个CPU都有自己独立的任务和定时器链表.

每次Tick的到来,处理函数会去扫描这两个链表,看有没有定时器超时的任务需要执行,有则立即执行定时任务,定时任务是所有任务中优先级最高的,0号优先级,在系列篇中有专门讲定时器任务,可自行翻看.

LOSCFG_KERNEL_SMP

# if (LOSCFG_KERNEL_SMP == YES)
# define LOSCFG_KERNEL_CORE_NUM                          LOSCFG_KERNEL_SMP_CORE_NUM //多核情况下支持的CPU核数
# else
# define LOSCFG_KERNEL_CORE_NUM                          1 //单核配置
# endif

多CPU核的操作系统有3种处理模式(SMP+AMP+BMP) 鸿蒙实现的是 SMP 的方式

  • 非对称多处理(Asymmetric multiprocessing,AMP)每个CPU内核运行一个独立的操作系统或同一操作系统的独立实例(instantiation)。

  • 对称多处理(Symmetric multiprocessing,SMP)一个操作系统的实例可以同时管理所有CPU内核,且应用并不绑定某一个内核。

  • 混合多处理(Bound multiprocessing,BMP)一个操作系统的实例可以同时管理所有CPU内核,但每个应用被锁定于某个指定的核心。

宏LOSCFG_KERNEL_SMP表示对多CPU核的支持,鸿蒙默认是打开LOSCFG_KERNEL_SMP的。

多CPU核支持

鸿蒙内核对CPU的操作见于 los_mp.c ,因文件不大,这里把代码都贴出来了.

    #if (LOSCFG_KERNEL_SMP == YES)
    //给参数CPU发送调度信号
    VOID LOS_MpSchedule(UINT32 target)//target每位对应CPU core 
    {
        UINT32 cpuid = ArchCurrCpuid();
        target &= ~(1U << cpuid);//获取除了自身之外的其他CPU
        HalIrqSendIpi(target, LOS_MP_IPI_SCHEDULE);//向目标CPU发送调度信号,核间中断(Inter-Processor Interrupts),IPI
    }
    //硬中断唤醒处理函数
    VOID OsMpWakeHandler(VOID)
    {
        /* generic wakeup ipi, do nothing */
    }
    //硬中断调度处理函数
    VOID OsMpScheduleHandler(VOID)
    {//将调度标志设置为与唤醒功能不同,这样就可以在硬中断结束时触发调度程序。
        /*
        * set schedule flag to differ from wake function,
        * so that the scheduler can be triggered at the end of irq.
        */
        OsPercpuGet()->schedFlag = INT_PEND_RESCH;//给当前Cpu贴上调度标签
    }
    //硬中断暂停处理函数
    VOID OsMpHaltHandler(VOID)
    {
        (VOID)LOS_IntLock();
        OsPercpuGet()->excFlag = CPU_HALT;//让当前Cpu停止工作

        while (1) {}//陷入空循环,也就是空闲状态
    }
    //MP定时器处理函数, 递归检查所有可用任务
    VOID OsMpCollectTasks(VOID)
    {
        LosTaskCB *taskCB = NULL;
        UINT32 taskID = 0;
        UINT32 ret;

        /* recursive checking all the available task */
        for (; taskID <= g_taskMaxNum; taskID++) { //递归检查所有可用任务
            taskCB = &g_taskCBArray[taskID];

            if (OsTaskIsUnused(taskCB) || OsTaskIsRunning(taskCB)) {
                continue;
            }

            /* 虽然任务状态不是原子的,但此检查可能成功,但无法完成删除,此删除将在下次运行之前处理
            * though task status is not atomic, this check may success but not accomplish
            * the deletion; this deletion will be handled until the next run.
            */
            if (taskCB->signal & SIGNAL_KILL) {//任务收到被干掉信号
                ret = LOS_TaskDelete(taskID);//干掉任务,回归任务池
                if (ret != LOS_OK) {
                    PRINT_WARN("GC collect task failed err:0x%x\n", ret);
                }
            }
        }
    }
    //MP(multiprocessing) 多核处理器初始化
    UINT32 OsMpInit(VOID)
    {
        UINT16 swtmrId;

        (VOID)LOS_SwtmrCreate(OS_MP_GC_PERIOD, LOS_SWTMR_MODE_PERIOD, //创建一个周期性,持续时间为 100个tick的定时器
                            (SWTMR_PROC_FUNC)OsMpCollectTasks, &swtmrId, 0);//OsMpCollectTasks为超时回调函数
        (VOID)LOS_SwtmrStart(swtmrId);//开始定时任务

        return LOS_OK;
    }
    #endif

代码一一都加上了注解,这里再一一说明下:

1.OsMpInit

多CPU核的初始化, 多核情况下每个CPU都有各自的编号, 内核有分成主次CPU, 0号默认为主CPU, OsMain()由主CPU执行,被汇编代码调用. 初始化只开了个定时任务,只干一件事就是回收不用的任务.回收的条件是任务是否收到了被干掉的信号. 例如shell命令 kill 9 14 ,意思是干掉14号线程的信号,这个信号会被线程保存起来. 可以选择自杀也可以等着被杀. 这里要注意,鸿蒙有两种情况下任务不能被干掉, 一种是系统任务不能被干掉的, 第二种是正在运行状态的任务.

2.次级CPU的初始化

同样由汇编代码调用,通过以下函数执行,完成每个CPU核的初始化

    //次级CPU初始化,本函数执行的次数由次级CPU的个数决定. 例如:在四核情况下,会被执行3次, 0号通常被定义为主CPU 执行main
    LITE_OS_SEC_TEXT_INIT VOID secondary_cpu_start(VOID)
    {
    #if (LOSCFG_KERNEL_SMP == YES)
        UINT32 cpuid = ArchCurrCpuid();

        OsArchMmuInitPerCPU();//每个CPU都需要初始化MMU

        OsCurrTaskSet(OsGetMainTask());//设置CPU的当前任务

        /* increase cpu counter */
        LOS_AtomicInc(&g_ncpu); //统计CPU的数量

        /* store each core's hwid */
        CPU_MAP_SET(cpuid, OsHwIDGet());//存储每个CPU的 hwid
        HalIrqInitPercpu(); //CPU硬件中断初始化

        OsCurrProcessSet(OS_PCB_FROM_PID(OsGetKernelInitProcessID())); //设置内核进程为CPU进程
        OsSwtmrInit();  //定时任务初始化,每个CPU维护自己的定时器队列
        OsIdleTaskCreate(); //创建空闲任务,每个CPU维护自己的任务队列
        OsStart(); //本CPU正式启动在内核层的工作
        while (1) {
            __asm volatile("wfi");//wait for Interrupt 等待中断,即下一次中断发生前都在此hold住不干活
        }//类似的还有 WFE: wait for Events 等待事件,即下一次事件发生前都在此hold住不干活
    #endif
    }

可以看出次级CPU有哪些初始化步骤:

  • 初始化MMU,OsArchMmuInitPerCPU

  • 设置当前任务 OsCurrTaskSet

  • 初始化硬件中断 HalIrqInitPercpu

  • 初始化定时器队列 OsSwtmrInit

  • 创建空任务 OsIdleTaskCreate, 外面没有任务的时CPU就待在这个空任务里自己转圈圈.

  • 开始自己的工作流程 OsStart,正式开始工作,跑任务

多CPU核还有哪些问题?

  • CPU之间抢资源的情况要怎么处理?

  • CPU之间通讯(也叫核间通讯)怎么解决?

  • 如果确保两个CPU不会同时执行同一个任务?

  • 汇编代码如何实现对各CPU的调动

请前往系列篇或直接前往内核注解代码查看.这里不再做说明.

百篇博客分析.深挖内核地基

  • 给鸿蒙内核源码加注释过程中,整理出以下文章。内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感,容易理解记忆。说别人能听得懂的话很重要! 百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思。更希望让内核变得栩栩如生,倍感亲切.确实有难度,自不量力,但已经出发,回头已是不可能的了。 😛
  • 与代码有bug需不断debug一样,文章和注解内容会存在不少错漏之处,请多包涵,但会反复修正,持续更新,v**.xx 代表文章序号和修改的次数,精雕细琢,言简意赅,力求打造精品内容。

按功能模块:

基础工具 加载运行 进程管理 编译构建
双向链表 位图管理 用栈方式 定时器 原子操作 时间管理 ELF格式 ELF解析 静态链接 重定位 进程映像 进程管理 进程概念 Fork 特殊进程 进程回收 信号生产 信号消费 Shell编辑 Shell解析 编译环境 编译过程 环境脚本 构建工具 gn应用 忍者ninja
进程通讯 内存管理 前因后果 任务管理
自旋锁 互斥锁 进程通讯 信号量 事件控制 消息队列 内存分配 内存管理 内存汇编 内存映射 内存规则 物理内存 总目录 调度故事 内存主奴 源码注释 源码结构 静态站点 时钟任务 任务调度 任务管理 调度队列 调度机制 线程概念 并发并行 CPU 系统调用 任务切换
文件系统 硬件架构
文件概念 文件系统 索引节点 挂载目录 根文件系统 字符设备 VFS 文件句柄 管道文件 汇编基础 汇编传参 工作模式 寄存器 异常接管 汇编汇总 中断切换 中断概念 中断管理

百万汉字注解.精读内核源码

WeHarmony/kernel_liteos_a_note

鸿蒙研究站 | 每天死磕一点点,原创不易,欢迎转载,但请注明出处。

展开阅读全文
加载中
点击加入讨论🔥(1) 发布并加入讨论🔥
打赏
1 评论
2 收藏
2
分享
返回顶部
顶部