开源鸿蒙内核源码分析系列 | 优雅的宏 | 编译器也喜欢复制粘贴(转载)
原文来自鸿蒙研究站:http://weharmonyos.com/blog/05.html
鸿蒙研究站网址:http://weharmonyos.com
什么是宏 ?
在真正的编译开始之前,编译器也需要热热身,俗话说的好:”前戏工作不能少,后续推进质量好”。宏就是编译器的前戏部分(预处理),简单的说就是复制粘贴。大多数人第一次接触宏多半是因为 π。
#define PI 3.1415926
人类进化的结果是不擅长做记忆类操作,但这并非缺点,相比3.1415926 ,π 能减少体能的消耗,这种将复杂信息取个别名方便记录和传播的做法人类用了几千年。据说在猴子的世界里也有专门负责盯梢的,看到远方有狮子来了,会手舞足蹈描绘狮子的外形和模仿声音以此告知同伴什么动物来了而躲避危险。但人类只需要喊一句 “狮子来了”足以,这才是食物链顶端的做法,单给万千事物取名的这一个能力就能甩动物世界几百条街。更别说以此衍生出来的畊宏女孩,山东彭于晏等等流行网络词汇,其做法都是用极低的成本达到极快的传播。
Macro是Macro Instruction的缩写,将某个输入映射到替代输出的一种规则或模式,输入和输出可以是一系列词汇标记或字符,或语法树。为什么中文被翻译成宏可能是因为单词宏观(macro) 。
若将宏只停留在 3.1415926 就简单肤浅了,要知道任何看似简单的背后都可以击鼓传花,熟能生巧,蛤蟆功练到最高境界那也能成为天下第一的。站长真正被宏的强大震撼到的是看了侯俊杰先生的 《深入浅出MFC》 对消息机制的实现,当时不由的惊呼 “靠,原来还能这么玩 ! ” ,从此对宏爱不释手,所以百篇博客必须为此开一篇,更何况开源鸿蒙内核对宏的讲究也并没有让人失望。
一段难懂的代码
能看懂以下代码中 for 循环 的C语言功底非同一般,绝对的大神。
STATIC VOID HPFPriorityRestore(LosTaskCB *owner, const LOS_DL_LIST *list, const SchedParam *param)
{
LosTaskCB *pendedTask = NULL;
// ...
for (pendedTask = ((LosTaskCB *)(VOID *)((CHAR *)((list)->pstNext) - ((UINTPTR)&((LosTaskCB *)0)->pendList)));
&(pendedTask)->pendList != (list);
pendedTask = ((LosTaskCB *)(VOID *)((CHAR *)((pendedTask)->pendList.pstNext) - ((UINTPTR)&((LosTaskCB *)0)->pendList))))
{
SchedHPF *pendSp = (SchedHPF *)&pendedTask->sp;
if ((pendedTask->ops == owner->ops) && (priority != pendSp->priority)) {
LOS_BitmapClr(&sp->priBitmap, pendSp->priority);
}
}
}
天天跟这样的代码打交道容易得脑梗,如果换成以下的样子就简洁了很多:
STATIC VOID HPFPriorityRestore(LosTaskCB *owner, const LOS_DL_LIST *list, const SchedParam *param)
{
LosTaskCB *pendedTask = NULL;
// ...
LOS_DL_LIST_FOR_EACH_ENTRY(pendedTask, list, LosTaskCB, pendList) {
SchedHPF *pendSp = (SchedHPF *)&pendedTask->sp;
if ((pendedTask->ops == owner->ops) && (priority != pendSp->priority)) {
LOS_BitmapClr(&sp->priBitmap, pendSp->priority);
}
}
}
LOS_DL_LIST_FOR_EACH_ENTRY(pendedTask, list, LosTaskCB, pendList) 含义如下:
- 遍历一个叫list的双向链表,链表上挂的是一个个叫LosTaskCB结构体节点。
- LosTaskCB是通过其成员变量pendList 挂到list 上的。
- 解铃还须系铃人,将节点摘下来也需通过pendList ,并通过地址偏移量找到 LosTaskCB的开始地址后给交给变量pendedTask处理。
- 使用两个嵌套宏LOS_DL_LIST_ENTRY,LOS_OFF_SET_OF完成了以上操作,三个宏完整原型如下:
#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))
#define LOS_DL_LIST_ENTRY(item, type, member) \
((type *)(VOID *)((CHAR *)(item) - LOS_OFF_SET_OF(type, member)))
#define LOS_OFF_SET_OF(type, member) ((UINTPTR)&((type *)0)->member)
- 遍历宏很简洁优雅,内核关于双向链表的遍历都可以通过它来完成,其他操作具体可翻看 双向链表篇。
除了简化对双向链表操作还有对红黑树(编者注:是每个节点都带有颜色属性的二叉查找树)的操作,尝试解读下以下代码的含义:
RB_WALK(pstTree, pstNode, pstWalk)
{
OsRbDeleteNode(pstTree, pstNode);
(VOID)pstTree->pfFree(pstNode);
}
RB_WALK_END(pstTree, pstNode, pstWalk);
#define RB_WALK(pstTree, pstNode, pstRbWalk) do { \
LosRbWalk *(pstRbWalk) = NULL; \
pstRbWalk = LOS_RbCreateWalk(pstTree); \
(pstNode) = LOS_RbWalkNext(pstRbWalk); \
for (; NULL != (pstNode); (pstNode) = LOS_RbWalkNext(pstRbWalk)) {
#define RB_WALK_END(pstTree, pstNode, pstRbWalk) } \
LOS_RbDeleteWalk(pstRbWalk); \
} \
while (0);
寄存器操作
硬件设备的对外使用接口是 寄存器,阅读硬件生产商提供的 Datasheet(数据手册)是每个硬件工程师都需具备的基本素养。寄存器分专用寄存器和通用寄存器,驱动工程师根据数据手册配置的一般是专用寄存器,对这些寄存器不同位的设置对应了不同的功能。通用寄存器的使用一般是由编译器完成,此处不展开讲,后续编译器系列篇中会详细说明。
以协处理器 cp15 举例 ,它是CPU的助手,一共有 16个寄存器 32 位的寄存器,其编号为 C0 ~ C15 ,用来控制Cache、TCM 和存储器管理。cp15 寄存器都是复合功能寄存器,不同功能对应不同的内存实体,全由访问指令的参数来决定。读写这些寄存器必须使用MRC(编者注:协处理器寄存器到ARM处理器寄存器的数据传送指令)和MCR(编者注:ARM处理器寄存器到协处理器寄存器的数据传送指令)指令。
#define CP15_REG(CRn, Op1, CRm, Op2) "p15, "#Op1", %0, "#CRn","#CRm","#Op2
#define MIDR CP15_REG(c0, 0, c0, 0) /*! Main ID Register | 主ID寄存器 */
#define MPIDR CP15_REG(c0, 0, c0, 5) /*! Multiprocessor Affinity Register | 多处理器关联寄存器给每个CPU制定一个逻辑地址*/
#define CCSIDR CP15_REG(c0, 1, c0, 0) /*! Cache Size ID Registers | 缓存大小ID寄存器*/
#define CLIDR CP15_REG(c0, 1, c0, 1) /*! Cache Level ID Register | 缓存登记ID寄存器*/
#define VPIDR CP15_REG(c0, 4, c0, 0) /*! Virtualization Processor ID Register | 虚拟化处理器ID寄存器*/
#define VMPIDR CP15_REG(c0, 4, c0, 5) /*! Virtualization Multiprocessor ID Register | 虚拟化多处理器ID寄存器*/
#define ARM_SYSREG_READ(REG) \
({ \
UINT32 _val; \
__asm__ volatile("mrc " REG : "=r" (_val)); \
_val; \
})
#define ARM_SYSREG_WRITE(REG, val) \
({ \
__asm__ volatile("mcr " REG :: "r" (val)); \
ISB; \
})
/// 获取当前CPUID
STATIC INLINE UINT32 ArchCurrCpuid(VOID)
{
#ifdef LOSCFG_KERNEL_SMP
return ARM_SYSREG_READ(MPIDR) & MPIDR_CPUID_MASK;
#else//ARM架构通过MPIDR(Multiprocessor Affinity Register)寄存器给每个CPU指定一个逻辑地址。
return 0;
#endif
}
在单CPU多核的情况下,内核是需要安排并记录各任务运行在哪些核心上,ArchCurrCpuid是获取当前任务运行在具体哪个核上,代码中将宏 ARM_SYSREG_READ(MPIDR) 展开后变成。
({
UINT32 _val;
__asm__ volatile("mrc p15, 0, %0, c0, c0,5" : "=r"(_val));
_val;
})
- __asm__ 或 asm 用来声明一个内联汇编表达式。
- __volatile__ 或 volatile 是可选的。如果用了它,则是向编译器声明不允许对该内联汇编优化。
- 其中 %0 和 “=r”(_val) 意思是编译器将选择 R0 寄存器来接收指令结果并将 R0 的值赋给变量 _val ,为什么要这么做呢 ? 因为对协处理器的读写必须通过寄存器,而在C语言层面是不能直接操作寄存器的。
- _val; 可理解为代码块的 return方式 以便执行接下去的 & MPIDR_CPUID_MASK 操作
DSB | DMB | ISB
内核中经常会出现 DSB 、DMB 、ISB、WFI ,它们有什么含义和作用呢 ? 具体可翻看 ARM 体系参考手册 | DSB on page A8-381。
#define DSB __asm__ volatile("dsb" ::: "memory")
#define DMB __asm__ volatile("dmb" ::: "memory")
#define ISB __asm__ volatile("isb" ::: "memory")
#define WFI __asm__ volatile("wfi" ::: "memory")
#define BARRIER __asm__ volatile("":::"memory") ///< 空指令
#define WFE __asm__ volatile("wfe" ::: "memory")
#define SEV __asm__ volatile("sev" ::: "memory")
如果没有这些指令的存在会导致系统发生紊乱危象,存在的原因是因为流水线和缓冲区
缓冲区(Cache):写缓冲是为了提高存储器的总体访问效率而设的,但它会带出来一个副作用就是同步问题,会导致写内存的指令被延迟几个周期执行,因此对存储器的设置不能即刻生效,这会导致紧临着的下一条指令仍然使用旧的存储器设置——但程序员的本意显然是使用新的存储器设置。这种紊乱危象是后患无穷的,常会破坏未知地址的数据,有时也会产生非法地址访问。
流水线(Pipeline):从原理上说,计算机的流水线工作方式就是将一个计算任务细分成若干个子任务,每个子任务都由专门的功能部件进行处理,一个计算任务的各个子任务由流水线上各个功能部件轮流进行处理 (即各子任务在流水线的各个功能阶段并发执行),最终完成工作。这样,不必等到上一个计算任务完成, 就可以开始下一个计算任务的执行。当指令流不能顺序执行时,流水过程会中断(即断流)。为了保证流水过程的工作效率,流水过程不应经常断流。在一个流水过程中,实现各个流水过程的各个功能段所需要的时间应该尽可能保持相等,以避免产生瓶颈,导致流水线断流。采用流水线技术通过硬件实现并行操作后,就某一条指令而言,其执行速度并没有加快,但就程序执行过程的整体而言,程序执行速度大大加快。
指令 | 全称 | 功能 | |
---|---|---|---|
DMB | Data Memory Barrier(DMB) 数据存储器隔离 | 等待前面访存的指令完成后再执行后面的访存指令 | A3.8.3 |
DSB | Data Synchronization Barrier 数据同步隔离 | 等待所有前面的指令完成后再执行后面的访存指令 | A3.8.3 |
ISB | Instruction Synchronization Barrier(ISB) 指令同步隔离 | 等待流水线中所有指令执行完成后再执行后面的指令 | A3.8.3 |
WFI | Wait For Interrupt 等待中断 | 等待中断,进入休眠模式。 | B1.8.14 |
WFE | Wait For Event 等待事件 | 等待事件,如果没有之前该事件的记录,进入休眠模式;如果有的话,则清除事件锁存并继续执行; | B1.8.13 |
SEV | Send Event 发送事件 | 多处理器环境中向所有的处理器发送事件(包括自身)。 | B1.8.13 |
严格程度 DMB < DSB < ISB
::: “memory” 强制编译器假设 RAM 所有内存单元均被汇编指令修改,这样CPU中的寄存器 和 Cache中已缓存的内存单元中的数据将作废。CPU将不得不在需要的时候重新读取内存中的数据。这就阻止了CPU又将 寄存器, Cache中的数据用于去优化指令,而避免去访问内存。
百文说内核 | 抓住主脉络
子曰:“诗三百,一言以蔽之,曰‘思无邪’。”——《论语》:为政篇。
百文相当于摸出内核的肌肉和器官系统,让人开始丰满有立体感,因是直接从注释源码起步,在开源鸿蒙内核源码加注释过程中,每每有心得处就整理,慢慢形成了以下文章。内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感,容易理解记忆。说别人能听得懂的话很重要! 百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思。更希望让内核变得栩栩如生,倍感亲切.确实有难度,自不量力,但已经出发,回头已是不可能的了。
百万汉字注解内核目的是要看清楚其毛细血管,细胞结构,等于在拿放大镜看内核。内核并不神秘,带着问题去源码中找答案是很容易上瘾的,你会发现很多文章对一些问题的解读是错误的,或者说不深刻难以自圆其说,你会慢慢形成自己新的解读,而新的解读又会碰到新的问题,如此层层递进,滚滚向前,拿着放大镜根本不愿意放手。
与代码有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