v04.03 鸿蒙内核源码分析(任务调度) | 任务是内核调度的单元 原创
子谓公冶长,“可妻也。虽在缧绁之中,非其罪也”。以其子妻之。《论语》:公冶长篇
百篇博客系列篇.本篇为:
v04.xx 鸿蒙内核源码分析(任务调度篇) | 任务是内核调度的单元
任务管理相关篇为:
- v03.06 鸿蒙内核源码分析(时钟任务) | 触发调度谁的贡献最大
- v04.03 鸿蒙内核源码分析(任务调度) | 任务是内核调度的单元
- v05.05 鸿蒙内核源码分析(任务管理) | 任务池是如何管理的
- v06.03 鸿蒙内核源码分析(调度队列) | 内核有多少个调度队列
- v07.08 鸿蒙内核源码分析(调度机制) | 任务是如何被调度执行的
- v21.07 鸿蒙内核源码分析(线程概念) | 是谁在不断的折腾CPU
- v25.05 鸿蒙内核源码分析(并发并行) | 听过无数遍的两个概念
- v32.03 鸿蒙内核源码分析(CPU) | 整个内核就是一个死循环
- v37.06 鸿蒙内核源码分析(系统调用) | 开发者永远的口头禅
- v41.03 鸿蒙内核源码分析(任务切换) | 看汇编如何切换任务
任务即线程
在鸿蒙内核中,广义上可理解为一个任务就是一个线程
官方是怎么描述线程的
基本概念
从系统的角度看,线程是竞争系统资源的最小运行单元。线程可以使用或等待CPU、使用内存空间等系统资源,并独立于其它线程运行。
鸿蒙内核每个进程内的线程独立运行、独立调度,当前进程内线程的调度不受其它进程内线程的影响。
鸿蒙内核中的线程采用抢占式调度机制,同时支持时间片轮转调度和FIFO调度方式。
鸿蒙内核的线程一共有32个优先级(0-31),最高优先级为0,最低优先级为31。
当前进程内高优先级的线程可抢占当前进程内低优先级线程,当前进程内低优先级线程必须在当前进程内高优先级线程阻塞或结束后才能得到调度。
线程状态说明:
初始化(Init):该线程正在被创建。
就绪(Ready):该线程在就绪列表中,等待CPU调度。
运行(Running):该线程正在运行。
阻塞(Blocked):该线程被阻塞挂起。Blocked状态包括:pend(因为锁、事件、信号量等阻塞)、suspend(主动pend)、delay(延时阻塞)、pendtime(因为锁、事件、信号量时间等超时等待)。
退出(Exit):该线程运行结束,等待父线程回收其控制块资源。
图 1 线程状态迁移示意图
注意官方文档说的是线程,没有提到task(任务),但内核源码中却有大量 task代码,很少有线程(thread)代码 ,这是怎么回事?
其实在鸿蒙内核中, task就是线程, 初学者完全可以这么理解,但二者还是有区别,否则干嘛要分两个词描述。
会有什么区别?是管理上的区别,task是调度层面的概念,线程是进程层面的概念。 就像同一个人在不同的管理体系中会有不同的身份一样,一个男人既可以是 孩子,爸爸,丈夫,或者程序员,视角不同功能也会不同。
如何证明是一个东西,继续再往下看。
执行task命令
看shell task 命令的执行结果:
task命令 查出每个任务在生命周期内的运行情况,它运行的内存空间,优先级,时间片,入口执行函数,进程ID,状态等等信息,非常的复杂。这么复杂的信息就需要一个结构体来承载。而这个结构体就是 LosTaskCB(任务控制块)
对应张大爷的故事:task就是一个用户的节目清单里的一个节目,用户总清单就是一个进程,所以上面会有很多的节目。
task长得什么样子
说LosTaskCB之前先说下官方文档任务状态对应的 define,可以看出task和线程是一个东西。
LosTaskCB长什么样?抱歉,它确实有点长,但还是要全部贴出全貌。
结构体LosTaskCB内容很多,各代表什么含义?
LosTaskCB相当于任务在内核中的身份证,它反映出每个任务在生命周期内的运行情况。既然是周期就会有状态,要运行就需要内存空间,就需要被内核算法调度,被选中CPU就去执行代码段指令,CPU要执行就需要告诉它从哪里开始执行,因为是多线程,但只有一个CPU就需要不断的切换任务,那执行会被中断,也需要再恢复后继续执行,又如何保证恢复的任务执行不会出错,这些问题都需要说明白。
Task怎么管理
什么是任务池?
前面已经说了任务是内核调度层面的概念,调度算法保证了task有序的执行,调度机制详见其他姊妹篇的介绍。
如此多的任务怎么管理和执行?管理靠任务池和就绪队列,执行靠调度算法。
代码如下(OsTaskInit):
g_taskCBArray 就是个任务池,默认创建128个任务,常驻内存,不被释放。
g_losFreeTask是空闲任务链表,想创建任务时来这里申请一个空闲任务,用完了就回收掉,继续给后面的申请使用。
g_taskRecyleList是回收任务链表,专用来回收exit 任务,任务所占资源被确认归还后被彻底删除,就像员工离职一样,得有个离职队列和流程,要归还电脑,邮箱,有没有借钱要还的 等操作。
对应张大爷的故事:用户要来场馆领取表格填节目单,场馆只准备了128张表格,领完就没有了,但是节目表演完了会回收表格,这样多了一张表格就可以给其他人领取了,这128张表格对应鸿蒙内核这就是任务池,简单吧。
就绪队列是怎么回事
CPU执行速度是很快的,鸿蒙内核默认一个时间片是 10ms, 资源有限,需要在众多任务中来回的切换,所以绝不能让CPU等待任务,CPU就像公司最大的领导,下面很多的部门等领导来审批,吃饭。只有大家等领导,哪有领导等你们的道理,所以工作要提前准备好,每个部门的优先级又不一样,所以每个部门都要有个任务队列,里面放的是领导能直接处理的任务,没准备好的不要放进来,因为这是给CPU提前准备好的粮食!
这就是就绪队列的原理,一共有32个就绪队列,进程和线程都有,因为线程的优先级是默认32个, 每个队列中放同等优先级的task.
还是看源码吧
注意看g_priQueueList 的内存分配,就是32个LOS_DL_LIST,还记得LOS_DL_LIST的妙用吗,不清楚的去 鸿蒙系统源码分析(总目录)里面翻。
对应张大爷的故事:就是门口那些排队的都是至少有一个节目单是符合表演标准的,资源都到位了,没有的连排队的资格都木有,就慢慢等吧。
任务栈是怎么回事
每个任务都是独立开的,任务之间也相互独立,之间通讯通过IPC,这里的“独立”指的是每个任务都有自己的运行环境 —— 栈空间,称为任务栈,栈空间里保存的信息包含局部变量、寄存器、函数参数、函数返回地址等等
但系统中只有一个CPU,任务又是独立的,调度的本质就是CPU执行一个新task,老task在什么地方被中断谁也不清楚,是随机的。那如何保证老任务被再次调度选中时还能从上次被中断的地方继续玩下去呢?
答案是:任务上下文,CPU内有一堆的寄存器,CPU运行本质的就是这些寄存器的值不断的变化,只要切换时把这些值保存起来,再还原回去就能保证task的连续执行,让用户毫无感知。鸿蒙内核给一个任务执行的时间是 20ms ,也就是说有多任务竞争的情况下,一秒钟内最多要来回切换50次。
对应张大爷的故事:就是碰到节目没有表演完就必须打断的情况下,需要把当时的情况记录下来,比如小朋友在演躲猫猫的游戏,一半不演了,张三正在树上,李四正在厕所躲,都记录下来,下次再回来你们上次在哪就会哪呆着去,就位了继续表演。这样就接上了,观众就木有感觉了。
任务上下文(TaskContext)是怎样的呢?还是直接看源码
发现基本都是CPU寄存器的恢复现场值, 具体各寄存器有什么作用大家可以去网上详查,后续也有专门的文章来介绍。这里说其中的三个寄存器 SP, LR, PC
LR
用途有二,一是保存子程序返回地址,当调用BL、BX、BLX等跳转指令时会自动保存返回地址到LR;二是保存异常发生的异常返回地址。
PC(Program Counter)
为程序计数器,用于保存程序的执行地址,在ARM的三级流水线架构中,程序流水线包括取址、译码和执行三个阶段,PC指向的是当前取址的程序地址,所以32位ARM中,译码地址(正在解析还未执行的程序)为PC-4,执行地址(当前正在执行的程序地址)为PC-8, 当突然发生中断的时候,保存的是PC的地址。
SP
每一种异常模式都有其自己独立的r13,它通常指向异常模式所专用的堆栈,当ARM进入异常模式的时候,程序就可以把一般通用寄存器压入堆栈,返回时再出栈,保证了各种模式下程序的状态的完整性。
任务栈初始化
任务栈的初始化就是任务上下文的初始化,因为任务没开始执行,里面除了上下文不会有其他内容,注意上下文存放的位置在栈的底部。初始状态下 sp就是指向的栈底, 栈顶内容永远是 0xCCCCCCCC “烫烫烫烫”,这几个字应该很熟悉吗? 如果不是那几个字了,那说明栈溢出了, 后续篇会详细说明这块,大家也可以自行去看代码,很有意思.
Task函数集
使用场景和功能
任务创建后,内核可以执行锁任务调度,解锁任务调度,挂起,恢复,延时等操作,同时也可以设置任务优先级,获取任务优先级。任务结束的时候,则进行当前任务自删除操作。
Huawei LiteOS 系统中的任务管理模块为用户提供下面几种功能。
功能分类 | 接口名 | 描述 |
---|---|---|
任务的创建和删除 | LOS_TaskCreateOnly | 创建任务,并使该任务进入suspend状态,并不调度。 |
LOS_TaskCreate | 创建任务,并使该任务进入ready状态,并调度。 | |
LOS_TaskDelete | 删除指定的任务。 | |
任务状态控制 | LOS_TaskResume | 恢复挂起的任务。 |
LOS_TaskSuspend | 挂起指定的任务。 | |
LOS_TaskDelay | 任务延时等待。 | |
LOS_TaskYield | 显式放权,调整指定优先级的任务调度顺序。 | |
任务调度的控制 | LOS_TaskLock | 锁任务调度。 |
LOS_TaskUnlock | 解锁任务调度。 | |
任务优先级的控制 | LOS_CurTaskPriSet | 设置当前任务的优先级。 |
LOS_TaskPriSet | 设置指定任务的优先级。 | |
LOS_TaskPriGet | 获取指定任务的优先级。 | |
任务信息获取 | LOS_CurTaskIDGet | 获取当前任务的ID。 |
LOS_TaskInfoGet | 设置指定任务的优先级。 | |
LOS_TaskPriGet | 获取指定任务的信息。 | |
LOS_TaskStatusGet | 获取指定任务的状态。 | |
LOS_TaskNameGet | 获取指定任务的名称。 | |
LOS_TaskInfoMonitor | 监控所有任务,获取所有任务的信息。 | |
LOS_NextTaskIDGet | 获取即将被调度的任务的ID。 |
创建任务的过程
创建任务之前先了解另一个结构体 tagTskInitParam
这些初始化参数是外露的任务初始参数,pfnTaskEntry 对java来说就是你new进程的run(),
需要上层使用者提供.
看个例子吧:shell中敲 ping 命令看下它创建的过程
发现ping的调度优先级是8,比shell 还高,那shell的是多少?答案是:看源码是 9
关于shell后续会详细介绍,请持续关注。
前置条件了解清楚后,具体看任务是如何一步步创建的,如何和进程绑定,加入调度就绪队列,还是继续看源码
对应张大爷的故事:就是节目单要怎么填,按格式来,从哪里开始演,要多大的空间,王场馆好协调好现场的环境。这里注意 在同一个节目单只要节目没演完,王场馆申请场地的空间就不能给别人用,这个场地空间对应的就是鸿蒙任务的栈空间,除非整个节目单都完了,就回收了。把整个场地干干净净的留给下一个人的节目单来表演。
至此的创建已经完成,已各就各位,源码最后还申请了一次LOS_Schedule();因为鸿蒙的调度方式是抢占式的,如何本次task的任务优先级高于其他就绪队列,那么接下来要执行的任务就是它了!
百万汉字注解.精读内核源码
百篇博客分析.深挖内核地基
给鸿蒙内核源码加注释过程中,整理出以下文章。内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感,容易理解记忆。说别人能听得懂的话很重要! 百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思。更希望让内核变得栩栩如生,倍感亲切.确实有难度,自不量力,但已经出发,回头已是不可能的了。 😛
与代码有bug需不断debug一样,文章和注解内容会存在不少错漏之处,请多包涵,但会反复修正,持续更新,.xx
代表修改的次数,精雕细琢,言简意赅,力求打造精品内容。
基础工具>> 双向链表 | 位图管理 | 用栈方式 | 定时器 | 原子操作 | 时间管理 |
加载运行>> ELF格式 | ELF解析 | 静态链接 | 重定位 | 进程映像 |
进程管理>> 进程管理 | 进程概念 | Fork | 特殊进程 | 进程回收 | 信号生产 | 信号消费 | Shell编辑 | Shell解析 |
编译构建>> 编译环境 | 编译过程 | 环境脚本 | 构建工具 | gn应用 | 忍者ninja |
进程通讯>> 自旋锁 | 互斥锁 | 进程通讯 | 信号量 | 事件控制 | 消息队列 |
内存管理>> 内存分配 | 内存管理 | 内存汇编 | 内存映射 | 内存规则 | 物理内存 |
前因后果>> 总目录 | 调度故事 | 内存主奴 | 源码注释 | 源码结构 | 静态站点 |
任务管理>> 时钟任务 | 任务调度 | 任务管理 | 调度队列 | 调度机制 | 线程概念 | 并发并行 | CPU | 系统调用 | 任务切换 |
文件系统>> 文件概念 | 文件系统 | 索引节点 | 挂载目录 | 根文件系统 | 字符设备 | VFS | 文件句柄 | 管道文件 |
硬件架构>> 汇编基础 | 汇编传参 | 工作模式 | 寄存器 | 异常接管 | 汇编汇总 | 中断切换 | 中断概念 | 中断管理 |
鸿蒙研究站 | 每天死磕一点点,原创不易,欢迎转载,但请注明出处。
这个大宝藏(此系列)会被越来越多人发现
感谢支持,但愿如此!