OpenHarmony开发者文档 | 同步最新鸿蒙官方文档 | OpenHarmony | HarmonyOS < 国内访问 | 国外访问 >
百篇博客系列篇.本篇为:
- v01.xx 鸿蒙内核源码分析(双向链表篇) | 谁是内核最重要结构体 | 51 .c .h .o
谁是鸿蒙内核最重要的结构体?
答案一定是:
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
不是拿来单独用的,它是寄生在内容结构体上的,谁用它谁就是它的数据.看图就明白了.
功能接口
鸿蒙系统中的双向链表模块为用户提供下面几个接口。
功能分类 接口名 描述
初始化链表 LOS_ListInit 对链表进行初始化
增加节点 LOSListAdd 将新节点添加到链表中
在链表尾部插入节点 LOS_ListTailInsert 将节点插入到双向链表尾部
在链表头部插入节点 LOS_ListHeadInsert 将节点插入到双向链表头部
删除节点 LOS_ListDelete 将指定的节点从链表中删除
判断双向链表是否为空 LOS_ListEmpty 判断链表是否为空
删除节点并初始化链表 LOS_ListDelInit 将指定的节点从链表中删除使用该节点初始化链表
在链表尾部插入链表 LOS_ListTailInsertList 将链表插入到双向链表尾部
在链表头部插入链表 LOS_ListHeadInsertList 将链表插入到双向链表头部
请结合下面的代码和图去理解双向链表,不管花多少时间,一定要理解它的插入/删除动作, 否则后续内容将无从谈起.
//将指定节点初始化为双向链表节点
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;
}
//将指定节点从链表中删除,并使用该节点初始化链表
LITE_OS_SEC_ALW_INLINE STATIC INLINE VOID LOS_ListDelInit(LOS_DL_LIST *list)
{
list->pstNext->pstPrev = list->pstPrev;
list->pstPrev->pstNext = list->pstNext;
LOS_ListInit(list);
}
此处仅列出
LOS_ListDelInit
图
强大的宏
除了内联函数,对双向链表的初始化,偏移定位,遍历 等等操作提供了更强大的宏支持.使内核以极其简洁高效的代码实现复杂逻辑的处理.
//定义一个节点并初始化为双向链表节点
#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_OFF_SET_OF 和 LOS_DL_LIST_ENTRY
这里要重点说下
LOS_OFF_SET_OF
和
LOS_DL_LIST_ENTRY
两个宏,个人认为它们是链表操作中最关键,最重要的宏.在读内核源码的过程会发现
LOS_DL_LIST_ENTRY
高频的出现,它们解决了通过结构体的任意一个成员变量来找到结构体的入口地址.
这个意义重大,因为在运行过程中,往往只能提供成员变量的地址,那它是如何做到通过个人找到组织的呢?
-
找到成员变量在结构体中的相对偏移位置. 在系列篇 用栈方式篇中 已说过 鸿蒙采用的是递减满栈的方式.以LOS_OFF_SET_OF
结构体举例ProcessCB
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;
因为在结构体的后面,所以它内存地址会比在前面的waitList
高,有了顺序方向就很容易得到pendList
的第一个变量的地址.ProcessCB
就是干这个的,含义就是相对第一个变量地址,你LOS_OFF_SET_OF
偏移了多少.waitList
- 如此,当外面只提供
的地址再减去偏移地址 就可以得到waitList
的起始地址.ProcessCB
当然如果提供#define LOS_DL_LIST_ENTRY(item, type, member) \ ((type *)(VOID *)((CHAR *)(item) - LOS_OFF_SET_OF(type, member)))
或pendList
的地址道理一样.exitChildList
实现了通过任意成员变量来获取LOS_DL_LIST_ENTRY
的起始地址.ProcessCB
OsGetTopTask
有了以上对链表操作的宏,可以使得代码变得简洁易懂,例如在调度算法中获取当前最高优先级的任务时,就需要遍历整个进程和其任务的就绪列表.
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
-
孩子链表,所有由它fork出来的进程都挂到这个链表上.上面的孩子进程在死亡前会将自己从上面摘出去,转而挂到childrenList
链表上.exitChildList
-
退出孩子链表,进入死亡程序的进程要挂到这个链表上,一个进程的死亡是件挺麻烦的事,进程池的数量有限,需要及时回收进程资源,但家族管理关系复杂,要去很多地方消除痕迹.尤其还有其他进程在看你笑话,等你死亡(exitChildList
/wait
)了通知它们一声.waitpid
-
兄弟链表,和你同一个父亲的进程都挂到了这个链表上.siblingList
-
朋友圈链表,里面是因为兴趣爱好(进程组)而挂在一起的进程,它们可以不是一个父亲,不是一个祖父,但一定是同一个老祖宗(用户态和内核态根进程).subordinateGroupList
-
线程链表,上面挂的是进程ID都是这个进程的线程(任务),进程和线程的关系是1:N的关系,一个线程只能属于一个进程.这里要注意任务在其生命周期中是不能改所属进程的.threadSiblingList
-
线程的调度队列数组,一共32个,任务和进程一样有32个优先级,调度算法的过程是先找到优先级最高的进程,在从该进程的任务队列里去最高的优先级任务运行.threadPriQueueList
-
是等待子进程消亡的任务链表,注意上面挂的是任务.任务是通过系统调用waitList
将任务挂到pid_t wait(int *status); pid_t waitpid(pid_t pid, int *status, int options);
上.鸿蒙waitList
系统调用为waitpid
,具体看进程回收篇.SysWait
双向链表是内核最重要的结构体,精读内核的路上它会反复的映入你的眼帘,理解它是理解内核运作的关键所在!
百篇博客.往期回顾
在加注过程中,整理出以下文章.内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感,容易理解记忆.
说别人能听得懂的话很重要! 百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思.更希望让内核变得栩栩如生,倍感亲切.确实有难度,自不量力,但已经出发,回头已是不可能的了.😛
与写代码有bug需不断debug一样,文章和注解内容会将错漏之处反复修正,持续更新,
代表修改的次数,精雕细琢,言简意赅,力求打造精品内容.
.xx
- v52.xx 鸿蒙内核源码分析(静态站点篇) | 五一哪也没去就干了这事 | 51 .c .h .o
- v51.xx 鸿蒙内核源码分析(ELF格式篇) | 应用程序入口并不是main | 51 .c .h .o
- v50.xx 鸿蒙内核源码分析(编译环境篇) | 编译鸿蒙看这篇或许真的够了 | 51 .c .h .o
- v49.xx 鸿蒙内核源码分析(信号消费篇) | 谁让CPU连续四次换栈运行 | 51 .c .h .o
- v48.xx 鸿蒙内核源码分析(信号生产篇) | 年过半百,依然活力十足 | 51 .c .h .o
- v47.xx 鸿蒙内核源码分析(进程回收篇) | 临终前如何向老祖宗托孤 | 51 .c .h .o
- v46.xx 鸿蒙内核源码分析(特殊进程篇) | 龙生龙凤生凤老鼠生儿会打洞 | 51 .c .h .o
- v45.xx 鸿蒙内核源码分析(Fork篇) | 一次调用,两次返回 | 51 .c .h .o
- v44.xx 鸿蒙内核源码分析(中断管理篇) | 江湖从此不再怕中断 | 51 .c .h .o
- v43.xx 鸿蒙内核源码分析(中断概念篇) | 海公公的日常工作 | 51 .c .h .o
- v42.xx 鸿蒙内核源码分析(中断切换篇) | 系统因中断活力四射 | 51 .c .h .o
- v41.xx 鸿蒙内核源码分析(任务切换篇) | 看汇编如何切换任务 | 51 .c .h .o
- v40.xx 鸿蒙内核源码分析(汇编汇总篇) | 汇编可爱如邻家女孩 | 51 .c .h .o
- v39.xx 鸿蒙内核源码分析(异常接管篇) | 社会很单纯,复杂的是人 | 51 .c .h .o
- v38.xx 鸿蒙内核源码分析(寄存器篇) | 小强乃宇宙最忙存储器 | 51 .c .h .o
- v37.xx 鸿蒙内核源码分析(系统调用篇) | 开发者永远的口头禅 | 51 .c .h .o
- v36.xx 鸿蒙内核源码分析(工作模式篇) | CPU是韦小宝,七个老婆 | 51 .c .h .o
- v35.xx 鸿蒙内核源码分析(时间管理篇) | 谁是内核基本时间单位 | 51 .c .h .o
- v34.xx 鸿蒙内核源码分析(原子操作篇) | 谁在为原子操作保驾护航 | 51 .c .h .o
- v33.xx 鸿蒙内核源码分析(消息队列篇) | 进程间如何异步传递大数据 | 51 .c .h .o
- v32.xx 鸿蒙内核源码分析(CPU篇) | 整个内核就是一个死循环 | 51 .c .h .o
- v31.xx 鸿蒙内核源码分析(定时器篇) | 哪个任务的优先级最高 | 51 .c .h .o
- v30.xx 鸿蒙内核源码分析(事件控制篇) | 任务间多对多的同步方案 | 51 .c .h .o
- v29.xx 鸿蒙内核源码分析(信号量篇) | 谁在负责解决任务的同步 | 51 .c .h .o
- v28.xx 鸿蒙内核源码分析(进程通讯篇) | 九种进程间通讯方式速揽 | 51 .c .h .o
- v27.xx 鸿蒙内核源码分析(互斥锁篇) | 比自旋锁丰满的互斥锁 | 51 .c .h .o
- v26.xx 鸿蒙内核源码分析(自旋锁篇) | 自旋锁当立贞节牌坊 | 51 .c .h .o
- v25.xx 鸿蒙内核源码分析(并发并行篇) | 听过无数遍的两个概念 | 51 .c .h .o
- v24.xx 鸿蒙内核源码分析(进程概念篇) | 进程在管理哪些资源 | 51 .c .h .o
- v23.xx 鸿蒙内核源码分析(汇编传参篇) | 如何传递复杂的参数 | 51 .c .h .o
- v22.xx 鸿蒙内核源码分析(汇编基础篇) | CPU在哪里打卡上班 | 51 .c .h .o
- v21.xx 鸿蒙内核源码分析(线程概念篇) | 是谁在不断的折腾CPU | 51 .c .h .o
- v20.xx 鸿蒙内核源码分析(用栈方式篇) | 程序运行场地由谁提供 | 51 .c .h .o
- v19.xx 鸿蒙内核源码分析(位图管理篇) | 谁能一分钱分两半花 | 51 .c .h .o
- v18.xx 鸿蒙内核源码分析(源码结构篇) | 内核每个文件的含义 | 51 .c .h .o
- v17.xx 鸿蒙内核源码分析(物理内存篇) | 怎么管理物理内存 | 51 .c .h .o
- v16.xx 鸿蒙内核源码分析(内存规则篇) | 内存管理到底在管什么 | 51 .c .h .o
- v15.xx 鸿蒙内核源码分析(内存映射篇) | 虚拟内存虚在哪里 | 51 .c .h .o
- v14.xx 鸿蒙内核源码分析(内存汇编篇) | 谁是虚拟内存实现的基础 | 51 .c .h .o
- v13.xx 鸿蒙内核源码分析(源码注释篇) | 鸿蒙必定成功,也必然成功 | 51 .c .h .o
- v12.xx 鸿蒙内核源码分析(内存管理篇) | 虚拟内存全景图是怎样的 | 51 .c .h .o
- v11.xx 鸿蒙内核源码分析(内存分配篇) | 内存有哪些分配方式 | 51 .c .h .o
- v10.xx 鸿蒙内核源码分析(内存主奴篇) | 皇上和奴才如何相处 | 51 .c .h .o
- v09.xx 鸿蒙内核源码分析(调度故事篇) | 用故事说内核调度过程 | 51 .c .h .o
- v08.xx 鸿蒙内核源码分析(总目录) | 百万汉字注解 百篇博客分析 | 51 .c .h .o
- v07.xx 鸿蒙内核源码分析(调度机制篇) | 任务是如何被调度执行的 | 51 .c .h .o
- v06.xx 鸿蒙内核源码分析(调度队列篇) | 内核有多少个调度队列 | 51 .c .h .o
- v05.xx 鸿蒙内核源码分析(任务管理篇) | 任务池是如何管理的 | 51 .c .h .o
- v04.xx 鸿蒙内核源码分析(任务调度篇) | 任务是内核调度的单元 | 51 .c .h .o
- v03.xx 鸿蒙内核源码分析(时钟任务篇) | 触发调度谁的贡献最大 | 51 .c .h .o
- v02.xx 鸿蒙内核源码分析(进程管理篇) | 谁在管理内核资源 | 51 .c .h .o
- v01.xx 鸿蒙内核源码分析(双向链表篇) | 谁是内核最重要结构体 | 51 .c .h .o
关于 51 .c .h .o
看系列篇文章会常看到
51 .c .h .o
,希望这对大家阅读不会造成影响.
分别对应以下四个站点的首个字符,感谢这些站点一直以来对系列篇的支持和推荐,尤其是 oschina gitee ,很喜欢它的界面风格,简洁大方,让人感觉到开源的伟大!
- 51cto
- csdn
- harmony
- oschina
而巧合的是
.c .h .o
是C语言的头/源/目标文件,这就很有意思了,冥冥之中似有天数,将这四个宝贝以这种方式融合在一起.
51 .c .h .o
, 我要CHO ,嗯嗯,hin 顺口 : )
百万汉字注解.百篇博客分析
百万汉字注解 >> 精读鸿蒙源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< gitee | github | csdn | coding >
百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点定期更新中< 51cto | csdn | harmony | osc >
关注不迷路.代码即人生
热爱是所有的理由和答案 - turing
原创不易,欢迎转载,但麻烦请注明出处.