美文网首页鸿蒙(HarmonyOS)开发知识@IT·互联网
鸿蒙内核源码分析 (并发并行篇) | 内核如何管理多个 CPU?

鸿蒙内核源码分析 (并发并行篇) | 内核如何管理多个 CPU?

作者: 迪士尼在逃程序员 | 来源:发表于2024-08-10 19:50 被阅读0次

    理解并发概念

    • 并发(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,正式开始工作,跑任务

    写在最后

    • 如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
    • 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
    • 关注小编,同时可以期待后续文章ing🚀,不定期分享原创知识。
    • 想要获取更多完整鸿蒙最新学习知识点,请移步前往小编:https://gitee.com/MNxiaona/733GH/blob/master/jianshu

    相关文章

      网友评论

        本文标题:鸿蒙内核源码分析 (并发并行篇) | 内核如何管理多个 CPU?

        本文链接:https://www.haomeiwen.com/subject/rlkztjtx.html