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

871 篇文章 15 订阅
694 篇文章 18 订阅

理解并发概念

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

为了能让大家更好的学习鸿蒙(HarmonyOS NEXT)开发技术,这边特意整理了《鸿蒙开发学习手册》(共计890页),希望对大家有所帮助:https://qr21.cn/FV7h05

《鸿蒙开发学习手册》:

如何快速入门:https://qr21.cn/FV7h05

  1. 基本概念
  2. 构建第一个ArkTS应用
  3. ……

开发基础知识:https://qr21.cn/FV7h05

  1. 应用基础知识
  2. 配置文件
  3. 应用数据管理
  4. 应用安全管理
  5. 应用隐私保护
  6. 三方应用调用管控机制
  7. 资源分类与访问
  8. 学习ArkTS语言
  9. ……

基于ArkTS 开发:https://qr21.cn/FV7h05

  1. Ability开发
  2. UI开发
  3. 公共事件与通知
  4. 窗口管理
  5. 媒体
  6. 安全
  7. 网络与链接
  8. 电话服务
  9. 数据管理
  10. 后台任务(Background Task)管理
  11. 设备管理
  12. 设备使用信息统计
  13. DFX
  14. 国际化开发
  15. 折叠屏系列
  16. ……

鸿蒙开发面试真题(含参考答案):https://qr18.cn/F781PH

鸿蒙开发面试大盘集篇(共计319页):https://qr18.cn/F781PH

1.项目开发必备面试题
2.性能优化方向
3.架构方向
4.鸿蒙开发系统底层方向
5.鸿蒙音视频开发方向
6.鸿蒙车载开发方向
7.鸿蒙南向开发方向

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值