鸿蒙内核源码分析(双向链表篇) | 谁是内核最重要结构体 ? | 开篇致敬鸿蒙内核开发者 | v1.10
鸿蒙内核源码分析(双向链表篇) | 谁是内核最重要结构体 ? | 开篇致敬鸿蒙内核开发者 | v1.10
谁是鸿蒙内核最重要的结构体?
答案一定是: LOS_DL_LIST
(双向链表),它长这样.
typedef struct LOS_DL_LIST {//双向链表,内核最重要结构体 struct LOS_DL_LIST *pstPrev; /**< Current node's pointer to the previous node *///前驱节点(左手) struct LOS_DL_LIST *pstNext; /**< Current node's pointer to the next node *///后继节点(右手) } LOS_DL_LIST;
登录后复制
结构体够简单了吧,只有前后两个指向自己的指针,但恰恰是因为太简单,所以才太不简单. 就像氢原子一样,宇宙中无处不在,占比最高,原因是因为它最简单,最稳定!
内核的各自模块都能看到双向链表的身影,下图是各处初始化双向链表的操作,因为太多了,只截取了部分:
很多人问图怎么来的, source insight 4.0
是阅读大型C/C++
工程的必备工具,要用4.0否则中文有乱码.
可以豪不夸张的说理解LOS_DL_LIST
及相关函数是读懂鸿蒙内核的关键。前后指针(注者后续将比喻成一对左右触手)灵活的指挥着系统精准的运行,越是深入分析内核源码,越能感受到内核开发者对LOS_DL_LIST
非凡的驾驭能力,笔者仿佛看到了无数双手前后相连,拉起了一个个双向循环链表,把指针的高效能运用到了极致,这也许就是编程的艺术吧!这么重要的结构体还是需详细讲解一下.
基本概念
双向链表是指含有往前和往后两个方向的链表,即每个结点中除存放下一个节点指针外,还增加一个指向其前一个节点的指针。其头指针head
是唯一确定的。从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点,这种数据结构形式使得双向链表在查找时更加方便,特别是大量数据的遍历。由于双向链表具有对称性,能方便地完成各种插入、删除等操作,但需要注意前后方向的操作。
有好几个同学问数据在哪? 确实LOS_DL_LIST
这个结构看起来怪怪的,它竟没有数据域!所以看到这个结构的人第一反应就是我们怎么访问数据?其实LOS_DL_LIST
不是拿来单独用的,它是寄生在内容结构体上的,谁用它谁就是它的数据.看图就明白了.
功能接口
鸿蒙系统中的双向链表模块为用户提供下面几个接口。
请结合下面的代码和图去理解双向链表,不管花多少时间,一定要理解它的插入/删除动作, 否则后续内容将无从谈起.
//将指定节点初始化为双向链表节点 LITE_OS_SEC_ALW_INLINE STATIC INLINE VOID LOS_ListInit(LOS_DL_LIST *list) { list->pstNext = list; list->pstPrev = list; } //将指定节点挂到双向链表头部 LITE_OS_SEC_ALW_INLINE STATIC INLINE VOID LOS_ListAdd(LOS_DL_LIST *list, LOS_DL_LIST *node) { node->pstNext = list->pstNext; node->pstPrev = list; list->pstNext->pstPrev = node; list->pstNext = node; } //将指定节点从链表中删除,自己把自己摘掉 LITE_OS_SEC_ALW_INLINE STATIC INLINE VOID LOS_ListDelete(LOS_DL_LIST *node) { node->pstNext->pstPrev = node->pstPrev; node->pstPrev->pstNext = node->pstNext; node->pstNext = NULL; node->pstPrev = NULL; }
登录后复制
强大的宏
除了内联函数,对双向遍历的初始化,定位,遍历 等等操作提供了更强大的宏支持.使内核以极其简洁高效的代码实现复杂逻辑的处理.
//定义一个节点并初始化为双向链表节点 #define LOS_DL_LIST_HEAD(list) LOS_DL_LIST list = { &(list), &(list) } //获取指定结构体内的成员相对于结构体起始地址的偏移量 #define LOS_OFF_SET_OF(type, member) ((UINTPTR)&((type *)0)->member) //获取包含链表的结构体地址,接口的第一个入参表示的是链表中的某个节点,第二个入参是要获取的结构体名称,第三个入参是链表在该结构体中的名称 #define LOS_DL_LIST_ENTRY(item, type, member) \ ((type *)(VOID *)((CHAR *)(item) - LOS_OFF_SET_OF(type, member))) //遍历双向链表 #define LOS_DL_LIST_FOR_EACH(item, list) \ for (item = (list)->pstNext; \ (item) != (list); \ item = (item)->pstNext) //遍历指定双向链表,获取包含该链表节点的结构体地址,并存储包含当前节点的后继节点的结构体地址 #define LOS_DL_LIST_FOR_EACH_ENTRY_SAFE(item, next, list, type, member) \ for (item = LOS_DL_LIST_ENTRY((list)->pstNext, type, member), \ next = LOS_DL_LIST_ENTRY((item)->member.pstNext, type, member); \ &(item)->member != (list); \ item = next, next = LOS_DL_LIST_ENTRY((item)->member.pstNext, type, member)) //遍历指定双向链表,获取包含该链表节点的结构体地址 #define LOS_DL_LIST_FOR_EACH_ENTRY(item, list, type, member) \ for (item = LOS_DL_LIST_ENTRY((list)->pstNext, type, member); \ &(item)->member != (list); \ item = LOS_DL_LIST_ENTRY((item)->member.pstNext, type, member))
登录后复制
例如在调度算法中获取当前最高优先级的任务时,就需要遍历整个进程和进程任务的所有就绪列表. LOS_DL_LIST_FOR_EACH_ENTRY
高效的解决了层层循环的问题,让代码简洁易懂.
LITE_OS_SEC_TEXT_MINOR LosTaskCB *OsGetTopTask(VOID) { UINT32 priority, processPriority; UINT32 bitmap; UINT32 processBitmap; LosTaskCB *newTask = NULL; #if (LOSCFG_KERNEL_SMP == YES) UINT32 cpuid = ArchCurrCpuid(); #endif LosProcessCB *processCB = NULL; processBitmap = g_priQueueBitmap; while (processBitmap) { processPriority = CLZ(processBitmap); LOS_DL_LIST_FOR_EACH_ENTRY(processCB, &g_priQueueList[processPriority], LosProcessCB, pendList) { bitmap = processCB->threadScheduleMap; while (bitmap) { priority = CLZ(bitmap); LOS_DL_LIST_FOR_EACH_ENTRY(newTask, &processCB->threadPriQueueList[priority], LosTaskCB, pendList) { #if (LOSCFG_KERNEL_SMP == YES) if (newTask->cpuAffiMask & (1U << cpuid)) { #endif newTask->taskStatus &= ~OS_TASK_STATUS_READY; OsPriQueueDequeue(processCB->threadPriQueueList, &processCB->threadScheduleMap, &newTask->pendList); OsDequeEmptySchedMap(processCB); goto OUT; #if (LOSCFG_KERNEL_SMP == YES) } #endif } bitmap &= ~(1U << (OS_PRIORITY_QUEUE_NUM - priority - 1)); } } processBitmap &= ~(1U << (OS_PRIORITY_QUEUE_NUM - processPriority - 1)); } OUT: return newTask; }
登录后复制
结构体的最爱
LOS_DL_LIST
是复杂结构体的最爱,以下举例 ProcessCB
(进程控制块)是描述一个进程的所有信息,其中用到了 8个双向链表,这简直比章鱼还牛逼,章鱼也才四双触手,但进程有8双(16只)触手.
typedef struct ProcessCB { //...此处省略其他变量 LOS_DL_LIST pendList; /**< Block list to which the process belongs */ //进程所属的阻塞列表,如果因拿锁失败,就由此节点挂到等锁链表上 LOS_DL_LIST childrenList; /**< my children process list */ //孩子进程都挂到这里,形成双循环链表 LOS_DL_LIST exitChildList; /**< my exit children process list */ //那些要退出孩子进程挂到这里,白发人送黑发人。 LOS_DL_LIST siblingList; /**< linkage in my parent's children list */ //兄弟进程链表, 56个民族是一家,来自同一个父进程. LOS_DL_LIST subordinateGroupList; /**< linkage in my group list */ //进程是组长时,有哪些组员进程 LOS_DL_LIST threadSiblingList; /**< List of threads under this process *///进程的线程(任务)列表 LOS_DL_LIST threadPriQueueList[OS_PRIORITY_QUEUE_NUM]; /**< The process's thread group schedules thepriority hash table */ //进程的线程组调度优先级哈希表 LOS_DL_LIST waitList; /**< The process holds the waitLits to support wait/waitpid *///进程持有等待链表以支持wait/waitpid } LosProcessCB;
登录后复制
解读
pendList
个人认为它是鸿蒙内核功能最多的一个链表,它远不止字面意思阻塞链表这么简单,只有深入解读源码后才能体会它真的是太会来事了,一般把它理解为阻塞链表就行.上面挂的是处于阻塞状态的进程.childrenList
孩子链表,所有由它fork出来的进程都挂到这个链表上.上面的孩子进程在死亡前会将自己从上面摘出去,转而挂到exitChildList
链表上.exitChildList
退出孩子链表,进入死亡程序的进程要挂到这个链表上,一个进程的死亡是件挺麻烦的事,进程池的数量有限,需要及时回收进程资源,但家族管理关系复杂,要去很多地方消除痕迹.尤其还有其他进程在看你笑话,等你死亡(wait
/waitpid
)了通知它们一声.siblingList
兄弟链表,和你同一个父亲的进程都挂到了这个链表上.subordinateGroupList
朋友圈链表,里面是因为兴趣爱好(进程组)而挂在一起的进程,它们可以不是一个父亲,不是一个祖父,但一定是同一个老祖宗(用户态和内核态根进程).threadSiblingList
线程链表,上面挂的是进程ID都是这个进程的线程(任务),进程和线程的关系是1:N的关系,一个线程只能属于一个进程.这里要注意任务在其生命周期中是不能改所属进程的.threadPriQueueList
线程的调度队列数组,一共32个,任务和进程一样有32个优先级,调度算法的过程是先找到优先级最高的进程,在从该进程的任务队列里去最高的优先级任务运行.waitList
是等待子进程消亡的任务链表,注意上面挂的是任务.任务是通过系统调用pid_t wait(int *status); pid_t waitpid(pid_t pid, int *status, int options);
登录后复制
将任务挂到
waitList
上.鸿蒙waitpid
系统调用为SysWait
,具体看进程回收篇.
双向链表是内核最重要的结构体,精读内核的路上它会反复的映入你的眼帘,理解它是理解内核运作的关键所在!
鸿蒙源码百篇博客 往期回顾
在给 鸿蒙内核源码加中文注释 过程中,整理出以下文章.内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感.百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思.更希望让内核变得栩栩如生,倍感亲切.确实有难度,自不量力,但已经出发,回头已是不可能的了.:P
写文章比写代码累多了,越深入研究,越觉得没写好,所以文章和注解会反复修正, .xx代表修改的次数, 将持续完善源码注解和文档内容,精雕细琢,言简意赅, 尽全力打磨精品内容.
v50.xx (编译环境篇) | 编译鸿蒙看这篇或许真的够了 < csdn | 51cto | osc >
v49.xx (信号消费篇) | 用户栈到内核栈的两次切换 < csdn | 51cto | osc >
v48.xx (信号生产篇) | 如何安装和发送信号? < csdn | 51cto | osc >
v47.xx (进程回收篇) | 进程在临终前如何向老祖宗托孤 < csdn | 51cto | osc >
v46.xx (特殊进程篇) | 龙生龙,凤生凤,老鼠生儿会打洞 < csdn | 51cto | osc >
v45.xx (fork篇) | fork是如何做到调用一次,返回两次的 ? < csdn | 51cto | osc >
v44.xx (中断管理篇) | 硬中断的实现<>观察者模式 < csdn | 51cto | osc >
v43.xx (中断概念篇) | 外人眼中权势滔天的当红海公公 < csdn | 51cto | osc >
v42.xx (中断切换篇) | 中断切换到底在切换什么? < csdn | 51cto | osc >
v41.xx (任务切换篇) | 汇编告诉任务到底在切换什么 < csdn | 51cto | osc >
v40.xx (汇编汇总篇) | 所有的汇编代码都在这里 < csdn | 51cto | osc >
v39.xx (异常接管篇) | 社会很单纯,复杂的是人 < csdn | 51cto | osc >
v38.xx (寄存器篇) | arm所有寄存器一网打尽,不再神秘 < csdn | 51cto | osc >
v37.xx (系统调用篇) | 系统调用到底经历了什么 < csdn | 51cto | osc >
v36.xx (工作模式篇) | cpu是韦小宝,有哪七个老婆? < csdn | 51cto | osc >
v35.xx (时间管理篇) | tick是操作系统的基本时间单位 < csdn | 51cto | osc >
v34.xx (原子操作篇) | 是谁在为原子操作保驾护航? < csdn | 51cto | osc >
v33.xx (消息队列篇) | 进程间如何异步解耦传递大数据 ? < csdn | 51cto | osc >
v32.xx (cpu篇) | 整个内核就是一个死循环 < csdn | 51cto | osc >
v31.xx (定时器篇) | 内核最高优先级任务是谁? < csdn | 51cto | osc >
v30.xx (事件控制篇) | 任务间多对多的同步方案 < csdn | 51cto | osc >
v29.xx (信号量篇) | 信号量解决任务同步问题 < csdn | 51cto | osc >
v28.xx (进程通讯篇) | 九种进程间通讯方式速揽 < csdn | 51cto | osc >
v27.xx (互斥锁篇) | 比自旋锁丰满许多的互斥锁 < csdn | 51cto | osc >
v26.xx (自旋锁篇) | 自旋锁当立贞节牌坊! < csdn | 51cto | osc >
v25.xx (并发并行篇) | 怎么记住并发并行的区别? < csdn | 51cto | osc >
v24.xx (进程概念篇) | 进程在管理哪些资源? < csdn | 51cto | osc >
v23.xx (汇编传参篇) | 汇编如何传递复杂的参数? < csdn | 51cto | osc >
v22.xx (汇编基础篇) | cpu在哪里打卡上班? < csdn | 51cto | osc >
v21.xx (线程概念篇) | 是谁在不断的折腾cpu? < csdn | 51cto | osc >
v20.xx (用栈方式篇) | 栈是构建底层运行的基础 < csdn | 51cto | osc >
v19.xx (位图管理篇) | 为何进程和线程优先级都是32个? < csdn | 51cto | osc >
v18.xx (源码结构篇) | 梳理内核源文件的作用和含义 < csdn | 51cto | osc >
v17.xx (物理内存篇) | 这样记伙伴算法永远不会忘 < csdn | 51cto | osc >
v16.xx (内存规则篇) | 内存管理到底在管什么? < csdn | 51cto | osc >
v15.xx (内存映射篇) | 什么是内存最重要的实现基础 ? < csdn | 51cto | osc >
v14.xx (内存汇编篇) | 什么是虚拟内存的实现基础? < csdn | 51cto | osc >
v13.xx (源码注释篇) | 鸿蒙必须成功,也必然成功 < csdn | 51cto | osc >
v12.xx (内存管理篇) | 虚拟内存全景图是怎样的? < csdn | 51cto | osc >
v11.xx (内存分配篇) | 内存有哪些分配方式? < csdn | 51cto | osc >
v10.xx (内存主奴篇) | 紫禁城的主子和奴才如何相处? < csdn | 51cto | osc >
v09.xx (调度故事篇) | 用故事说内核调度过程 < csdn | 51cto | osc >
v08.xx (总目录) | 百万汉字注解 百篇博客分析 < csdn | 51cto | osc >
v07.xx (调度机制篇) | 任务是如何被调度执行的? < csdn | 51cto | osc >
v06.xx (调度队列篇) | 内核有多少个调度队列? < csdn | 51cto | osc >
v05.xx (任务管理篇) | 任务池是如何管理的? < csdn | 51cto | osc >
v04.xx (任务调度篇) | 任务是内核调度的单元 < csdn | 51cto | osc >
v03.xx (时钟任务篇) | 调度最大的源动力来自哪里? < csdn | 51cto | osc >
v02.xx (进程管理篇) | 进程是内核资源管理单元 < csdn | 51cto | osc >
v01.xx (双向链表篇) | 谁是内核最重要结构体? < csdn | 51cto | osc >
进入 >> osc | csdn | 51cto | 掘金 | 公众号 | 头条号 | gitee | github
各大站点搜 "鸿蒙内核源码分析" .原创不易,欢迎转载,但请注明出处.
热爱是所有的理由和答案 - turing
©著作权归作者所有:来自51CTO博客作者weharmony的原创作品,如需转载,请注明出处,否则将追究法律责任