开源鸿蒙内核源码分析系列 | 任务管理|老师如何管熊孩子上篇(转载)
官方是怎么描述线程的?
在开源鸿蒙内核中,广义上可理解为一个任务就是一个线程。
基本概念:
从系统的角度看,线程是竞争系统资源的最小运行单元。线程可以使用或等待CPU、使用内存空间等系统资源,并独立于其它线程运行。
开源鸿蒙内核每个进程内的线程独立运行、独立调度,当前进程内线程的调度不受其它进程内线程的影响。
开源鸿蒙内核中的线程采用抢占式调度机制,同时支持时间片轮转调度和FIFO调度方式。
开源鸿蒙内核的线程一共有32个优先级(0-31),最高优先级为0,最低优先级为31。
当前进程内高优先级的线程可抢占当前进程内低优先级线程,当前进程内低优先级线程必须在当前进程内高优先级线程阻塞或结束后才能得到调度。
线程状态说明:
初始化(Init):该线程正在被创建。
就绪(Ready):该线程在就绪列表中,等待CPU调度。
运行(Running):该线程正在运行。
阻塞(Blocked):该线程被阻塞挂起。Blocked状态包括:pend(因为锁、事件、信号量等阻塞)、suspend(主动pend)、delay(延时阻塞)、pendtime(因为锁、事件、信号量时间等超时等待)。
退出(Exit):该线程运行结束,等待父线程回收其控制块资源。
注意官方文档说的是线程,没有提到task(任务),但内核源码中却有大量 task代码,很少有线程(thread)代码 ,这是怎么回事?
其实在鸿蒙内核中, task就是线程, 初学者完全可以这么理解,但二者还是有区别,否则干嘛要分两个词描述?
会有什么区别?是管理上的区别。task是调度层面的概念,线程是进程层面的概念。就像同一个人在不同的管理体系中会有不同的身份一样,一个男人既可以是 孩子,爸爸,丈夫,或者程序员,视角不同功能也会不同。
如何证明是一个东西,继续再往下看。
执行task命令
看shell task 命令的执行结果:
ask命令 查出每个任务在生命周期内的运行情况,它运行的内存空间,优先级,时间片,入口执行函数,进程ID,状态等等信息,非常的复杂。这么复杂的信息就需要一个结构体来承载。而这个结构体就是 LosTaskCB(任务控制块)。
对应张大爷的故事:task就是一个用户的节目清单里的一个节目,用户总清单就是一个进程,所以上面会有很多的节目。
task长得什么样子?
说LosTaskCB之前先说下官方文档任务状态对应的 define,可以看出task和线程是一个东西。
#define OS_TASK_STATUS_INIT 0x0001U
#define OS_TASK_STATUS_READY 0x0002U
#define OS_TASK_STATUS_RUNNING 0x0004U
#define OS_TASK_STATUS_SUSPEND 0x0008U
#define OS_TASK_STATUS_PEND 0x0010U
#define OS_TASK_STATUS_DELAY 0x0020U
#define OS_TASK_STATUS_TIMEOUT 0x0040U
#define OS_TASK_STATUS_PEND_TIME 0x0080U
#define OS_TASK_STATUS_EXIT 0x0100U
LosTaskCB长什么样?抱歉,它确实有点长,但还是要全部贴出全貌。
typedef struct {
VOID *stackPointer; /**< Task stack pointer | 内核栈指针位置(SP) */
UINT16 taskStatus; /**< Task status | 各种状态标签,可以拥有多种标签,按位标识 */
UINT16 priority; /**< Task priority | 任务优先级[0:31],默认是31级 */
UINT16 policy; ///< 任务的调度方式(三种 .. LOS_SCHED_RR LOS_SCHED_FIFO .. )
UINT64 startTime; /**< The start time of each phase of task | 任务开始时间 */
UINT64 irqStartTime; /**< Interrupt start time | 任务中断开始时间 */
UINT32 irqUsedTime; /**< Interrupt consumption time | 任务中断消耗时间 */
UINT32 initTimeSlice; /**< Task init time slice | 任务初始的时间片 */
INT32 timeSlice; /**< Task remaining time slice | 任务剩余时间片 */
UINT32 waitTimes; /**< Task delay time, tick number | 设置任务调度延期时间 */
SortLinkList sortList; /**< Task sortlink node | 跟CPU捆绑的任务排序链表节点,上面挂的是就绪队列的下一个阶段,进入CPU要执行的任务队列 */
UINT32 stackSize; /**< Task stack size | 内核态栈大小,内存来自内核空间 */
UINTPTR topOfStack; /**< Task stack top | 内核态栈顶 bottom = top + size */
UINT32 taskID; /**< Task ID | 任务ID,任务池本质是一个大数组,ID就是数组的索引,默认 < 128 */
TSK_ENTRY_FUNC taskEntry; /**< Task entrance function | 任务执行入口地址 */
VOID *joinRetval; /**< pthread adaption | 用来存储join线程的入口地址 */
VOID *taskMux; /**< Task-held mutex | task在等哪把锁 */
VOID *taskEvent; /**< Task-held event | task在等哪个事件 */
UINTPTR args[4]; /**< Parameter, of which the maximum number is 4 | 入口函数的参数 例如 main (int argc,char *argv[]) */
CHAR taskName[OS_TCB_NAME_LEN]; /**< Task name | 任务的名称 */
LOS_DL_LIST pendList; /**< Task pend node | 如果任务阻塞时就通过它挂到各种阻塞情况的链表上,比如OsTaskWait时 */
LOS_DL_LIST threadList; /**< thread list | 挂到所属进程的线程链表上 */
UINT32 eventMask; /**< Event mask | 任务对哪些事件进行屏蔽 */
UINT32 eventMode; /**< Event mode | 事件三种模式(LOS_WAITMODE_AND,LOS_WAITMODE_OR,LOS_WAITMODE_CLR) */
UINT32 priBitMap; /**< BitMap for recording the change of task priority,the priority can not be greater than 31
| 任务在执行过程中优先级会经常变化,这个变量用来记录所有曾经变化过的优先级,例如 ..01001011 曾经有过 0,1,3,6 优先级 */
#ifdef LOSCFG_KERNEL_CPUP
OsCpupBase taskCpup; /**< task cpu usage | CPU 使用统计 */
#endif
INT32 errorNo; /**< Error Num | 错误序号 */
UINT32 signal; /**< Task signal | 任务信号类型,(SIGNAL_NONE,SIGNAL_KILL,SIGNAL_SUSPEND,SIGNAL_AFFI) */
sig_cb sig; ///< 信号控制块,用于异步通信,类似于 linux singal模块
#ifdef LOSCFG_KERNEL_SMP
UINT16 currCpu; /**< CPU core number of this task is running on | 正在运行此任务的CPU内核号 */
UINT16 lastCpu; /**< CPU core number of this task is running on last time | 上次运行此任务的CPU内核号 */
UINT16 cpuAffiMask; /**< CPU affinity mask, support up to 16 cores | CPU亲和力掩码,最多支持16核,亲和力很重要,多核情况下尽量一个任务在一个CPU核上运行,提高效率 */
#ifdef LOSCFG_KERNEL_SMP_TASK_SYNC //多核情况下的任务同步开关,采用信号量实现
UINT32 syncSignal; /**< Synchronization for signal handling | 用于CPU之间同步信号量 */
#endif
#ifdef LOSCFG_KERNEL_SMP_LOCKDEP //SMP死锁检测开关
LockDep lockDep; ///< 死锁依赖检测
#endif
#endif
#ifdef LOSCFG_SCHED_DEBUG //调试调度开关
SchedStat schedStat; /**< Schedule statistics | 调度统计 */
#endif
UINTPTR userArea; ///< 用户空间的堆区开始位置
UINTPTR userMapBase; ///< 用户空间的栈顶位置,内存来自用户空间,和topOfStack有本质的区别.
UINT32 userMapSize; /**< user thread stack size ,real size : userMapSize + USER_STACK_MIN_SIZE | 用户栈大小 */
UINT32 processID; /**< Which belong process | 所属进程ID */
FutexNode futex; ///< 实现快锁功能
LOS_DL_LIST joinList; /**< join list | 联结链表,允许任务之间相互释放彼此 */
LOS_DL_LIST lockList; /**< Hold the lock list | 该链表上挂的都是已持有的锁 */
UINTPTR waitID; /**< Wait for the PID or GID of the child process | 等待子进程的PID或GID */
UINT16 waitFlag; /**< The type of child process that is waiting, belonging to a group or parent,
a specific child process, or any child process | 等待的子进程以什么样的方式结束(OS_TASK_WAIT_PROCESS | OS_TASK_WAIT_GID | ..) */
#ifdef LOSCFG_KERNEL_LITEIPC //轻量级进程间通信开关
IpcTaskInfo *ipcTaskInfo; ///< 任务间通讯信息结构体
#endif
#ifdef LOSCFG_KERNEL_PERF
UINTPTR pc; ///< pc寄存器
UINTPTR fp; ///< fp寄存器
#endif
} LosTaskCB;
结构体LosTaskCB内容很多,各代表什么含义?
LosTaskCB相当于任务在内核中的身份证,它反映出每个任务在生命周期内的运行情况。既然是周期就会有状态,要运行就需要内存空间,就需要被内核算法调度,被选中CPU就去执行代码段指令,CPU要执行就需要告诉它从哪里开始执行,因为是多线程,但只有一个CPU就需要不断的切换任务,那执行会被中断,也需要再恢复后继续执行,又如何保证恢复的任务执行不会出错,这些问题都需要说明白。
什么是任务池?
Pool With Two Figures by David Hockney
前面已经说了任务是内核调度层面的概念,调度算法保证了task有序的执行,调度机制详见其他姊妹篇《调度故事 | 大郎,该喝药了》的介绍。
如此多的任务怎么管理和执行?管理靠任务池和就绪队列,执行靠调度算法。
代码如下(OsTaskInit):
LITE_OS_SEC_TEXT_INIT UINT32 OsTaskInit(VOID)
{
UINT32 index;
UINT32 ret;
UINT32 size;
g_taskMaxNum = LOSCFG_BASE_CORE_TSK_LIMIT;//任务池中最多默认128个,可谓铁打的任务池流水的线程
size = (g_taskMaxNum + 1) * sizeof(LosTaskCB);//计算需分配内存总大小
/*
* This memory is resident memory and is used to save the system resources
* of task control block and will not be freed。
*/
g_taskCBArray = (LosTaskCB *)LOS_MemAlloc(m_aucSysMem0, size);//任务池 常驻内存,不被释放
if (g_taskCBArray == NULL) {
return LOS_ERRNO_TSK_NO_MEMORY;
}
(VOID)memset_s(g_taskCBArray, size, 0, size);
LOS_ListInit(&g_losFreeTask);//空闲任务链表
LOS_ListInit(&g_taskRecyleList);//需回收任务链表
for (index = 0; index < g_taskMaxNum; index++) {
g_taskCBArray[index]。taskStatus = OS_TASK_STATUS_UNUSED;
g_taskCBArray[index]。taskID = index;//任务ID最大默认127
LOS_ListTailInsert(&g_losFreeTask, &g_taskCBArray[index]。pendList);//都插入空闲任务列表
}//注意:这里挂的是pendList节点,所以取TCB要通过 OS_TCB_FROM_PENDLIST 取。
ret = OsPriQueueInit();//创建32个任务优先级队列,即32个双向循环链表
if (ret != LOS_OK) {
return LOS_ERRNO_TSK_NO_MEMORY;
}
/* init sortlink for each core */
for (index = 0; index < LOSCFG_KERNEL_CORE_NUM; index++) {
ret = OsSortLinkInit(&g_percpu[index]。taskSortLink);//每个CPU内核都有一个执行任务链表
if (ret != LOS_OK) {
return LOS_ERRNO_TSK_NO_MEMORY;
}
}
return LOS_OK;
}
g_taskCBArray 就是个任务池,默认创建128个任务,常驻内存,不被释放。
g_losFreeTask是空闲任务链表,想创建任务时来这里申请一个空闲任务,用完了就回收掉,继续给后面的申请使用。
g_taskRecyleList是回收任务链表,专用来回收exit 任务,任务所占资源被确认归还后被彻底删除,就像员工离职一样,得有个离职队列和流程,要归还电脑,邮箱,有没有借钱要还的 等操作。
对应张大爷的故事:用户要来场馆领取表格填节目单,场馆只准备了128张表格,领完就没有了,但是节目表演完了会回收表格,这样多了一张表格就可以给其他人领取了,这128张表格对应鸿蒙内核这就是任务池,简单吧。
那么就绪队列是怎么回事?Task怎么管理呢?我们下篇再继续分解。
《开源鸿蒙内核源码分析系列 | 任务管理|老师如何管熊孩子下篇》
百文说内核 | 抓住主脉络
子曰:“诗三百,一言以蔽之,曰‘思无邪’。”——《论语》:为政篇。
百文相当于摸出内核的肌肉和器官系统,让人开始丰满有立体感,因是直接从注释源码起步,在开源鸿蒙内核源码加注释过程中,每每有心得处就整理,慢慢形成了以下文章。内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感,容易理解记忆。说别人能听得懂的话很重要! 百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思。更希望让内核变得栩栩如生,倍感亲切.确实有难度,自不量力,但已经出发,回头已是不可能的了。
百万汉字注解内核目的是要看清楚其毛细血管,细胞结构,等于在拿放大镜看内核。内核并不神秘,带着问题去源码中找答案是很容易上瘾的,你会发现很多文章对一些问题的解读是错误的,或者说不深刻难以自圆其说,你会慢慢形成自己新的解读,而新的解读又会碰到新的问题,如此层层递进,滚滚向前,拿着放大镜根本不愿意放手。
与代码有bug需不断debug一样,文章和注解内容会存在不少错漏之处,请多包涵,但会反复修正,持续更新,v**.xx 代表文章序号和修改的次数,精雕细琢,言简意赅,力求打造精品内容。百篇博客系列思维导图结构如下:
根据上图的思维导图,我们未来将要和大家一一分享以上大部分关键技术点的博客文章。
百万汉字注解.精读内核源码
如果大家觉得看文章不过瘾,想直接撸代码的话,可以去下面四大码仓围观同步注释内核源码:
gitee仓:
https://gitee.com/weharmony/kernel_liteos_a_note
github仓 :
https://github.com/kuangyufei/kernel_liteos_a_note
codechina仓:
https://codechina.csdn.net/kuangyufei/kernel_liteos_a_note
coding仓:
https://weharmony.coding.net/public/harmony/kernel_liteos_a_note/git/files
写在最后
我们最近正带着大家玩嗨OpenHarmony。如果你有用OpenHarmony开发的好玩的东东,或者有对OpenHarmony的深度技术剖析,想通过我们平台让更多的小伙伴知道和分享的,欢迎投稿,让我们一起嗨起来!有点子,有想法,有Demo,立刻联系我们:
合作邮箱:zzliang@atomsource.org