首页 文章 精选 留言 我的

精选列表

搜索[HarmonyOS],共1236篇文章
优秀的个人博客,低调大师

鸿蒙内核源码分析(异常接管篇) | 社会很单纯 , 复杂的是人 | 中文注解HarmonyOS源码 | v39.02

百万汉字注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< gitee | github | csdn | coding > 百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,多站点每日同步更新< oschina | csdn | weharmony > 系列篇ARM部分说明基于ARM720T.pdf文档. 为何要有异常接管? 拿小孩成长打比方,大人总希望孩子能健康成长,但在成长过程中总会遇到各种各样的问题,树欲静而风不止,成长路上有危险,有时是自己的问题有时是外在环境问题.就像抖音最近的流行口水歌一样,社会很单纯,复杂的是人啊,每次听到都想站起来扭几下.哎! 老衲到底做错什么了? 比如:老被小朋友欺负怎么弄? 发现乱花钱怎么搞? 青春期发育怎么应对? 失恋要跳楼又怎么办? 意思超过他的认知范围,就是靠它自己解决不了了,就需要有更高权限,更高智慧的人介入进来,帮着解决,干擦屁股的事. 那么应用程序就是那个小孩,内核就是监护人,有更高的权限,更高的智慧.而且监护人还不止一个,而是六个,每个监护人对应解决一种情况,情况发生了就由它来接管这件事的处理,小朋友你就别管了,先把你关家里,处理好了外面安全了再把应用程序放出来玩去. 这六个人处理问题都自带工具,有标准的解决方案,有自己独立的办公场所,办公场所就是栈空间(独立的),标准解决方案就是私有代码段,放在固定的位置.而自带的工具就是 SPSR_***,SP_***,LR_***寄存器组.详见 系列篇之工作模式篇 ,这里再简单回顾下有哪些工作模式,包括小孩自己(用户模式)一共是七种模式. 七种工作模式 图来源于 ARM720T.pdf第43页,在ARM体系中,CPU工作在以下七种模式中: 用户模式(usr):属于正常的用户模式,不能直接切换到其他模式,ARM处理器正常的程序执行状态。 快速中断模式(fiq):支持高速数据传输及通道处理,FIQ异常响应时进入此模式 外部中断模式(irq):用于通用中断处理,IRQ异常响应时进入此模式 管理模式(svc):操作系统保护模式,系统复位和软件中断响应时进入此模式(由系统调用执行软中断SWI命令触发) 数据访问终止模式(abt):当数据或指令预取终止时进入该模式,可用于处理存储器故障、实现虚拟存储器和存储器保护。 系统模式(sys):运行具有特权的操作系统任务,与用户模式类似,但具有可以直接切换到其他模式等特权 未定义指令中止模式(und):处理未定义的指令陷阱,当未定义的指令执行时进入该模式,可用于支持硬件协处理器的软件仿真。 除用户模式外,其余6种工作模式都属于特权模式 特权模式中除了系统模式以外的其余5种模式称为异常模式 大多数程序运行于用户模式 进入特权模式是为了处理中断、异常、或者访问被保护的系统资源 硬件权限级别:系统模式 > 异常模式 > 用户模式 快中断(fiq)与慢中断(irq)区别:快中断处理时禁止中断 每种模式都有自己独立的入口和独立的运行栈空间. 系列篇之CPU篇 已介绍过只要提供了入口函数和运行空间,CPU就可以干活了.入口函数解决了指令来源问题,运行空间解决了指令的运行场地问题. 而且在多核情况下,每个CPU核的每种特权模式都有自己独立的栈空间.注意是特权模式下的栈空间,用户模式的栈空间是由用户(应用)程序提供的. 官方概念 异常接管是操作系统对运行期间发生的异常情况(芯片硬件异常)进行处理的一系列动作,例如打印异常发生时当前函数的调用栈信息、CPU现场信息、任务的堆栈情况等。 异常接管作为一种调测手段,可以在系统发生异常时给用户提供有用的异常信息,譬如异常类型、发生异常时的系统状态等,方便用户定位分析问题。 鸿蒙的异常接管,在系统发生异常时的处理动作为:显示异常发生时正在运行的任务信息(包括任务名、任务号、堆栈大小等),以及CPU现场等信息。 进入和退出异常方式 异常接管切换需要处理好两件事: 一个是代码要切到哪个位置,也就是要重置PC寄存器,每种异常模式下的切换方式如图: 另一个是要恢复每种模式的状态,即 CPSR(1个) 和 SPSR(共5个) 的关系,对M[4:0]的修改,如图: 以下是M[4:0]在每种模式下具体操作方式: 栈帧 每个函数都有自己的栈空间,称为栈帧。调用函数时,会创建子函数的栈帧,同时将函数入参、局部变量、寄存器入栈。栈帧从高地址向低地址生长,也就是说栈底是高地址,栈顶是底地址. 详见 系列篇之用栈方式篇 以ARM32 CPU架构为例,每个栈帧中都会保存PC、LR、SP和FP寄存器的历史值。 堆栈分析原理如下图所示,实际堆栈信息根据不同CPU架构有所差异,此处仅做示意。 图中不同颜色的寄存器表示不同的函数。可以看到函数调用过程中,寄存器的保存。通过FP寄存器,栈回溯到异常函数的父函数,继续按照规律对栈进行解析,推出函数调用关系,方便用户定位问题。 解读 LR寄存器(Link Register),链接寄存器,指向函数的返回地址。 R11:可以用作通用寄存器,在开启特定编译选项时可以用作帧指针寄存器FP,用来实现栈回溯功能。 GNU编译器(gcc)默认将R11作为存储变量的通用寄存器,因而默认情况下无法使用FP的栈回溯功能。为支持调用栈解析功能,需要在编译参数中添加-fno-omit-frame-pointer选项,提示编译器将R11作为FP使用。 FP寄存器(Frame Point),帧指针寄存器,指向当前函数的父函数的栈帧起始地址。利用该寄存器可以得到父函数的栈帧,从栈帧中获取父函数的FP,就可以得到祖父函数的栈帧,以此类推,可以追溯程序调用栈,得到函数间的调用关系。 当系统发生异常时,系统打印异常函数的栈帧中保存的寄存器内容,以及父函数、祖父函数的栈帧中的LR、FP寄存器内容,用户就可以据此追溯函数间的调用关系,定位异常原因。 六种异常模式实现代码 /* Define exception type ID */ //ARM处理器一共有7种工作模式,除了用户和系统模式其余都叫异常工作模式 #define OS_EXCEPT_RESET 0x00 //重置功能,例如:开机就进入CPSR_SVC_MODE模式 #define OS_EXCEPT_UNDEF_INSTR 0x01 //未定义的异常,就是others #define OS_EXCEPT_SWI 0x02 //软中断 #define OS_EXCEPT_PREFETCH_ABORT 0x03 //预取异常(取指异常), 指令三步骤: 取指,译码,执行, #define OS_EXCEPT_DATA_ABORT 0x04 //数据异常 #define OS_EXCEPT_FIQ 0x05 //快中断异常 #define OS_EXCEPT_ADDR_ABORT 0x06 //地址异常 #define OS_EXCEPT_IRQ 0x07 //普通中断异常 地址异常处理(Address abort) @ Description: Address abort exception handler _osExceptAddrAbortHdl: @地址异常处理 SUB LR, LR, #8 @ LR offset to return from this exception: -8. STMFD SP, {R0-R7} @ Push working registers, but don`t change SP. MOV R0, #OS_EXCEPT_ADDR_ABORT @ Set exception ID to OS_EXCEPT_ADDR_ABORT. B _osExceptDispatch @跳到异常分发统一处理 快中断处理(fiq) @ Description: Fast interrupt request exception handler _osExceptFiqHdl: @快中断异常处理 SUB LR, LR, #4 @ LR offset to return from this exception: -4. STMFD SP, {R0-R7} @ Push working registers. MOV R0, #OS_EXCEPT_FIQ @ Set exception ID to OS_EXCEPT_FIQ. B _osExceptDispatch @ Branch to global exception handler. 解读 快中断处理时需禁用普通中断 取指异常(Prefectch abort) @ Description: Prefectch abort exception handler _osExceptPrefetchAbortHdl: #ifdef LOSCFG_GDB #if __LINUX_ARM_ARCH__ >= 7 GDB_HANDLE OsPrefetchAbortExcHandleEntry #endif #else SUB LR, LR, #4 @ LR offset to return from this exception: -4. STMFD SP, {R0-R7} @ Push working registers, but don`t change SP. MOV R5, LR MRS R1, SPSR MOV R0, #OS_EXCEPT_PREFETCH_ABORT @ Set exception ID to OS_EXCEPT_PREFETCH_ABORT. AND R4, R1, #CPSR_MASK_MODE @ Interrupted mode CMP R4, #CPSR_USER_MODE @ User mode BEQ _osExcPageFault @ Branch if user mode _osKernelExceptPrefetchAbortHdl: MOV LR, R5 B _osExceptDispatch @ Branch to global exception handler. #endif 数据访问异常(Data abort) @ Description: Data abort exception handler _osExceptDataAbortHdl: @数据异常处理,缺页就属于数据异常 #ifdef LOSCFG_GDB #if __LINUX_ARM_ARCH__ >= 7 GDB_HANDLE OsDataAbortExcHandleEntry #endif #else SUB LR, LR, #8 @ LR offset to return from this exception: -8. STMFD SP, {R0-R7} @ Push working registers, but don`t change SP. MOV R5, LR MRS R1, SPSR MOV R0, #OS_EXCEPT_DATA_ABORT @ Set exception ID to OS_EXCEPT_DATA_ABORT. B _osExcPageFault @跳到缺页异常处理 #endif 软中断处理(swi) @ Description: Software interrupt exception handler _osExceptSwiHdl: @软中断异常处理 SUB SP, SP, #(4 * 16) @先申请16个栈空间用于处理本次软中断 STMIA SP, {R0-R12} @保存R0-R12寄存器值 MRS R3, SPSR @读取本模式下的SPSR值 MOV R4, LR @保存回跳寄存器LR AND R1, R3, #CPSR_MASK_MODE @ Interrupted mode 获取中断模式 CMP R1, #CPSR_USER_MODE @ User mode 是否为用户模式 BNE OsKernelSVCHandler @ Branch if not user mode 非用户模式下跳转 @ 当为用户模式时,获取SP和LR寄出去值 @ we enter from user mode, we need get the values of USER mode r13(sp) and r14(lr). @ stmia with ^ will return the user mode registers (provided that r15 is not in the register list). MOV R0, SP @获取SP值,R0将作为OsArmA32SyscallHandle的参数 STMFD SP!, {R3} @ Save the CPSR 入栈保存CPSR值 ADD R3, SP, #(4 * 17) @ Offset to pc/cpsr storage 跳到PC/CPSR存储位置 STMFD R3!, {R4} @ Save the CPSR and r15(pc) 保存LR寄存器 STMFD R3, {R13, R14}^ @ Save user mode r13(sp) and r14(lr) 保存用户模式下的SP和LR寄存器 SUB SP, SP, #4 PUSH_FPU_REGS R1 @保存中断模式(用户模式模式) MOV FP, #0 @ Init frame pointer CPSIE I @开中断,表明在系统调用期间可响应中断 BLX OsArmA32SyscallHandle /*交给C语言处理系统调用*/ CPSID I @执行后续指令前必须先关中断 POP_FPU_REGS R1 @弹出FP值给R1 ADD SP, SP,#4 @ 定位到保存旧SPSR值的位置 LDMFD SP!, {R3} @ Fetch the return SPSR 弹出旧SPSR值 MSR SPSR_cxsf, R3 @ Set the return mode SPSR 恢复该模式下的SPSR值 @ we are leaving to user mode, we need to restore the values of USER mode r13(sp) and r14(lr). @ ldmia with ^ will return the user mode registers (provided that r15 is not in the register list) LDMFD SP!, {R0-R12} @恢复R0-R12寄存器 LDMFD SP, {R13, R14}^ @ Restore user mode R13/R14 恢复用户模式的R13/R14寄存器 ADD SP, SP, #(2 * 4) @定位到保存旧PC值的位置 LDMFD SP!, {PC}^ @ Return to user 切回用户模式运行 普通中断处理(irq) OsIrqHandler: @硬中断处理,此时已切换到硬中断栈 SUB LR, LR, #4 /* push r0-r3 to irq stack */ STMFD SP, {R0-R3} @r0-r3寄存器入 irq 栈 SUB R0, SP, #(4 * 4)@r0 = sp - 16 MRS R1, SPSR @获取程序状态控制寄存器 MOV R2, LR @r2=lr /* disable irq, switch to svc mode */@超级用户模式(SVC 模式),主要用于 SWI(软件中断)和 OS(操作系统)。 CPSID i, #0x13 @切换到SVC模式,此处一切换,后续指令将入SVC的栈 @CPSID i为关中断指令,对应的是CPSIE /* push spsr and pc in svc stack */ STMFD SP!, {R1, R2} @实际是将 SPSR,和LR入栈,入栈顺序为 R1,R2,SP自增 STMFD SP, {LR} @LR再入栈,SP不自增 AND R3, R1, #CPSR_MASK_MODE @获取CPU的运行模式 CMP R3, #CPSR_USER_MODE @中断是否发生在用户模式 BNE OsIrqFromKernel @中断不发生在用户模式下则跳转到OsIrqFromKernel /* push user sp, lr in svc stack */ STMFD SP, {R13, R14}^ @sp和LR入svc栈 解读 普通中断处理时可以响应快中断 未定义异常处理(undef) @ Description: Undefined instruction exception handler _osExceptUndefInstrHdl:@出现未定义的指令处理 #ifdef LOSCFG_GDB GDB_HANDLE OsUndefIncExcHandleEntry #else @ LR offset to return from this exception: 0. STMFD SP, {R0-R7} @ Push working registers, but don`t change SP. MOV R0, #OS_EXCEPT_UNDEF_INSTR @ Set exception ID to OS_EXCEPT_UNDEF_INSTR. B _osExceptDispatch @ Branch to global exception handler. #endif 异常分发统一处理 _osExceptDispatch: @异常模式统一分发处理 MRS R2, SPSR @ Save CPSR before exception. MOV R1, LR @ Save PC before exception. SUB R3, SP, #(8 * 4) @ Save the start address of working registers. MSR CPSR_c, #(CPSR_INT_DISABLE | CPSR_SVC_MODE) @ Switch to SVC mode, and disable all interrupts MOV R5, SP EXC_SP_SET __exc_stack_top, OS_EXC_STACK_SIZE, R6, R7 STMFD SP!, {R1} @ Push Exception PC STMFD SP!, {LR} @ Push SVC LR STMFD SP!, {R5} @ Push SVC SP STMFD SP!, {R8-R12} @ Push original R12-R8, LDMFD R3!, {R4-R11} @ Move original R7-R0 from exception stack to original stack. STMFD SP!, {R4-R11} STMFD SP!, {R2} @ Push task`s CPSR (i.e. exception SPSR). CMP R0, #OS_EXCEPT_DATA_ABORT @是数据异常吗? BNE 1f @不是跳到 锚点1处 MRC P15, 0, R8, C6, C0, 0 @R8=C6(内存失效的地址) 0(访问数据失效) MRC P15, 0, R9, C5, C0, 0 @R9=C5(内存失效的状态) 0(无效整个指令cache) B 3f @跳到锚点3处执行 1: CMP R0, #OS_EXCEPT_PREFETCH_ABORT @是预取异常吗? BNE 2f @不是跳到 锚点2处 MRC P15, 0, R8, C6, C0, 2 @R8=C6(内存失效的地址) 2(访问指令失效) MRC P15, 0, R9, C5, C0, 1 @R9=C5(内存失效的状态) 1(虚拟地址) B 3f @跳到锚点3处执行 2: MOV R8, #0 MOV R9, #0 3: AND R2, R2, #CPSR_MASK_MODE CMP R2, #CPSR_USER_MODE @ User mode BNE 4f @不是用户模式 STMFD SP, {R13, R14}^ @ save user mode sp and lr 4: SUB SP, SP, #(4 * 2) @sp=sp-(4*2) 非常重要的ARM37个寄存器 详见 系列篇之寄存器篇 结尾 以上为异常接管对应的代码处理,具体每种异常发生的场景和代码细节处理,因内容太多,太复杂,系列篇后续将分篇一一分析.敬请关注! 参与贡献 访问注解仓库地址 Fork 本仓库 >> 新建 Feat_xxx 分支 >> 提交代码注解 >> 新建 Pull Request 新建 Issue 喜欢请大方 点赞+关注+收藏 吧 各大站点搜 "鸿蒙内核源码分析" ,快速找到组织. 欢迎转载,请注明出处,公众号转载申请方式: 关注后直接回复您的公众号名称即可. 百万汉字注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< gitee | github | csdn | coding > 百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,多站点每日同步更新< oschina | csdn | weharmony >

优秀的个人博客,低调大师

鸿蒙内核源码分析(异常接管篇) | 社会很单纯 , 复杂的是人 | 中文注解HarmonyOS源码 | v39.01

百万汉字注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< gitee | github | csdn | coding > 百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,多站点每日同步更新< oschina | csdn | weharmony > 系列篇ARM部分说明基于ARM720T.pdf文档. 为何要有异常接管? 拿小孩成长打比方,大人总希望孩子能健康成长,但在成长过程中总会遇到各种各样的问题,树欲静而风不止,成长路上有危险,有时是自己的问题有时是外在环境问题.就像抖音最近的流行口水歌一样,社会很单纯,复杂的是人啊,每次听到都想站起来扭几下.哎! 老衲到底做错什么了? 比如:老被小朋友欺负怎么弄? 发现乱花钱怎么搞? 青春期发育怎么应对? 失恋要跳楼又怎么办? 意思超过他的认知范围,就是靠它自己解决不了了,就需要有更高权限,更高智慧的人介入进来,帮着解决,干擦屁股的事. 那么应用程序就是那个小孩,内核就是监护人,有更高的权限,更高的智慧.而且监护人还不止一个,而是六个,每个监护人对应解决一种情况,情况发生了就由它来接管这件事的处理,小朋友你就别管了,先把你关家里,处理好了外面安全了再把应用程序放出来玩去. 这六个人处理问题都自带工具,有标准的解决方案,有自己独立的办公场所,办公场所就是栈空间(独立的),标准解决方案就是私有代码段,放在固定的位置.而自带的工具就是 SPSR_***,SP_***,LR_***寄存器组.详见 系列篇之工作模式篇 ,这里再简单回顾下有哪些工作模式,包括小孩自己(用户模式)一共是七种模式. 七种工作模式 图来源于 ARM720T.pdf第43页,在ARM体系中,CPU工作在以下七种模式中: 用户模式(usr):属于正常的用户模式,ARM处理器正常的程序执行状态。 快速中断模式(fiq):用于处理快速中断,对高速数据传输或通道处理 外部中断模式(irq):对一般情况下的中断进行处理。 管理模式(svc):属于操作系统使用的保护模式,处理软件中断swi reset。 数据访问终止模式(abt):当数据或指令预取终止时进入该模式,可用于处理存储器故障、实现虚拟存储器和存储器保护。 系统模式(sys):运行具有特权的操作系统任务。 未定义指令中止模式(und):处理未定义的指令陷阱,当未定义的指令执行时进入该模式,可用于支持硬件协处理器的软件仿真。 除了用户模式外,其它六种均为特权模式或者叫异常模式。每种模式都有自己独立的入口和独立的运行栈空间. 而且在多核情况下,每个CPU核的每种异常模式都有自己独立的栈空间.注意是异常模式下的栈空间,用户模式的栈空间是由用户(应用)程序提供的. 官方概念 异常接管是操作系统对运行期间发生的异常情况(芯片硬件异常)进行处理的一系列动作,例如打印异常发生时当前函数的调用栈信息、CPU现场信息、任务的堆栈情况等。 异常接管作为一种调测手段,可以在系统发生异常时给用户提供有用的异常信息,譬如异常类型、发生异常时的系统状态等,方便用户定位分析问题。 鸿蒙的异常接管,在系统发生异常时的处理动作为:显示异常发生时正在运行的任务信息(包括任务名、任务号、堆栈大小等),以及CPU现场等信息。 进入和退出异常方式 异常接管切换需要处理好两件事: 一个是代码要切到哪个位置,也就是要重置PC寄存器,每种异常模式下的切换方式如图: 另一个是要恢复每种模式的状态,即 CPSR(1个) 和 SPSR(共5个) 的关系,对M[4:0]的修改,如图: 以下是M[4:0]在每种模式下具体操作方式: 栈帧 每个函数都有自己的栈空间,称为栈帧。调用函数时,会创建子函数的栈帧,同时将函数入参、局部变量、寄存器入栈。栈帧从高地址向低地址生长,也就是说栈底是高地址,栈顶是底地址. 详见 系列篇之用栈方式篇 以ARM32 CPU架构为例,每个栈帧中都会保存PC、LR、SP和FP寄存器的历史值。 堆栈分析原理如下图所示,实际堆栈信息根据不同CPU架构有所差异,此处仅做示意。 图中不同颜色的寄存器表示不同的函数。可以看到函数调用过程中,寄存器的保存。通过FP寄存器,栈回溯到异常函数的父函数,继续按照规律对栈进行解析,推出函数调用关系,方便用户定位问题。 解读 LR寄存器(Link Register),链接寄存器,指向函数的返回地址。 R11:可以用作通用寄存器,在开启特定编译选项时可以用作帧指针寄存器FP,用来实现栈回溯功能。 GNU编译器(gcc)默认将R11作为存储变量的通用寄存器,因而默认情况下无法使用FP的栈回溯功能。为支持调用栈解析功能,需要在编译参数中添加-fno-omit-frame-pointer选项,提示编译器将R11作为FP使用。 FP寄存器(Frame Point),帧指针寄存器,指向当前函数的父函数的栈帧起始地址。利用该寄存器可以得到父函数的栈帧,从栈帧中获取父函数的FP,就可以得到祖父函数的栈帧,以此类推,可以追溯程序调用栈,得到函数间的调用关系。 当系统发生异常时,系统打印异常函数的栈帧中保存的寄存器内容,以及父函数、祖父函数的栈帧中的LR、FP寄存器内容,用户就可以据此追溯函数间的调用关系,定位异常原因。 六种异常模式实现代码 地址异常处理(Address abort) @ Description: Address abort exception handler _osExceptAddrAbortHdl: @地址异常处理 SUB LR, LR, #8 @ LR offset to return from this exception: -8. STMFD SP, {R0-R7} @ Push working registers, but don`t change SP. MOV R0, #OS_EXCEPT_ADDR_ABORT @ Set exception ID to OS_EXCEPT_ADDR_ABORT. B _osExceptDispatch @跳到异常分发统一处理 快中断处理(fiq) @ Description: Fast interrupt request exception handler _osExceptFiqHdl: @快中断异常处理 SUB LR, LR, #4 @ LR offset to return from this exception: -4. STMFD SP, {R0-R7} @ Push working registers. MOV R0, #OS_EXCEPT_FIQ @ Set exception ID to OS_EXCEPT_FIQ. B _osExceptDispatch @ Branch to global exception handler. 取指异常(Prefectch abort) @ Description: Prefectch abort exception handler _osExceptPrefetchAbortHdl: #ifdef LOSCFG_GDB #if __LINUX_ARM_ARCH__ >= 7 GDB_HANDLE OsPrefetchAbortExcHandleEntry #endif #else SUB LR, LR, #4 @ LR offset to return from this exception: -4. STMFD SP, {R0-R7} @ Push working registers, but don`t change SP. MOV R5, LR MRS R1, SPSR MOV R0, #OS_EXCEPT_PREFETCH_ABORT @ Set exception ID to OS_EXCEPT_PREFETCH_ABORT. AND R4, R1, #CPSR_MASK_MODE @ Interrupted mode CMP R4, #CPSR_USER_MODE @ User mode BEQ _osExcPageFault @ Branch if user mode _osKernelExceptPrefetchAbortHdl: MOV LR, R5 B _osExceptDispatch @ Branch to global exception handler. #endif 数据访问异常(Data abort) @ Description: Data abort exception handler _osExceptDataAbortHdl: @数据异常处理,缺页就属于数据异常 #ifdef LOSCFG_GDB #if __LINUX_ARM_ARCH__ >= 7 GDB_HANDLE OsDataAbortExcHandleEntry #endif #else SUB LR, LR, #8 @ LR offset to return from this exception: -8. STMFD SP, {R0-R7} @ Push working registers, but don`t change SP. MOV R5, LR MRS R1, SPSR MOV R0, #OS_EXCEPT_DATA_ABORT @ Set exception ID to OS_EXCEPT_DATA_ABORT. B _osExcPageFault @跳到缺页异常处理 #endif 软中断处理(swi) @ Description: Software interrupt exception handler _osExceptSwiHdl: @软中断异常处理 SUB SP, SP, #(4 * 16) @先申请16个栈空间用于处理本次软中断 STMIA SP, {R0-R12} @保存R0-R12寄存器值 MRS R3, SPSR @读取本模式下的SPSR值 MOV R4, LR @保存回跳寄存器LR AND R1, R3, #CPSR_MASK_MODE @ Interrupted mode 获取中断模式 CMP R1, #CPSR_USER_MODE @ User mode 是否为用户模式 BNE OsKernelSVCHandler @ Branch if not user mode 非用户模式下跳转 @ 当为用户模式时,获取SP和LR寄出去值 @ we enter from user mode, we need get the values of USER mode r13(sp) and r14(lr). @ stmia with ^ will return the user mode registers (provided that r15 is not in the register list). MOV R0, SP @获取SP值,R0将作为OsArmA32SyscallHandle的参数 STMFD SP!, {R3} @ Save the CPSR 入栈保存CPSR值 ADD R3, SP, #(4 * 17) @ Offset to pc/cpsr storage 跳到PC/CPSR存储位置 STMFD R3!, {R4} @ Save the CPSR and r15(pc) 保存LR寄存器 STMFD R3, {R13, R14}^ @ Save user mode r13(sp) and r14(lr) 保存用户模式下的SP和LR寄存器 SUB SP, SP, #4 PUSH_FPU_REGS R1 @保存中断模式(用户模式模式) MOV FP, #0 @ Init frame pointer CPSIE I @开中断,表明在系统调用期间可响应中断 BLX OsArmA32SyscallHandle /*交给C语言处理系统调用*/ CPSID I @执行后续指令前必须先关中断 POP_FPU_REGS R1 @弹出FP值给R1 ADD SP, SP,#4 @ 定位到保存旧SPSR值的位置 LDMFD SP!, {R3} @ Fetch the return SPSR 弹出旧SPSR值 MSR SPSR_cxsf, R3 @ Set the return mode SPSR 恢复该模式下的SPSR值 @ we are leaving to user mode, we need to restore the values of USER mode r13(sp) and r14(lr). @ ldmia with ^ will return the user mode registers (provided that r15 is not in the register list) LDMFD SP!, {R0-R12} @恢复R0-R12寄存器 LDMFD SP, {R13, R14}^ @ Restore user mode R13/R14 恢复用户模式的R13/R14寄存器 ADD SP, SP, #(2 * 4) @定位到保存旧PC值的位置 LDMFD SP!, {PC}^ @ Return to user 切回用户模式运行 硬中断处理(irq) OsIrqHandler: @硬中断处理,此时已切换到硬中断栈 SUB LR, LR, #4 /* push r0-r3 to irq stack */ STMFD SP, {R0-R3} @r0-r3寄存器入 irq 栈 SUB R0, SP, #(4 * 4)@r0 = sp - 16 MRS R1, SPSR @获取程序状态控制寄存器 MOV R2, LR @r2=lr /* disable irq, switch to svc mode */@超级用户模式(SVC 模式),主要用于 SWI(软件中断)和 OS(操作系统)。 CPSID i, #0x13 @切换到SVC模式,此处一切换,后续指令将入SVC的栈 @CPSID i为关中断指令,对应的是CPSIE /* push spsr and pc in svc stack */ STMFD SP!, {R1, R2} @实际是将 SPSR,和LR入栈,入栈顺序为 R1,R2,SP自增 STMFD SP, {LR} @LR再入栈,SP不自增 AND R3, R1, #CPSR_MASK_MODE @获取CPU的运行模式 CMP R3, #CPSR_USER_MODE @中断是否发生在用户模式 BNE OsIrqFromKernel @中断不发生在用户模式下则跳转到OsIrqFromKernel /* push user sp, lr in svc stack */ STMFD SP, {R13, R14}^ @sp和LR入svc栈 未定义异常处理(undef) @ Description: Undefined instruction exception handler _osExceptUndefInstrHdl:@出现未定义的指令处理 #ifdef LOSCFG_GDB GDB_HANDLE OsUndefIncExcHandleEntry #else @ LR offset to return from this exception: 0. STMFD SP, {R0-R7} @ Push working registers, but don`t change SP. MOV R0, #OS_EXCEPT_UNDEF_INSTR @ Set exception ID to OS_EXCEPT_UNDEF_INSTR. B _osExceptDispatch @ Branch to global exception handler. #endif 异常分发统一处理 _osExceptDispatch: @异常模式统一分发处理 MRS R2, SPSR @ Save CPSR before exception. MOV R1, LR @ Save PC before exception. SUB R3, SP, #(8 * 4) @ Save the start address of working registers. MSR CPSR_c, #(CPSR_INT_DISABLE | CPSR_SVC_MODE) @ Switch to SVC mode, and disable all interrupts MOV R5, SP EXC_SP_SET __exc_stack_top, OS_EXC_STACK_SIZE, R6, R7 STMFD SP!, {R1} @ Push Exception PC STMFD SP!, {LR} @ Push SVC LR STMFD SP!, {R5} @ Push SVC SP STMFD SP!, {R8-R12} @ Push original R12-R8, LDMFD R3!, {R4-R11} @ Move original R7-R0 from exception stack to original stack. STMFD SP!, {R4-R11} STMFD SP!, {R2} @ Push task`s CPSR (i.e. exception SPSR). CMP R0, #OS_EXCEPT_DATA_ABORT @是数据异常吗? BNE 1f @不是跳到 锚点1处 MRC P15, 0, R8, C6, C0, 0 @R8=C6(内存失效的地址) 0(访问数据失效) MRC P15, 0, R9, C5, C0, 0 @R9=C5(内存失效的状态) 0(无效整个指令cache) B 3f @跳到锚点3处执行 1: CMP R0, #OS_EXCEPT_PREFETCH_ABORT @是预取异常吗? BNE 2f @不是跳到 锚点2处 MRC P15, 0, R8, C6, C0, 2 @R8=C6(内存失效的地址) 2(访问指令失效) MRC P15, 0, R9, C5, C0, 1 @R9=C5(内存失效的状态) 1(虚拟地址) B 3f @跳到锚点3处执行 2: MOV R8, #0 MOV R9, #0 3: AND R2, R2, #CPSR_MASK_MODE CMP R2, #CPSR_USER_MODE @ User mode BNE 4f @不是用户模式 STMFD SP, {R13, R14}^ @ save user mode sp and lr 4: SUB SP, SP, #(4 * 2) @sp=sp-(4*2) 非常重要的ARM37个寄存器 详见 系列篇之寄存器篇 结尾 以上为异常接管对应的代码处理,具体每种异常发生的场景和代码细节处理,因内容太多,太复杂,系列篇后续将分篇一一分析.敬请关注! 参与贡献 访问注解仓库地址 Fork 本仓库 >> 新建 Feat_xxx 分支 >> 提交代码注解 >> 新建 Pull Request 新建 Issue 喜欢就大方 点赞+关注+收藏 吧 各大站点搜 "鸿蒙内核源码分析" ,快速找到组织, 欢迎转载,请注明出处. 百万汉字注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< gitee | github | csdn | coding > 百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,多站点每日同步更新< oschina | csdn | weharmony >

优秀的个人博客,低调大师

| 中文注解HarmonyOS源码 | v36.01

百万汉字注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< Gitee | Github | CSDN | Coding > 百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,多站点每日同步更新< OSCHINA | CSDN | WeHarmony > 系列篇硬件部分说明基于ARM720T.pdf文档. 本篇说清楚CPU的工作模式 读本篇之前建议先读鸿蒙内核源码分析(总目录)其他篇. 正如一个互联网项目的后台管理系统有权限管理一样,CPU工作是否也有权限(模式)? 一个成熟的软硬件架构,肯定会有这些设计,只是大部分人不知道,也不需要知道,老百姓就干好老百姓的活就行了,有工作能吃饱饭就知足了,宫的事你管那么多干嘛,你也管不了. 应用程序就只关注应用功能,业务逻辑相关的部分就行了,底层实现对应用层屏蔽的越干净系统设计的就越优良. 但鸿蒙内核源码分析系列篇的定位就是要把整个底层解剖,全部掰开,看看宫里究竟发生了么事.从本篇开始要接触大量的汇编的代码,将鸿蒙内核的每段汇编代码一一说明白.如此才能知道最开始的开始发生了什么,最后的最后又发生了什么. 七种模式 先看一张图,图来源于 ARM720T.pdf 第43页,在ARM体系中,CPU工作在以下七种模式中: 用户模式(usr):属于正常的用户模式,ARM处理器正常的程序执行状态。 快速中断模式(fiq):用于处理快速中断,对高速数据传输或通道处理 外部中断模式(irq):对一般情况下的中断进行处理。 管理模式(svc):属于操作系统使用的保护模式,处理软件中断swi reset。 数据访问终止模式(abt):当数据或指令预取终止时进入该模式,可用于处理存储器故障、实现虚拟存储器和存储器保护。 系统模式(sys):运行具有特权的操作系统任务。 未定义指令中止模式(und):处理未定义的指令陷阱,当未定义的指令执行时进入该模式,可用于支持硬件协处理器的软件仿真。 除了用户模式外,其它六种均为特权模式或者叫异常模式。每种模式都有自己独立的入口和独立的运行栈空间.系列篇之CPU篇已介绍过只要提供了入口函数和运行空间,CPU就可以干活了.入口函数解决了指令来源问题,运行空间解决了指令的运行问题. 而且在多核情况下,每个CPU核的每种异常模式都有自己独立的栈空间.注意是异常模式下的栈空间,用户模式的栈空间是由用户(应用)程序提供的. 如何让这七种模式能流畅的跑起来呢? 至少需要以下解决三个基本问题. 栈空间是怎么申请的?申请了多大? 被切换中的模式代码放在哪里?谁来安排它们放在哪里? 模式之间是怎么切换的?状态怎么保存? 本篇代码来源于鸿蒙内核源码之reset_vector_mp.S,点击查看 这个汇编文件大概 500多行,非常重要,本篇受限于篇幅只列出一小部分,说清楚以上三个问题.系列其余篇中将详细说明每段汇编代码的作用和实现,可前往查阅. 1.异常模式栈空间怎么申请? 鸿蒙是如何给异常模式申请栈空间的 #define CORE_NUM LOSCFG_KERNEL_SMP_CORE_NUM //CPU 核数 #ifdef LOSCFG_GDB #define OS_EXC_UNDEF_STACK_SIZE 512 #define OS_EXC_ABT_STACK_SIZE 512 #else #define OS_EXC_UNDEF_STACK_SIZE 40 #define OS_EXC_ABT_STACK_SIZE 40 #endif #define OS_EXC_FIQ_STACK_SIZE 64 #define OS_EXC_IRQ_STACK_SIZE 64 #define OS_EXC_SVC_STACK_SIZE 0x2000 //8K #define OS_EXC_STACK_SIZE 0x1000 //4K @六种特权模式申请对应的栈运行空间 __undef_stack: .space OS_EXC_UNDEF_STACK_SIZE * CORE_NUM __undef_stack_top: __abt_stack: .space OS_EXC_ABT_STACK_SIZE * CORE_NUM __abt_stack_top: __irq_stack: .space OS_EXC_IRQ_STACK_SIZE * CORE_NUM __irq_stack_top: __fiq_stack: .space OS_EXC_FIQ_STACK_SIZE * CORE_NUM __fiq_stack_top: __svc_stack: .space OS_EXC_SVC_STACK_SIZE * CORE_NUM __svc_stack_top: __exc_stack: .space OS_EXC_STACK_SIZE * CORE_NUM __exc_stack_top: 代码解读 六种异常模式都有自己独立的栈空间 每种模式的OS_EXC_***_STACK_SIZE栈大小都不一样,最大是管理模式(svc)8K,最小的只有40个字节. svc模式为什么要这么大呢? 因为开机代码和系统调用代码的运行都在管理模式,系统调用的函数实现往往较复杂,最大不能超过8K. 例如:某个系统调用中定义一个8K的局部变量,内核肯定立马闪蹦.因为栈将溢出,处理异常的程序出现了异常,后面就再也没人兜底了,只能是死局. 鸿蒙是支持多核处理的,CORE_NUM表明,每个CPU核的每种异常模式都有自己的独立栈空间.注意理解这个是理解内核代码的基础.否则会一头雾水. 2.异常模式入口地址在哪? 再看一张图,图来源于 ARM720T.pdf 第56页 这就是一切一切的开始,指定所有异常模式的入口地址表,这就是规定,没得商量的.在低地址情况下.开机代码就是放在 0x00000000的位置, 触发开机键后,硬件将PC寄存器置为0x00000000,开始了万里长征的第一步.在系统运行过程中就这么来回跳. b reset_vector @开机代码 b _osExceptUndefInstrHdl @异常处理之CPU碰到不认识的指令 b _osExceptSwiHdl @异常处理之:软中断 b _osExceptPrefetchAbortHdl @异常处理之:取指异常 b _osExceptDataAbortHdl @异常处理之:数据异常 b _osExceptAddrAbortHdl @异常处理之:地址异常 b OsIrqHandler @异常处理之:硬中断 b _osExceptFiqHdl @异常处理之:快中断 以上是各个异常情况下的入口地址,在reset_vector_mp.S中都能找到,经过编译链接后就会变成 b 0x00000000 @开机代码 b 0x00000004 @异常处理之CPU碰到不认识的指令 b 0x00000008 @异常处理之:软中断 b 0x0000000C @异常处理之:取指异常 b 0x00000010 @异常处理之:数据异常 b 0x00000014 @异常处理之:地址异常 b 0x00000018 @异常处理之:硬中断 b 0x0000001C @异常处理之:快中断 不管是主动切换的异常,还是被动切换的异常,都会先跳到对应的入口去处理.每个异常的代码都起始于汇编,处理完了再切回去.举个例子: 某个应用程序调用了系统调用(比如创建定时器),会经过以下大致过程: swi指令将用户模式切换到管理模式(svc) 在管理模式中先保存用户模式的现场信息(R0-R15寄存器值入栈) 获取系统调用号,知道是调用了哪个系统调用 查询系统调用对应的注册函数 执行真正的创建定时器函数 执行完成后,恢复用户模式的现场信息(R0-R15寄存器值出栈) 跳回用户模式继续执行 各异常处理代码很多,不一一列出,本篇只列出开机代码,请尝试读懂鸿蒙内核开机代码,后续讲详细说明每行代码的用处. 开机代码 reset_vector: //开机代码 /* clear register TPIDRPRW */ mov r0, #0 @r0 = 0 mcr p15, 0, r0, c13, c0, 4 @c0,c13 = 0, C13为进程标识符 含义见 ARM720T.PDF 第64页 /* do some early cpu setup: i/d cache disable, mmu disabled */ @禁用MMU, i/d缓存 mrc p15, 0, r0, c1, c0, 0 @r0 = c1 ,c1寄存器详细解释见第64页 bic r0, #(1<<12) @位清除指令,清除r0的第11位 bic r0, #(1<<2 | 1<<0) @清除第0和2位 ,禁止 MMU和缓存 0位:MMU enable/disable 2位:Cache enable/disable mcr p15, 0, r0, c1, c0, 0 @c1=r0 /* r11: delta of physical address and virtual address */@物理地址和虚拟地址的增量 adr r11, pa_va_offset @将基于PC相对偏移的地址pa_va_offset值读取到寄存器R11中 ldr r0, [r11] @将R11的值给r0 sub r11, r11, r0 @r11 = r11 - r0 mrc p15, 0, r12, c0, c0, 5 /* r12: get cpuid */ @获取CPUID and r12, r12, #MPIDR_CPUID_MASK @r12经过掩码过滤 cmp r12, #0 @当前是否为0号CPU bne secondary_cpu_init @不是0号主CPU则调用secondary_cpu_init /* if we need to relocate to proper location or not */ adr r4, __exception_handlers /* r4: base of load address */ @r4获得加载基地址 ldr r5, =SYS_MEM_BASE /* r5: base of physical address */@r5获得物理基地址 subs r12, r4, r5 /* r12: delta of load address and physical address */ @r12=r4-r5 加载地址和物理地址的增量 beq reloc_img_to_bottom_done /* if we load image at the bottom of physical address */ /* we need to relocate image at the bottom of physical address */ ldr r7, =__exception_handlers /* r7: base of linked address (or vm address) */ ldr r6, =__bss_start /* r6: end of linked address (or vm address) */ sub r6, r7 /* r6: delta of linked address (or vm address) */ add r6, r4 /* r6: end of load address */ 异常的权限 当同时出现多个异常时,该响应哪一个呢?就涉及到了异常的权限,如下 Reset (highest priority). Data Abort. FIQ. IRQ. Prefetch Abort. Undefined Instruction, SWI (lowest priority). 可以看出swi的权限最低,swi就是软件中断,系统调用就是通过它来实现的. 3.异常模式怎么切换? 写应用程序经常会用到状态,来记录各种分支逻辑,传递参数.这么多异常模式,相互切换,中间肯定会有很多的状态需要保存.比如:如何能知道当前运行在哪种模式下?怎么查?去哪里查呢? 答案是: CPSR 和 SPSR CPSR:程序状态寄存器(current program status register) (当前程序状态寄存器),在任何处理器模式下被访问。 SPSR:程序状态保存寄存器(saved program status register),每一种处理器模式下都有一个状态寄存器SPSR,SPSR用于保存CPSR的状态,以便异常返回后恢复异常发生时的工作状态。当特定 的异常中断发生时,这个寄存器用于存放当前程序状态寄存器的内容。在异常中断退出时,可以用SPSR来恢复CPSR。 这些寄存器: 保存有关最近执行的ALU操作的信息 控制中断的启用和禁用 设置处理器操作模式 喜欢就大方 点赞+关注+收藏 吧 各大站点搜 "鸿蒙内核源码分析" ,快速找到组织. 百万汉字注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< Gitee | Github | CSDN | Coding > 百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,多站点每日同步更新< OSCHINA | CSDN | WeHarmony >

优秀的个人博客,低调大师

| 中文注解HarmonyOS源码 | v36.02

百万汉字注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< Gitee | Github | CSDN | Coding > 百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,多站点每日同步更新< OSCHINA | CSDN | WeHarmony > 系列篇硬件部分说明基于ARM720T.pdf文档. 本篇说清楚CPU的工作模式 读本篇之前建议先读鸿蒙内核源码分析(总目录)其他篇. 正如一个互联网项目的后台管理系统有权限管理一样,CPU工作是否也有权限(模式)? 一个成熟的软硬件架构,肯定会有这些设计,只是大部分人不知道,也不需要知道,老百姓就干好老百姓的活就行了,有工作能吃饱饭就知足了,宫的事你管那么多干嘛,你也管不了. 应用程序就只关注应用功能,业务逻辑相关的部分就行了,底层实现对应用层屏蔽的越干净系统设计的就越优良. 但鸿蒙内核源码分析系列篇的定位就是要把整个底层解剖,全部掰开,看看宫里究竟发生了么事.从本篇开始要接触大量的汇编的代码,将鸿蒙内核的每段汇编代码一一说明白.如此才能知道最开始的开始发生了什么,最后的最后又发生了什么. 七种模式 先看一张图,图来源于 ARM720T.pdf第43页,在ARM体系中,CPU工作在以下七种模式中: 用户模式(usr):属于正常的用户模式,ARM处理器正常的程序执行状态。 快速中断模式(fiq):用于处理快速中断,对高速数据传输或通道处理 外部中断模式(irq):对一般情况下的中断进行处理。 管理模式(svc):属于操作系统使用的保护模式,处理软件中断swi reset。 数据访问终止模式(abt):当数据或指令预取终止时进入该模式,可用于处理存储器故障、实现虚拟存储器和存储器保护。 系统模式(sys):运行具有特权的操作系统任务。 未定义指令中止模式(und):处理未定义的指令陷阱,当未定义的指令执行时进入该模式,可用于支持硬件协处理器的软件仿真。 除了用户模式外,其它六种均为特权模式或者叫异常模式。每种模式都有自己独立的入口和独立的运行栈空间.系列篇之CPU篇已介绍过只要提供了入口函数和运行空间,CPU就可以干活了.入口函数解决了指令来源问题,运行空间解决了指令的运行问题. 而且在多核情况下,每个CPU核的每种异常模式都有自己独立的栈空间.注意是异常模式下的栈空间,用户模式的栈空间是由用户(应用)程序提供的. 如何让这七种模式能流畅的跑起来呢? 至少需要以下解决三个基本问题. 栈空间是怎么申请的?申请了多大? 被切换中的模式代码放在哪里?谁来安排它们放在哪里? 模式之间是怎么切换的?状态怎么保存? 本篇代码来源于鸿蒙内核源码之reset_vector_mp.S,点击查看 这个汇编文件大概 500多行,非常重要,本篇受限于篇幅只列出一小部分,说清楚以上三个问题.系列其余篇中将详细说明每段汇编代码的作用和实现,可前往查阅. 1.异常模式栈空间怎么申请? 鸿蒙是如何给异常模式申请栈空间的 #define CORE_NUM LOSCFG_KERNEL_SMP_CORE_NUM //CPU 核数 #ifdef LOSCFG_GDB #define OS_EXC_UNDEF_STACK_SIZE 512 #define OS_EXC_ABT_STACK_SIZE 512 #else #define OS_EXC_UNDEF_STACK_SIZE 40 #define OS_EXC_ABT_STACK_SIZE 40 #endif #define OS_EXC_FIQ_STACK_SIZE 64 #define OS_EXC_IRQ_STACK_SIZE 64 #define OS_EXC_SVC_STACK_SIZE 0x2000 //8K #define OS_EXC_STACK_SIZE 0x1000 //4K @六种特权模式申请对应的栈运行空间 __undef_stack: .space OS_EXC_UNDEF_STACK_SIZE * CORE_NUM __undef_stack_top: __abt_stack: .space OS_EXC_ABT_STACK_SIZE * CORE_NUM __abt_stack_top: __irq_stack: .space OS_EXC_IRQ_STACK_SIZE * CORE_NUM __irq_stack_top: __fiq_stack: .space OS_EXC_FIQ_STACK_SIZE * CORE_NUM __fiq_stack_top: __svc_stack: .space OS_EXC_SVC_STACK_SIZE * CORE_NUM __svc_stack_top: __exc_stack: .space OS_EXC_STACK_SIZE * CORE_NUM __exc_stack_top: 代码解读 六种异常模式都有自己独立的栈空间 每种模式的OS_EXC_***_STACK_SIZE栈大小都不一样,最大是管理模式(svc)8K,最小的只有40个字节. svc模式为什么要这么大呢? 因为开机代码和系统调用代码的运行都在管理模式,系统调用的函数实现往往较复杂,最大不能超过8K. 例如:某个系统调用中定义一个8K的局部变量,内核肯定立马闪蹦.因为栈将溢出,处理异常的程序出现了异常,后面就再也没人兜底了,只能是死局. 鸿蒙是支持多核处理的,CORE_NUM表明,每个CPU核的每种异常模式都有自己的独立栈空间.注意理解这个是理解内核代码的基础.否则会一头雾水. 2.异常模式入口地址在哪? 再看一张图,图来源于 ARM720T.pdf 第56页 这就是一切一切的开始,指定所有异常模式的入口地址表,这就是规定,没得商量的.在低地址情况下.开机代码就是放在 0x00000000的位置, 触发开机键后,硬件将PC寄存器置为0x00000000,开始了万里长征的第一步.在系统运行过程中就这么来回跳. b reset_vector @开机代码 b _osExceptUndefInstrHdl @异常处理之CPU碰到不认识的指令 b _osExceptSwiHdl @异常处理之:软中断 b _osExceptPrefetchAbortHdl @异常处理之:取指异常 b _osExceptDataAbortHdl @异常处理之:数据异常 b _osExceptAddrAbortHdl @异常处理之:地址异常 b OsIrqHandler @异常处理之:硬中断 b _osExceptFiqHdl @异常处理之:快中断 以上是各个异常情况下的入口地址,在reset_vector_mp.S中都能找到,经过编译链接后就会变成 b 0x00000000 @开机代码 b 0x00000004 @异常处理之CPU碰到不认识的指令 b 0x00000008 @异常处理之:软中断 b 0x0000000C @异常处理之:取指异常 b 0x00000010 @异常处理之:数据异常 b 0x00000014 @异常处理之:地址异常 b 0x00000018 @异常处理之:硬中断 b 0x0000001C @异常处理之:快中断 不管是主动切换的异常,还是被动切换的异常,都会先跳到对应的入口去处理.每个异常的代码都起始于汇编,处理完了再切回去. 举个例子:某个应用程序调用了系统调用(比如创建定时器),会经过以下大致过程: swi指令将用户模式切换到管理模式(svc) 在管理模式中先保存用户模式的现场信息(R0-R15寄存器值入栈) 获取系统调用号,知道是调用了哪个系统调用 查询系统调用对应的注册函数 执行真正的创建定时器函数 执行完成后,恢复用户模式的现场信息(R0-R15寄存器值出栈) 跳回用户模式继续执行 各异常处理代码很多,不一一列出,本篇只列出开机代码,请尝试读懂鸿蒙内核开机代码,后续详细说明每行代码的用处. 开机代码 reset_vector: //开机代码 /* clear register TPIDRPRW */ mov r0, #0 @r0 = 0 mcr p15, 0, r0, c13, c0, 4 @c0,c13 = 0, C13为进程标识符 含义见 ARM720T.PDF 第64页 /* do some early cpu setup: i/d cache disable, mmu disabled */ @禁用MMU, i/d缓存 mrc p15, 0, r0, c1, c0, 0 @r0 = c1 ,c1寄存器详细解释见第64页 bic r0, #(1<<12) @位清除指令,清除r0的第11位 bic r0, #(1<<2 | 1<<0) @清除第0和2位 ,禁止 MMU和缓存 0位:MMU enable/disable 2位:Cache enable/disable mcr p15, 0, r0, c1, c0, 0 @c1=r0 /* r11: delta of physical address and virtual address */@物理地址和虚拟地址的增量 adr r11, pa_va_offset @将基于PC相对偏移的地址pa_va_offset值读取到寄存器R11中 ldr r0, [r11] @将R11的值给r0 sub r11, r11, r0 @r11 = r11 - r0 mrc p15, 0, r12, c0, c0, 5 /* r12: get cpuid */ @获取CPUID and r12, r12, #MPIDR_CPUID_MASK @r12经过掩码过滤 cmp r12, #0 @当前是否为0号CPU bne secondary_cpu_init @不是0号主CPU则调用secondary_cpu_init /* if we need to relocate to proper location or not */ adr r4, __exception_handlers /* r4: base of load address */ @r4获得加载基地址 ldr r5, =SYS_MEM_BASE /* r5: base of physical address */@r5获得物理基地址 subs r12, r4, r5 /* r12: delta of load address and physical address */ @r12=r4-r5 加载地址和物理地址的增量 beq reloc_img_to_bottom_done /* if we load image at the bottom of physical address */ /* we need to relocate image at the bottom of physical address */ ldr r7, =__exception_handlers /* r7: base of linked address (or vm address) */ ldr r6, =__bss_start /* r6: end of linked address (or vm address) */ sub r6, r7 /* r6: delta of linked address (or vm address) */ add r6, r4 /* r6: end of load address */ 异常的优先级 当同时出现多个异常时,该响应哪一个呢?这涉及到了异常的优先级,顺序如下 Reset (highest priority). Data Abort. FIQ. IRQ. Prefetch Abort. Undefined Instruction, SWI (lowest priority). 可以看出swi的优先级最低,swi就是软中断,系统调用就是通过它来实现的. 3.异常模式怎么切换? 写应用程序经常会用到状态,来记录各种分支逻辑,传递参数.这么多异常模式,相互切换,中间肯定会有很多的状态需要保存.比如:如何能知道当前运行在哪种模式下?怎么查?去哪里查呢? 答案是: CPSR 和 SPSR CPSR:程序状态寄存器(current program status register) (当前程序状态寄存器),在任何处理器模式下被访问。 SPSR:程序状态保存寄存器(saved program status register),每一种处理器模式下都有一个状态寄存器SPSR,SPSR用于保存CPSR的状态,以便异常返回后恢复异常发生时的工作状态。当特定 的异常中断发生时,这个寄存器用于存放当前程序状态寄存器的内容。在异常中断退出时,可以用SPSR来恢复CPSR。 这些寄存器: 保存有关最近执行的ALU操作的信息 控制中断的启用和禁用 设置处理器操作模式 喜欢就大方 点赞+关注+收藏 吧 各大站点搜 "鸿蒙内核源码分析" ,快速找到组织. 百万汉字注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< Gitee | Github | CSDN | Coding > 百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,多站点每日同步更新< OSCHINA | CSDN | WeHarmony >

优秀的个人博客,低调大师

鸿蒙内核源码分析(互斥锁篇) | 互斥锁比自旋锁丰满多了 | 中文注解HarmonyOS源码 | v27.01

鸿蒙内核源码中文注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆, 四大源码仓每日同步更新< Gitee | Github | CSDN | Coding > 鸿蒙内核源码分析博客 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点每日同步更新< OSCHINA | CSDN | WeHarmony > 本篇说清楚互斥锁 读本篇之前建议先读鸿蒙内核源码分析(总目录)之自旋锁篇. 内核中哪些模块会用到互斥锁?看图: 图中是内核有关模块对互斥锁初始化,有文件,有内存,用消息队列等等,使用面非常的广.其实在给内核源码加注的过程中,会看到大量的自旋锁和互斥锁,它们的存在有序的保证了内核和应用程序的正常运行.是非常基础和重要的功能. 概述 自旋锁 和 互斥锁 虽都是锁,但解决的问题不同, 自旋锁解决用于CPU核间共享内存的竞争,而互斥锁解决线程(任务)间共享内存的竞争. 自旋锁的特点是死守共享资源,拿不到锁,CPU选择睡眠,等待其他CPU释放资源.所以共享代码段不能太复杂,否则容易死锁,休克. 互斥锁的特点是拿不到锁往往原任务阻塞,切换到新任务运行.CPU是会一直跑的.这样很容易会想到几个问题: 第一:会出现很多任务在等同一把锁的情况出现,因为切换新任务也可能因要同一把锁而被阻塞,CPU又被调去跑新新任务了.这样就会出现一个等锁的链表. 第二:持有锁的一方再申请同一把锁时还能成功吗? 答案是可以的,这种锁叫递归锁,是鸿蒙内核默认方式. 第三:当优先级很高的A任务要锁失败,主动让出CPU进入睡眠,而如果持有锁的B任务优先级很低, 迟迟等不到调度不到B任务运行,无法释放锁怎么办? 答案是会临时调整B任务的优先级,调到A一样高,这样B能很快的被调度到,等B释放锁后其优先级又会被打回原形.所以一个任务的优先级会看情况时高时低. 第四:B任务释放锁之后要主动唤醒等锁的任务链表,使他们能加入就绪队列,等待被调度.调度算法是一视同仁的,它只看优先级. 带着这些问题,进入鸿蒙内核互斥锁的实现代码,本篇代码量较大, 每行代码都一一注解说明. 互斥锁长什么样? enum { LOS_MUX_PRIO_NONE = 0, //线程的优先级和调度不会受到互斥锁影响,先来后到,普通排队. LOS_MUX_PRIO_INHERIT = 1, //当高优先级的等待低优先级的线程释放锁时,低优先级的线程以高优先级线程的优先级运行。 //当线程解锁互斥量时,线程的优先级自动被将到它原来的优先级 LOS_MUX_PRIO_PROTECT = 2 //详见:OsMuxPendOp中的注解,详细说明了LOS_MUX_PRIO_PROTECT的含义 }; enum { LOS_MUX_NORMAL = 0, //非递归锁 只有[0.1]两个状态,不做任何特殊的错误检,不进行deadlock detection(死锁检测) LOS_MUX_RECURSIVE = 1, //递归锁 允许同一线程在互斥量解锁前对该互斥量进行多次加锁。递归互斥量维护锁的计数,在解锁次数和加锁次数不相同的情况下,不会释放锁,别的线程就无法加锁此互斥量。 LOS_MUX_ERRORCHECK = 2, //进行错误检查,如果一个线程企图对一个已经锁住的mutex进行relock或对未加锁的unlock,将返回一个错误。 LOS_MUX_DEFAULT = LOS_MUX_RECURSIVE //鸿蒙系统默认使用递归锁 }; typedef struct { //互斥锁的属性 UINT8 protocol; //协议 UINT8 prioceiling; //优先级上限 UINT8 type; //类型属性 UINT8 reserved; //保留字段 } LosMuxAttr; typedef struct OsMux { //互斥锁结构体 UINT32 magic; /**< magic number */ //魔法数字 LosMuxAttr attr; /**< Mutex attribute */ //互斥锁属性 LOS_DL_LIST holdList; /**< The task holding the lock change */ //当有任务拿到本锁时,通过holdList节点把锁挂到该任务的锁链表上 LOS_DL_LIST muxList; /**< Mutex linked list */ //等这个锁的任务链表,上面挂的都是任务,注意和holdList的区别. VOID *owner; /**< The current thread that is locking a mutex */ //当前拥有这把锁的任务 UINT16 muxCount; /**< Times of locking a mutex */ //锁定互斥体的次数,递归锁允许多次 } LosMux; 这互斥锁长的明显的比自旋锁丰满多啦,还记得自旋锁的样子吗,就一个变量,单薄到令人心疼. 初始化 LITE_OS_SEC_TEXT UINT32 LOS_MuxInit(LosMux *mutex, const LosMuxAttr *attr) { //... SCHEDULER_LOCK(intSave); //拿到调度自旋锁 mutex->muxCount = 0; //锁定互斥量的次数 mutex->owner = NULL; //持有该锁的任务 LOS_ListInit(&mutex->muxList); //初始化等待该锁的任务链表 mutex->magic = OS_MUX_MAGIC; //固定标识,互斥锁的魔法数字 SCHEDULER_UNLOCK(intSave); //释放调度自旋锁 return LOS_OK; } 留意mutex->muxList,这又是一个双向链表, 双向链表是内核最重要的结构体,不仅仅是鸿蒙内核,在linux内核中(list_head)又何尝不是,牢牢的寄生在宿主结构体上.muxList上挂的是未来所有等待这把锁的任务. 三种申请模式 申请互斥锁有三种模式:无阻塞模式、永久阻塞模式、定时阻塞模式。 无阻塞模式:即任务申请互斥锁时,入参timeout等于0。若当前没有任务持有该互斥锁,或者持有该互斥锁的任务和申请该互斥锁的任务为同一个任务,则申请成功,否则立即返回申请失败。 永久阻塞模式:即任务申请互斥锁时,入参timeout等于0xFFFFFFFF。若当前没有任务持有该互斥锁,则申请成功。否则,任务进入阻塞态,系统切换到就绪任务中优先级最高者继续执行。任务进入阻塞态后,直到有其他任务释放该互斥锁,阻塞任务才会重新得以执行。 定时阻塞模式:即任务申请互斥锁时,0<timeout<0xFFFFFFFF。若当前没有任务持有该互斥锁,则申请成功。否则该任务进入阻塞态,系统切换到就绪任务中优先级最高者继续执行。任务进入阻塞态后,超时前如果有其他任务释放该互斥锁,则该任务可成功获取互斥锁继续执行,若超时前未获取到该互斥锁,接口将返回超时错误码。 如果有任务阻塞于该互斥锁,则唤醒被阻塞任务中优先级最高的,该任务进入就绪态,并进行任务调度。 如果没有任务阻塞于该互斥锁,则互斥锁释放成功。 申请互斥锁主函数 OsMuxPendOp //互斥锁的主体函数,由OsMuxlockUnsafe调用,互斥锁模块最重要的几个函数之一 //最坏情况就是拿锁失败,让出CPU,变成阻塞任务,等别的任务释放锁后排到自己了接着执行. STATIC UINT32 OsMuxPendOp(LosTaskCB *runTask, LosMux *mutex, UINT32 timeout) { UINT32 ret; LOS_DL_LIST *node = NULL; LosTaskCB *owner = NULL; if ((mutex->muxList.pstPrev == NULL) || (mutex->muxList.pstNext == NULL)) {//列表为空时的处理 /* This is for mutex macro initialization. */ mutex->muxCount = 0;//锁计数器清0 mutex->owner = NULL;//锁没有归属任务 LOS_ListInit(&mutex->muxList);//初始化锁的任务链表,后续申请这把锁任务都会挂上去 } if (mutex->muxCount == 0) {//无task用锁时,肯定能拿到锁了.在里面返回 mutex->muxCount++; //互斥锁计数器加1 mutex->owner = (VOID *)runTask; //当前任务拿到锁 LOS_ListTailInsert(&runTask->lockList, &mutex->holdList);//持有锁的任务改变了,节点挂到当前task的锁链表 if ((runTask->priority > mutex->attr.prioceiling) && (mutex->attr.protocol == LOS_MUX_PRIO_PROTECT)) {//看保护协议的做法是怎样的? LOS_BitmapSet(&runTask->priBitMap, runTask->priority);//1.priBitMap是记录任务优先级变化的位图,这里把任务当前的优先级记录在priBitMap OsTaskPriModify(runTask, mutex->attr.prioceiling);//2.把高优先级的mutex->attr.prioceiling设为当前任务的优先级. }//注意任务优先级有32个, 是0最高,31最低!!!这里等于提高了任务的优先级,目的是让其在下次调度中继续提高被选中的概率,从而快速的释放锁. return LOS_OK; } //递归锁muxCount>0 如果是递归锁就要处理两种情况 1.runtask持有锁 2.锁被别的任务拿走了 if (((LosTaskCB *)mutex->owner == runTask) && (mutex->attr.type == LOS_MUX_RECURSIVE)) {//第一种情况 runtask是锁持有方 mutex->muxCount++; //递归锁计数器加1,递归锁的目的是防止死锁,鸿蒙默认用的就是递归锁(LOS_MUX_DEFAULT = LOS_MUX_RECURSIVE) return LOS_OK; //成功退出 } //到了这里说明锁在别的任务那里,当前任务只能被阻塞了. if (!timeout) {//参数timeout表示等待多久再来拿锁 return LOS_EINVAL;//timeout = 0表示不等了,没拿到锁就返回不纠结,返回错误.见于LOS_MuxTrylock } //自己要被阻塞,只能申请调度,让出CPU core 让别的任务上 if (!OsPreemptableInSched()) {//不能申请调度 (不能调度的原因是因为没有持有调度任务自旋锁) return LOS_EDEADLK;//返回错误,自旋锁被别的CPU core 持有 } OsMuxBitmapSet(mutex, runTask, (LosTaskCB *)mutex->owner);//设置锁位图,尽可能的提高锁持有任务的优先级 owner = (LosTaskCB *)mutex->owner; //记录持有锁的任务 runTask->taskMux = (VOID *)mutex; //记下当前任务在等待这把锁 node = OsMuxPendFindPos(runTask, mutex);//在等锁链表中找到一个优先级比当前任务更低的任务 ret = OsTaskWait(node, timeout, TRUE);//task陷入等待状态 TRUE代表需要调度 if (ret == LOS_ERRNO_TSK_TIMEOUT) {//这行代码虽和OsTaskWait挨在一起,但要过很久才会执行到,因为在OsTaskWait中CPU切换了任务上下文 runTask->taskMux = NULL;// 所以重新回到这里时可能已经超时了 ret = LOS_ETIMEDOUT;//返回超时 } if (timeout != LOS_WAIT_FOREVER) {//不是永远等待的情况 OsMuxBitmapRestore(mutex, runTask, owner);//恢复锁的位图 } return ret; } 释放锁的主体函数 OsMuxPostOp //是否有其他任务持有互斥锁而处于阻塞状,如果是就要唤醒它,注意唤醒一个任务的操作是由别的任务完成的 //OsMuxPostOp只由OsMuxUnlockUnsafe,参数任务归还锁了,自然就会遇到锁要给谁用的问题, 因为很多任务在申请锁,由OsMuxPostOp来回答这个问题 STATIC UINT32 OsMuxPostOp(LosTaskCB *taskCB, LosMux *mutex, BOOL *needSched) { LosTaskCB *resumedTask = NULL; if (LOS_ListEmpty(&mutex->muxList)) {//如果互斥锁列表为空 LOS_ListDelete(&mutex->holdList);//把持有互斥锁的节点摘掉 mutex->owner = NULL; return LOS_OK; } resumedTask = OS_TCB_FROM_PENDLIST(LOS_DL_LIST_FIRST(&(mutex->muxList)));//拿到等待互斥锁链表的第一个任务实体,接下来要唤醒任务 if (mutex->attr.protocol == LOS_MUX_PRIO_INHERIT) {//互斥锁属性协议是继承会怎么操作? if (resumedTask->priority > taskCB->priority) {//拿到锁的任务优先级低于参数任务优先级 if (LOS_HighBitGet(taskCB->priBitMap) != resumedTask->priority) {//参数任务bitmap中最低的优先级不等于等待锁的任务优先级 LOS_BitmapClr(&taskCB->priBitMap, resumedTask->priority);//把等待任务锁的任务的优先级记录在参数任务的bitmap中 } } else if (taskCB->priBitMap != 0) {//如果bitmap不等于0说明参数任务至少有任务调度的优先级 OsMuxPostOpSub(taskCB, mutex);// } } mutex->muxCount = 1;//互斥锁数量为1 mutex->owner = (VOID *)resumedTask;//互斥锁的持有人换了 resumedTask->taskMux = NULL;//resumedTask不再等锁了 LOS_ListDelete(&mutex->holdList);//自然要从等锁链表中把自己摘出去 LOS_ListTailInsert(&resumedTask->lockList, &mutex->holdList);//把锁挂到恢复任务的锁链表上,lockList是任务持有的所有锁记录 OsTaskWake(resumedTask);//resumedTask有了锁就唤醒它,因为当初在没有拿到锁时处于了pend状态 if (needSched != NULL) {//如果不为空 *needSched = TRUE;//就走起再次调度流程 } return LOS_OK; } 总结 1.互斥锁解决的是任务间竞争共享内存的问题. 2.申请锁失败的任务会进入睡眠OsTaskWait,内核会比较持有锁的任务和申请锁任务的优先级,把持有锁的任务优先级调到尽可能的高,以便更快的被调度执行,早日释放锁. 3.释放锁的任务会在等锁链表中找一个高优先级任务,通过OsTaskWake唤醒它,并向调度算法申请调度.但要注意,调度算法只是按优先级来调度,并不保证调度后的任务一定是要唤醒的任务. 4.互斥锁篇关键是看懂 OsMuxPendOp 和 OsMuxPostOp 两个函数. 喜欢就请收藏吧 各大站点搜 "鸿蒙内核源码分析" ,快速找到组织. 鸿蒙内核源码中文注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆, 四大源码仓每日同步更新< Gitee | Github | CSDN | Coding > 鸿蒙内核源码分析博客 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点每日同步更新< OSCHINA | CSDN | WeHarmony >

优秀的个人博客,低调大师

| 中文注解HarmonyOS源码 | v23.01

鸿蒙内核源码注释 >> 精读内核源码,中文注解分析,深挖地基工程,大脑永久记忆,四大码仓每日同步更新 鸿蒙内核源码分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点每日同步更新 汇编如何传复杂的参数? 鸿蒙内核源码分析(汇编基础篇) | CPU在哪里打卡上班 ? < CSDN | OSCHINA | WeHarmony | 源动力 >中很详细的介绍了一段具有代表性很经典的汇编代码,有循环,有判断,有运算,有多级函数调用。但有一个问题没有涉及,就是很复杂的参数如何处理? 实际在开发过程中函数参数往往是很复杂的结构体,那复杂参数(比如结构体)怎么传递呢? 先看一段C语言和编译的汇编代码 #include <stdio.h> #include <math.h> struct reg{ int Rn[100]; int pc; }; int fp(reg cpu) { return cpu.Rn[0] * cpu.pc; } int main() { reg cpu; cpu.Rn[0] = 1; cpu.pc = 2; return fp(cpu); } //编译器: armv7-a gcc (9.2.1) fp(reg): sub sp, sp, #16 @申请栈空间 str fp, [sp, #-4]! @保护fp帧指针,等同于push {fp} add fp, sp, #0 @fp新值,同时也指向了栈顶 add ip, fp, #4 @定位到入栈口,让剩余参数依次入栈 stm ip, {r0, r1, r2, r3}@r0-r3入栈保存 ldr r3, [fp, #4] @取值cpu.pc = 2 ldr r2, [fp, #404] @取值cpu.Rn[0] = 1 mul r3, r2, r3 @cpu.Rn[0] * cpu.pc mov r0, r3 @返回值r0带回 add sp, fp, #0 @重置sp ldr fp, [sp], #4 @重置fp add sp, sp, #16 @归还栈空间 bx lr @跳回main函数 main: push {fp, lr} @入栈保存调用函数现场 add fp, sp, #4 @fp更新 sub sp, sp, #800 @分配800个栈空间给main mov r3, #1 @r3 = 1 str r3, [fp, #-408] @将1放置 fp-408处 mov r3, #2 @r3 = 2 str r3, [fp, #-8] @将2放置 fp-8处 mov r0, sp @r0 = sp sub r3, fp, #392 @r3 = fp - 392 mov r2, #388 @只拷贝388,剩下4个由寄存器传参 mov r1, r3 @保存由r1保存r3,用于memcpy bl memcpy @拷贝结构体部分内容,将r1的内容拷贝r2的数量到r0 sub r3, fp, #408 @定位到结构体剩余未拷贝处 ldm r3, {r0, r1, r2, r3} @将剩余结构体内容通过寄存器传参 bl fp(reg) @执行fp mov r3, r0 @返回值给r3 nop @用于程序指令的对齐 mov r0, r3 @再将返回值给r0 sub sp, fp, #4 @恢复SP值 pop {fp, lr} @出栈恢复调用函数现场 bx lr @跳回调用函数 两个函数对应两段汇编,干净利落,去除中间各项干扰,只有一个结构体reg,具体来看看汇编如何传递它,它在栈中的数据变化是怎样的? 入参方式 结构体中共101个栈空间(一个栈空间单位四个字节),对应就是404个字节地址. main上来就申请了 sub sp, sp, #800 @分配800个栈空间给main,即 200个栈空间 int main() { reg cpu; cpu.Rn[0] = 1; cpu.pc = 2; return fp(cpu); } 但main函数只有一个变量,只要101个栈空间,其他都算上也用不了200个的.为什么要这么做呢? 而且注意下里面的数字 388, 408, 392 这些都是什么意思? 看完main汇编能得到一个结论是 200个栈空间中除了存放了main函数本身的变量reg cpu外 ,还存放了 fp函数的参数cpu的部分值,存放了多少个?答案是 97个. 注意变量CPU没有共用,而是拷贝了一部份.如何拷贝的?继续看 memcpy汇编调用 mov r0, sp @r0 = sp sub r3, fp, #392 @r3 = fp - 392 mov r2, #388 @只拷贝388,剩下4个由寄存器传参 mov r1, r3 @保存由r1保存r3,用于memcpy bl memcpy @拷贝结构体部分内容,将r1的内容拷贝r2的数量到r0 sub r3, fp, #408 @定位到结构体剩余未拷贝处 ldm r3, {r0, r1, r2, r3} @将剩余结构体内容通过寄存器传参 看这段汇编拷贝,意思是从r1开始位置拷贝r2的数量到r0的位置,而且只拷贝了 388个,也就是 388/4 = 97个栈空间.剩余的4个通过寄存器传的参数.ldm代表从fp-408的位置将内存地址的值连续的给 r0 - r3寄存器 fp参数取用 fp(reg): sub sp, sp, #16 @申请栈空间 str fp, [sp, #-4]!@保护fp帧指针,等同于push {fp} add fp, sp, #0 @fp新值,同时也指向了栈顶 add ip, fp, #4 @定位到入栈口,让剩余参数依次入栈 stm ip, {r0, r1, r2, r3}@r0-r3入栈保存 ldr r3, [fp, #4] @取值cpu.pc = 2 ldr r2, [fp, #404]@取值cpu.Rn[0] = 1 mul r3, r2, r3 @cpu.Rn[0] * cpu.pc mov r0, r3 @返回值r0带回 add sp, fp, #0 @重置sp ldr fp, [sp], #4 @重置fp add sp, sp, #16 @归还栈空间 bx lr @跳回main函数 fp申请了4个栈空间就是用来存放四个寄存器值的,注意它和另外的reg cpu 97个栈空间是连续的. 同时 add fp, sp, #0 表示fp指向了栈顶位置 fp+404 和 fp+4 刚好取到了 cpu.Rn[0] 和 cpu.pc 的值. 如此完成了乘法运算. 总结 因为寄存器数量有限,所以只能通过这种方式来传递大的参数,也只能在main函数栈中保存,也必须确保数据的连续性. 一部分通过寄存器传,一部分通过拷贝的方式倒是挺有意思的. 喜欢就请注入源动力吧 各大站点搜 "鸿蒙内核源码分析",快速找到组织.或者更简单的,如图: 鸿蒙内核源码注释 >> 精读内核源码,中文注解分析,深挖地基工程,大脑永久记忆,四大码仓每日同步更新 鸿蒙内核源码分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点每日同步更新

优秀的个人博客,低调大师

鸿蒙内核源码分析(调度故事篇) | 西门和金莲的那点破事 | 中文注解HarmonyOS源码 | v8.05

鸿蒙内核源码注释 >> 精读内核源码,中文注解分析,深挖地基工程,大脑永久记忆,四大码仓每日同步更新 鸿蒙内核源码分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点每日同步更新 前言 本篇只讲一个故事,听完希望大家能把故事的场景过程记在脑子里,鸿蒙内核源码分析系列文章会反复的提这个故事,在后续具体源码解读过程中都会去用故事里的细节导读源码。如果看了也喜欢也请分享给更多的人知道这个故事。故事就得有个名字,就叫张大爷的故事吧,故事开始。 场馆介绍 某地有一个运动场馆,分成室内馆(400平米)和室外馆(4万平米),管理处在室内馆,那是工作人员办公的地方,非工作人员不得进入。 场馆的定位是供本地公司/组织/团体安排举办各种活动使用的,在舞台上表演(统称舞台剧),同时表演的舞台剧只能一个,但因为生意太好,申请人太多了,所以用馆要先申请->排队->上演.场馆里面有一座永远很准时,不会停的大钟表,每十分钟就自动响一次,场馆里有很多的资源,有篮球,酒馆,小卖部,桌椅,还有演员(人也算资源),反正就是应有尽有,但是数量有限. 资源由管理处统一管理,这些资源也得先申请才能使用.场地外有个大屏幕,屏幕实时对外公布场馆舞台剧情况,屏幕内容如下: 舞台剧名 优先级 状态 进行中节目 就绪节目 管理处 0 正在工作 打扫场地卫生 无 三国演义 19 已就绪 无 骂死王朗 淘宝直播 20 已就绪 无 薇娅9点直播 没错,场馆的内部工作也是个剧,只不过它的优先级最高.而且注意这里只展示正在和就绪的剧情节目,就绪是万事俱备,只欠登台表演的意思.例如上表中有两个剧都准备好了,组成了一个就绪队列,都等着管理处打扫完卫生后表演,但同时只能演一个剧,而三国演义的优先级更高(场馆规定越小的优先级越高),所以不出意外,下一个表演的节目就是三国演义之骂死王朗,这里请记住就绪队列,后续会反复的提它,很重要! 表演走什么流程? 用馆者需提交你舞台剧的剧本,剧本可以是玩游戏,拍电视剧,直播电商等等,反正精彩的世界任你书写,场馆内有专人(统称导演)负责跟进你的剧本上演。剧本由各种各样的场景剧组成(统称节目),比如要拍个水浒传的剧本. 被分成武松打虎,西门和金莲那点破事等等节目剧.申请流程是去管理处先填一张电子节目表,节目表有固定的格式,填完点提交你的工作就完成了,接下来就是导演的事了.节目表单格式如下. 剧本名称 节目章回 内容 优先级 所需资源 状态 水浒传 第18回 武松打虎 12 武松,老虎一只,酒18碗 未开始 水浒传 第28回 西门和金莲那点破事 2 西门庆,金莲,王婆,一个炕 未开始 水浒传 第36回 武松拳打蒋门神 14 武松,蒋门神,猪肉 未开始 故事写到这里,大家脑子里有个画面了吧,记住这两张表,继续走起。 西门大官人什么时候表演? 场馆都会给每个用馆单位发个标号代表你使用场馆的优先级,剧本中每个场景节目也有优先级,都是0级最高,31级最低,这里比如水浒传优先级为8,西门庆和金莲那点破事节目为2,节目资源是需要两位主角(西门,金莲)和王婆,一个炕等资源,这些资源要向场馆负责人申请好,节目资源申请到位了就可以进入就绪队列,如果你的剧本里没有一个节目的资源申请到了那对不起您连排号的资格都没有。这里假如水浒传审核通过,并只有西门大官人节目资源申请成功,而管理处卫生打扫完了,以上两个表格的内容将做如下更新 舞台剧名 优先级 状态 进行中节目 就绪节目 水浒传 8 正在进行 西门和金莲那点破事 无 三国演义 19 已就绪 无 骂死王朗 淘宝直播 20 已就绪 无 薇娅9点直播 注意虽然三国演义先来,但此时水浒传排在三国的前面,是因为它的优先级高,优先级分32级,0最高,31最低. 剧本名称 节目章回 内容 优先级 所需资源 状态 表演位置 水浒传 第18回 武松打虎 12 武松,老虎一只,酒18碗 未开始 暂无 水浒传 第28回 西门和金莲那点破事 2 西门,金莲,王婆,炕一个 正在进行 西门火急火燎的跑进金莲屋内 水浒传 第36回 武松拳打蒋门神 14 武松,蒋门神,猪肉 未开始 暂无 注意看表中状态的变化和优先级,一个是剧本的优先级,一个是同一个剧本中节目的优先级.而之前优先级最高的管理处,因为没有其他节目要运行,所以移出了就绪队列. 西门好事被破坏了怎么办了? 场馆会根据节目上的内容把节目演完。每个节目十分钟,时间到了要回去重新排队,如果还是你就可以继续你的表演。但这里经常会有异常情况发生. 比如上级领导给场馆来个电话临时有个更高优先级节目要插进来,没办法西门你的好事要先停止,please stop! 场地要让给别人办事,西门灰溜溜得回就绪队列排队去,但请放心会在你西门退场前会记录下来表演到哪个位置了(比如:西门官人已脱完鞋),以便回来时继续接着表演。高优先级的事处理完后,如果西门的优先级还是最高的就可以继续用场地,会先还原现场演到哪了再继续办事就完了,绝不重复西门前面的准备工作,否则西门绝不答应! 节目表演完所有资源要回收,这个节目从此消亡,如果你剧本里所有节目都表演完了,那你的整个剧本也可以拜拜了,导演回到导演组,又可以去接下一部戏了. 这里还原下当西门和金莲那点破事被场馆紧急电话打断后表的变化是怎样的,如下: 剧本名称 优先级 状态 进行中节目 就绪节目 管理处 0 正在工作 接听上级电话 无 水浒传 8 已就绪 无 西门和金莲那点破事 三国演义 19 已就绪 无 骂死王朗 淘宝直播 20 已就绪 无 薇娅9点直播 剧本名称 节目章回 内容 优先级 所需资源 状态 表演位置 水浒传 第18回 武松打虎 12 武松,老虎一只,酒18碗 未开始 暂无 水浒传 第28回 西门和金莲那点破事 2 西门庆,金莲,王婆,一个炕 就绪 西门官人脱完鞋 水浒传 第36回 武松拳打蒋门神 14 武松,蒋门神,猪肉 未开始 暂无 表演给谁看呢? 外面那些吃瓜观众啊,群众你我他,游戏公司设计了游戏的剧本,电商公司设计了电商剧本,西门大官人被翻拍了这么多次不就是都爱看嘛,场馆会按你的剧本来表演,当然也可以互动,表演的场景需要观众操作时,观众在外面可以操作,发送指令。想想你玩游戏输入名字登录的场景。场馆里面有三个团队,张大爷团队负责导演组演剧本,王场馆负责场地的使用规划的,李后勤负责搞搞后勤. 张大爷团队做什么的? 上面这些工作都是张大爷团队的工作,接待剧本的导演组,管理剧本清单,指派导演跟进,申请节目资源,调整剧本优先级,控制时间,以使舞台能被公平公正的被调度使用等等 王场馆是做什么的? 看名字能知道负责场地用度的,你想想这么多节目,场地只有这么点,同时只能由一个节目上演,怎么合理的规划才能即公平又效率最大化呢,这就是王场馆的工作,但咱王总也有两把刷子,会给用馆公司感觉到整个场馆都是自己在用,具体不在这个故事里说明,后续有专门讲王场馆如何高效的管理内外场地的故事篇. 李后勤是做什么的? 场馆每天的开业,歇业,场地清理,管理处的对外业务,接听电话,有人闹事了怎么处理,收钱开发票 等等也有很多工作统称为后勤工作要有专门的团队来对接,具体不在这里说明,后续也有专门讲这块的故事. 故事想说什么呢? 故事到底想说什么呢?这就是操作系统的调度机制,熟悉了这个故事就熟悉了鸿蒙系统内核任务调度的工作原理!操作系统就是管理场馆和确保工作人员有序工作的系统解决方案商,外面公司只要提供个剧本,就能按剧本把这台戏演好给广大观众观看。有了这个故事垫底,鸿蒙内核源码分析系列就有了一个非常好的开始基础。 真的是这样的吗?必须的。 内核和故事的关系映射 故事概念 内核概念 备注 剧本 程序 一个剧本一个负责人跟进,跑起来的程序叫进程 导演 进程 进程负责剧本整个运行过程,是资源管理单元,任务也是一种资源 节目 线程/任务 任务记录节目的整个运行过程,任务是调度的单元 西门被打断 保存现场 本质是保存寄存器(PC,LR,FP,SP)的状态 西门继续来 恢复现场 本质是还原寄存器(PC,LR,FP,SP)的状态 表演场地 用户空间 所有节目都在同一块场地表演 管理处 内核空间 管理处非工作人员不得入内 外部场地 磁盘空间 故事暂未涉及,留在内存故事中讲解 节目内容 代码段 任务涉及的具体代码段 管理处的服务 系统调用 软中断实现,切换至内核栈 场馆大钟 系统时钟 十分钟响一次代表一个节拍(tick) 节目20分钟 时间片 鸿蒙时间片默认 2个tick,20ms 上级电话 中断 硬中断,直接跳到中断处理函数执行 表演顺序 优先级 进程和线程都是32个优先级,[0-31],从高到低 张大爷 进程/线程管理 抢占式调度,优先级高者运行 王场馆 内存管理 虚拟内存,内存分配,缺页置换 == 李后勤 异常接管 中断,跟踪,异常接管 == 请牢记这个故事 当然还有很多的细节在故事里没有讲到,比如王场馆和李后勤的工作细节,还有后续故事一一拆解.太细不可能真的在一个故事里全面讲完,笔者想说的是框架,架构思维,要先有整体框架再顺藤摸瓜寻细节,层层深入,否则很容易钻进死胡同里出不来。读着读着就放弃了,其实真没那么难。当你摸清了整个底层的运作机制再看上层的应用,就会有了拨开云雾见阳光,神清气爽的感觉。具体的我们在后续的章节里一一展开,用这个故事去理解鸿蒙系统内核调度过程,没毛病,请务必牢记这个故事。 喜欢就请注入源动力吧 各大站点搜 "鸿蒙内核源码分析",快速找到组织.或者更简单的,如图: 鸿蒙内核源码注释 >> 精读内核源码,中文注解分析,深挖地基工程,大脑永久记忆,四大码仓每日同步更新 鸿蒙内核源码分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点每日同步更新

优秀的个人博客,低调大师

鸿蒙内核源码分析(ninja应用篇) | 简单而快速的构建系统 | 百篇博客分析HarmonyOS源码 | v61.01

OpenHarmony | 鸿蒙研究站 | WeHarmony < 国内 | 国外 > 百篇博客系列篇.本篇为: v61.xx 鸿蒙内核源码分析(ninja应用篇) | 简单而快速的构建系统 | 51 .c .h .o 编译构建模块相关篇为: v60.xx 鸿蒙内核源码分析(gn应用篇) | gn语法及在鸿蒙的使用 | 51 .c .h .o v59.xx 鸿蒙内核源码分析(构建工具篇) | 顺瓜摸藤调试鸿蒙构建过程 | 51 .c .h .o v58.xx 鸿蒙内核源码分析(环境脚本篇) | 有了它编译鸿蒙好简单 | 51 .c .h .o v57.xx 鸿蒙内核源码分析(编译过程篇) | 简单案例窥视GCC编译全过程 | 51 .c .h .o v50.xx 鸿蒙内核源码分析(编译环境篇) | docker编译鸿蒙真的很香 | 51 .c .h .o ninja是什么? ninja是一个重视速度的构建系统,与其对标的是Make,它们都依赖于文件的时间戳进行检测重编. 它的设计目的是让更高级别的构建系统生成其输入端文件,其并不希望你手动去编.ninja文件,可以生成.ninja的工具有gn,cmake,premake,甚至你自己都可以写个 ninja 生成工具. ninja非常高效,可理解为构建系统中的汇编语言。 ninja文件没有分支、循环的流程控制,是被指定了一堆规则的文件,所以要比Makefile简单很多 目前已知的GoogleChrome,Android的一部分,LLVM, V8, 方舟编译器, 鸿蒙 等大型系统都使用到了ninja构建. 基本概念 概念 中译 解释 edge 边 即build语句,指定目标(输出)、规则与输入,是编译过程拓扑图中的一条边(edge)。 target 目标 编译过程需要产生的目标,由build语句指定。 output 输出 build语句的前半段,是target的另一种称呼。 input 输入 build语句的后半段,用来产生output的文件或目标,另一种称呼是依赖。 rule 规则 通过指定command与一些内置变量,决定如何从输入产生输出。 pool 池 一组rule或edge,通过指定其depth,可以控制并行上限。 scope 作用域 变量的作用范围,有rule与build语句的块级,也有文件级别。rule也有scope。 -------------------------------------------------------------------------------------------- 关键字 作用 build 定义一个edge。 rule 定义一个rule。 pool 定义一个pool。 default 指定默认的一个或多个target。 include 添加一个ninja文件到当前scope。 subninja 添加一个ninja文件,其scope与当前文件不同。 phony 一个内置的特殊规则,指定非文件的target。 简单的ninja 首先 ninja 一定是简单的,呆板的.凡是能被工具生成的东西,一定是在不断的重复某种简单,众多的简单按一定的规则有效叠加起来就能解决复杂的问题,请仔细想想是不是这个道理.ninja简单到没什么语法,只是几个概念和规则. 以至于 ninja参考手册 比 gn参考手册 简单的太多. 看个示例: cflags = -Wall -Werror #全局变量 rule cc command = gcc $cflags -c $in -o $out build foo.o: cc foo.c build special.o: cc special.c cflags = -Wall #局部变量,范围只在编译special.c上有效 解读 cflags:定义一个用户变量,用于给规则传参. rule:定义一个叫cc的规则. command:将生成bash命令,接收外部三个参数 第一个build,将foo.c用cc规则编译成foo.o 最终编译选项:gcc -Wall -Werror -c foo.c -o foo.o 第二个build,将special.c用cc规则编译成special.o 最终编译选项:gcc -Wall -c foo.c -o foo.o in,out是ninja的两个内置变量. phony规则 跟称呼弗拉基米尔·弗拉基米罗维奇·普京为普总一样, 有些文件路径会很长,ninja提供取别名的功能,这仅仅是为了方便. build ability: phony ./libability.so build ability_notes: phony obj/foundation/aafwk/aafwk_lite/frameworks/ability_lite/ability_notes.stamp build ability_test: phony obj/foundation/aafwk/aafwk_lite/services/abilitymgr_lite/unittest/ability_test.stamp build ability_test_pageAbilityTest_group_lv0: phony obj/foundation/aafwk/aafwk_lite/services/abilitymgr_lite/unittest/test_lv0/page_ability_test/ability_test_pageAbilityTest_group_lv0.stamp 有了上面的铺垫,读懂鸿蒙的ninja部分应该没多大障碍了. 鸿蒙 | ninja 在v60.xx 鸿蒙内核源码分析(gn应用篇) | gn语法及在鸿蒙的使用 | 51 .c .h .o 篇的末尾已说明通过 gn gen生成了以下文件和目录 turing@ubuntu:/home/openharmony/code-v1.1.1-LTS/out/hispark_aries/ipcamera_hispark_aries$ ls args.gn build.ninja build.ninja.d NOTICE_FILE obj test_info toolchain.ninja args.gn :一些参数 build.ninja : ninja的主文件 build.ninja.d :记录生成所有.ninja 所依赖的BUILD.gn文件路劲列表,一个BUILD.gn就生成一个.ninja文件 obj :各组件模块构建/编译文件输出地. toolchain :放置ninja规则,将被 subninja 进 build.ninja build.ninja build.ninja内容如下: ninja_required_version = 1.7.2 rule gn command = ../../../../tools/gn --root=../../.. -q --dotfile=../../../build/lite/.gn --script-executable=python3 gen . description = Regenerating ninja files build build.ninja: gn generator = 1 depfile = build.ninja.d subninja toolchain.ninja build ability: phony ./libability.so build ability_notes: phony obj/foundation/aafwk/aafwk_lite/frameworks/ability_lite/ability_notes.stamp build ability_test: phony obj/foundation/aafwk/aafwk_lite/services/abilitymgr_lite/unittest/ability_test.stamp build ability_test_pageAbilityTest_group_lv0: phony obj/foundation/aafwk/aafwk_lite/services/abilitymgr_lite/unittest/test_lv0/page_ability_test/ability_test_pageAbilityTest_group_lv0.stamp #此处省略诸多 phony .. build all: phony $ ./libcameraApp.so $ obj/applications/sample/camera/cameraApp/cameraApp_hap.stamp $ ./libgallery.so $ ... default all 解读 前面部分是定义一个 gn规则,用于干嘛呢? 重新生成一遍 *ninja文件 subninja相当于 #include文件 default all,指定默认的一个或多个target toolchain | 定义规则 toolchain.ninja 定义了编译c,c++,汇编器,链接,静态/动态链接库,时间戳,拷贝等规则. 内容如下: rule cxx command = /root/llvm/bin/clang++ ${defines} ${include_dirs} ${cflags_cc} -c ${in} -o ${out} description = clang++ ${out} depfile = ${out}.d deps = gcc rule alink command = /root/llvm/bin/llvm-ar -cr ${out} @"${out}.rsp" description = AR ${out} rspfile = ${out}.rsp rspfile_content = ${in} rule link command = /root/llvm/bin/clang ${ldflags} ${in} ${libs} -o ${output_dir}/bin/${target_output_name}${output_extension} description = LLVM LINK ${output_dir}/bin/${target_output_name}${output_extension} rspfile = ${output_dir}/bin/${target_output_name}${output_extension}.rsp rspfile_content = ${in} rule solink command = /root/llvm/bin/clang -shared ${ldflags} ${in} ${libs} -o ${output_dir}/${target_output_name}${output_extension} description = SOLINK ${output_dir}/${target_output_name}${output_extension} rspfile = ${output_dir}/${target_output_name}${output_extension}.rsp rspfile_content = ${in} rule stamp command = /usr/bin/touch ${out} description = STAMP ${out} rule asm command = /root/llvm/bin/clang ${include_dirs} ${asmflags} -c ${in} -o ${out} description = ASM ${out} depfile = ${out}.d deps = gcc rule cc command = /root/llvm/bin/clang ${defines} ${include_dirs} ${cflags} ${cflags_c} -c ${in} -o ${out} description = clang ${out} rule copy command = cp -afd ${in} ${out} description = COPY ${in} ${out} 注意这些规则中的描述description字段,其后面的内容会打到控制台上,每一条输出都是一次 build,如图所示,通过这些描述就知道使用了什么规则去构建. 组件编译 本篇以编译ability组件为例说明 ninja对组件的编译情况.每个组件都有自己的.ninja,描述组件的编译细节.而整个鸿蒙系统就是由众多的类似.ninja构建编译完成的. ├── foundation │ ├── aafwk │ │ └── aafwk_lite │ │ ├── frameworks │ │ │ ├── ability_lite │ │ │ │ └── ability.ninja ability.ninja内容如下: defines = -DOHOS_APPEXECFWK_BMS_BUNDLEMANAGER \ -D_XOPEN_SOURCE=700 -DOHOS_DEBUG \ -D_FORTIFY_SOURCE=2 \ -D__LITEOS__ -D__LITEOS_A__ include_dirs = -I../../../foundation/aafwk/aafwk_lite/frameworks/abilitymgr_lite/include \ -I../../../foundation/aafwk/aafwk_lite/frameworks/want_lite/include \ -I../../../foundation/aafwk/aafwk_lite/interfaces/innerkits/abilitymgr_lite \ -I../../../foundation/aafwk/aafwk_lite/interfaces/kits/want_lite \ -I../../../foundation/aafwk/aafwk_lite/interfaces/kits/ability_lite \ -I../../../foundation/appexecfwk/appexecfwk_lite/utils/bundle_lite \ -I../../../foundation/appexecfwk/appexecfwk_lite/interfaces/kits/bundle_lite \ -I../../../foundation/appexecfwk/appexecfwk_lite/frameworks/bundle_lite/include \ -I../../../foundation/graphic/ui/frameworks -I../../../foundation/graphic/surface/interfaces/kits \ -I../../../foundation/distributedschedule/samgr_lite/interfaces/kits/registry \ -I../../../foundation/distributedschedule/samgr_lite/interfaces/kits/samgr \ -I../../../foundation/communication/ipc_lite/frameworks/liteipc/include \ -I../../../kernel/liteos_a/kernel/include \ -I../../../kernel/liteos_a/kernel/common \ -I../../../third_party/bounds_checking_function/include \ -I../../../third_party/freetype/include \ -I../../../utils/native/lite/kv_store/innerkits \ -I../../../utils/native/lite/include \ -I../../../foundation/aafwk/aafwk_lite/frameworks/ability_lite/include \ -I../../../foundation/aafwk/aafwk_lite/frameworks/ability_lite \ -I/root/llvm/include/c++/v1 \ -I../../../prebuilts/lite/sysroot/usr/include/arm-liteos \ -I../../../base/hiviewdfx/hilog_lite/interfaces/native/innerkits/hilog \ -I../../../base/hiviewdfx/hilog_lite/interfaces/native/innerkits \ -I../../../third_party/bounds_checking_function/include \ -I../../../third_party/bounds_checking_function/include \ -I../../../foundation/communication/ipc_lite/interfaces/kits \ -I../../../utils/native/lite/include cflags = -Wall -Wno-format -Wno-format-extra-args -fPIC \ --target=arm-liteos \ --sysroot=/home/openharmony/prebuilts/lite/sysroot \ -Oz -flto -mfloat-abi=softfp -mcpu=cortex-a7 -nostdlib -fno-common -fno-builtin -fno-strict-aliasing -Wall -fsigned-char -mno-unaligned-access -fno-omit-frame-pointer -fstack-protector-all -fPIC cflags_cc = -Wall -Wno-format -Wno-format-extra-args -fPIC \ --target=arm-liteos \ --sysroot=/home/openharmony/prebuilts/lite/sysroot \ -Oz -flto -mfloat-abi=softfp -mcpu=cortex-a7 -nostdlib -fno-common -fno-builtin -fno-strict-aliasing -Wall -mno-unaligned-access -fno-omit-frame-pointer -fstack-protector-all -fexceptions -std=c++11 -fPIC target_output_name = libability build obj/foundation/aafwk/aafwk_lite/frameworks/ability_lite/src/libability.ability.o: cxx ../../../foundation/aafwk/aafwk_lite/frameworks/ability_lite/src/ability.cpp build obj/foundation/aafwk/aafwk_lite/frameworks/ability_lite/src/libability.ability_context.o: cxx ../../../foundation/aafwk/aafwk_lite/frameworks/ability_lite/src/ability_context.cpp build obj/foundation/aafwk/aafwk_lite/frameworks/ability_lite/src/libability.ability_env.o: cxx ../../../foundation/aafwk/aafwk_lite/frameworks/ability_lite/src/ability_env.cpp build obj/foundation/aafwk/aafwk_lite/frameworks/ability_lite/src/libability.ability_env_impl.o: cxx ../../../foundation/aafwk/aafwk_lite/frameworks/ability_lite/src/ability_env_impl.cpp build obj/foundation/aafwk/aafwk_lite/frameworks/ability_lite/src/libability.ability_event_handler.o: cxx ../../../foundation/aafwk/aafwk_lite/frameworks/ability_lite/src/ability_event_handler.cpp build obj/foundation/aafwk/aafwk_lite/frameworks/ability_lite/src/libability.ability_loader.o: cxx ../../../foundation/aafwk/aafwk_lite/frameworks/ability_lite/src/ability_loader.cpp build obj/foundation/aafwk/aafwk_lite/frameworks/ability_lite/src/libability.ability_main.o: cxx ../../../foundation/aafwk/aafwk_lite/frameworks/ability_lite/src/ability_main.cpp build obj/foundation/aafwk/aafwk_lite/frameworks/ability_lite/src/libability.ability_scheduler.o: cxx ../../../foundation/aafwk/aafwk_lite/frameworks/ability_lite/src/ability_scheduler.cpp build obj/foundation/aafwk/aafwk_lite/frameworks/ability_lite/src/libability.ability_thread.o: cxx ../../../foundation/aafwk/aafwk_lite/frameworks/ability_lite/src/ability_thread.cpp build ./libability.so: solink \ obj/foundation/aafwk/aafwk_lite/frameworks/ability_lite/src/libability.ability.o \ obj/foundation/aafwk/aafwk_lite/frameworks/ability_lite/src/libability.ability_context.o \ obj/foundation/aafwk/aafwk_lite/frameworks/ability_lite/src/libability.ability_env.o \ obj/foundation/aafwk/aafwk_lite/frameworks/ability_lite/src/libability.ability_env_impl.o \ obj/foundation/aafwk/aafwk_lite/frameworks/ability_lite/src/libability.ability_event_handler.o \ obj/foundation/aafwk/aafwk_lite/frameworks/ability_lite/src/libability.ability_loader.o \ obj/foundation/aafwk/aafwk_lite/frameworks/ability_lite/src/libability.ability_main.o \ obj/foundation/aafwk/aafwk_lite/frameworks/ability_lite/src/libability.ability_scheduler.o \ obj/foundation/aafwk/aafwk_lite/frameworks/ability_lite/src/libability.ability_thread.o \ ./libabilitymanager.so ./libbundle.so ./libhilog_shared.so ./libliteipc_adapter.so \ ./libsec_shared.so ./libutils_kv_store.so || obj/utils/native/lite/kv_store/kv_store.stamp ldflags = -lstdc++ \ --target=arm-liteos \ --sysroot=/home/openharmony/prebuilts/lite/sysroot \ -L/root/llvm/lib/arm-liteos/c++ \ -L/home/openharmony/prebuilts/lite/sysroot/usr/lib/arm-liteos \ -L/root/llvm/lib/clang/9.0.0/lib/arm-liteos \ -lclang_rt.builtins -lc -lc++ -lc++abi \ --sysroot=/home/openharmony/prebuilts/lite/sysroot \ -mcpu=cortex-a7 -lc \ -L/home/openharmony/out/hispark_aries/ipcamera_hispark_aries \ -Wl,-rpath-link=/home/openharmony/out/hispark_aries/ipcamera_hispark_aries -Wl,-z,now -Wl,-z,relro -Wl,-z,noexecstack libs = frameworks = output_extension = .so output_dir = . 解读 defines,include_dirs,cflags_cc都是用户自定义变量,为了给 rule cxx准备参数,对.cpp的编译使用了这个规则 rule cxx command = /root/llvm/bin/clang++ ${defines} ${include_dirs} ${cflags_cc} -c ${in} -o ${out} description = clang++ ${out} depfile = ${out}.d deps = gcc in,out是两个内置变量,无须定义,值由build提供,如此就编译成了一个个的 .o文件. 在最后在当前目录下使用了solink规则,生成一个动态链接库libability.so. rule solink command = /root/llvm/bin/clang -shared ${ldflags} ${in} ${libs} -o ${output_dir}/${target_output_name}${output_extension} description = SOLINK ${output_dir}/${target_output_name}${output_extension} rspfile = ${output_dir}/${target_output_name}${output_extension}.rsp rspfile_content = ${in} ability | 最终生成文件 turing@ubuntu:/home/openharmony/code-v1.1.1-LTS/out/hispark_aries/ipcamera_hispark_aries/obj/foundation/aafwk/aafwk_lite/frameworks/ability_lite$ tree . ├── aafwk_abilitykit_lite.stamp ├── ability.ninja ├── ability_notes.stamp └── src ├── libability.ability_context.o ├── libability.ability_env_impl.o ├── libability.ability_env.o ├── libability.ability_event_handler.o ├── libability.ability_loader.o ├── libability.ability_main.o ├── libability.ability.o ├── libability.ability_scheduler.o └── libability.ability_thread.o 1 directory, 12 files turing@ubuntu:/home/openharmony/code-v1.1.1-LTS/out/hispark_aries/ipcamera_hispark_aries/obj/foundation/aafwk/aafwk_lite/frameworks/ability_lite$ stat ability_notes.stamp File: ability_notes.stamp Size: 0 Blocks: 0 IO Block: 4096 regular empty file Device: 805h/2053d Inode: 1217028 Links: 1 Access: (0644/-rw-r--r--) Uid: ( 1000/ turing) Gid: ( 0/ root) Access: 2021-07-21 00:38:52.237373740 -0700 Modify: 2021-07-21 00:34:30.207312566 -0700 Change: 2021-07-21 00:34:30.207312566 -0700 百篇博客.往期回顾 在加注过程中,整理出以下文章。内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感,容易理解记忆。说别人能听得懂的话很重要! 百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思。更希望让内核变得栩栩如生,倍感亲切.确实有难度,自不量力,但已经出发,回头已是不可能的了。 :P 与代码有bug需不断debug一样,文章和注解内容会存在不少错漏之处,请多包涵,但会反复修正,持续更新,.xx代表修改的次数,精雕细琢,言简意赅,力求打造精品内容。 v61.xx 鸿蒙内核源码分析(ninja应用篇) | 简单而快速的构建系统 | 51 .c .h .o v60.xx 鸿蒙内核源码分析(gn应用篇) | gn语法及在鸿蒙的使用 | 51 .c .h .o v59.xx 鸿蒙内核源码分析(构建工具篇) | 顺瓜摸藤调试鸿蒙构建过程 | 51 .c .h .o v58.xx 鸿蒙内核源码分析(环境脚本篇) | 有了它编译鸿蒙好简单 | 51 .c .h .o v57.xx 鸿蒙内核源码分析(编译过程篇) | 简单案例窥视GCC编译全过程 | 51 .c .h .o v56.xx 鸿蒙内核源码分析(进程映像篇) | ELF是如何被加载运行的? | 51 .c .h .o v55.xx 鸿蒙内核源码分析(重定位篇) | 与国际接轨的对外部发言人 | 51 .c .h .o v54.xx 鸿蒙内核源码分析(静态链接篇) | 完整小项目看透静态链接过程 | 51 .c .h .o v53.xx 鸿蒙内核源码分析(ELF解析篇) | 你要忘了她姐俩你就不是银 | 51 .c .h .o v52.xx 鸿蒙内核源码分析(静态站点篇) | 五一哪也没去就干了这事 | 51 .c .h .o v51.xx 鸿蒙内核源码分析(ELF格式篇) | 应用程序入口并不是main | 51 .c .h .o v50.xx 鸿蒙内核源码分析(编译环境篇) | docker编译鸿蒙真的很香 | 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 原创不易,欢迎转载,但麻烦请注明出处.

优秀的个人博客,低调大师

鸿蒙内核源码分析(信号生产篇) | 生产异步通讯信号的过程 | 百篇博客分析HarmonyOS源码 | v48.02

百万汉字注解 >> 精读鸿蒙源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< gitee | github | csdn | coding > 百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点定期更新中< oschina | 51cto | csdn | harmony > 信号 关于信号篇,本只想写一篇,但发现把它想简单了,内容不多,难度极大.整理了好长时间,理解了为何<<深入理解linux内核>>要单独为它开一章. 1.是信号相关的结构体多,而且还容易搞混.所以看本篇要注意结构体的名字和作用. 2.是系统调用太多了,涉及面广,信号的来源分硬件和软件.相当于软中断和硬中断,这就会涉及到汇编代码,但信号的处理函数又在用户空间,CPU是禁止内核态执行用户态代码的,所以运行过程需在用户空间和内核空间来回的折腾,频繁的切换上下文. 信号为系统提供了一种进程间异步通讯的方式,一个进程不必通过任何操作来等待信号的到达。事实上,进程也不可能知道信号到底什么时候到达。一般来说,只需用户进程提供信号处理函数,当有信号到达的时候,由内核空间异步回调用户空间的信号处理函数,其异步过程可理解为生成者和消费者.鉴于此,系列篇将分成 信号生产 和 信号消费 两篇说明: 本篇是信号生产,说概念,说结构体,理清信号和进程,任务的关系.最后说发送过程. 另外一篇专门说信号处理过程,围绕信号切换上下文sig_switch_context来说. 信号分类 每个信号都有一个名字和编号,这些名字都以SIG开头,例如SIGQUIT、SIGCHLD等等。 信号定义在signal.h头文件中,信号名都定义为正整数。 具体的信号名称可以使用kill -l来查看信号的名字以及序号,信号是从1开始编号的,不存在0号信号。不过kill对于信号0有特殊的应用。啥用呢? 可用来查询进程是否还在. 敲下 kill 0 pid 就知道了. 信号分为两大类:可靠信号与不可靠信号,前32种信号为不可靠信号,后32种为可靠信号。 不可靠信号: 也称为非实时信号,不支持排队,信号可能会丢失, 比如发送多次相同的信号, 进程只能收到一次. 信号值取值区间为1~31; 可靠信号: 也称为实时信号,支持排队, 信号不会丢失, 发多少次, 就可以收到多少次. 信号值取值区间为32~64 #define SIGHUP 1 //终端挂起或者控制进程终止 #define SIGINT 2 //键盘中断(ctrl + c) #define SIGQUIT 3 //键盘的退出键被按下 #define SIGILL 4 //非法指令 #define SIGTRAP 5 //跟踪陷阱(trace trap),启动进程,跟踪代码的执行 #define SIGABRT 6 //由abort(3)发出的退出指令 #define SIGIOT SIGABRT //abort发出的信号 #define SIGBUS 7 //总线错误 #define SIGFPE 8 //浮点异常 #define SIGKILL 9 //常用的命令 kill 9 123 | 不能被忽略、处理和阻塞 #define SIGUSR1 10 //用户自定义信号1 #define SIGSEGV 11 //无效的内存引用, 段违例(segmentation violation),进程试图去访问其虚地址空间以外的位置 #define SIGUSR2 12 //用户自定义信号2 #define SIGPIPE 13 //向某个非读管道中写入数据 #define SIGALRM 14 //由alarm(2)发出的信号,默认行为为进程终止 #define SIGTERM 15 //终止信号 #define SIGSTKFLT 16 //栈溢出 #define SIGCHLD 17 //子进程结束信号 #define SIGCONT 18 //进程继续(曾被停止的进程) #define SIGSTOP 19 //终止进程 | 不能被忽略、处理和阻塞 #define SIGTSTP 20 //控制终端(tty)上 按下停止键 #define SIGTTIN 21 //进程停止,后台进程企图从控制终端读 #define SIGTTOU 22 //进程停止,后台进程企图从控制终端写 #define SIGURG 23 //I/O有紧急数据到达当前进程 #define SIGXCPU 24 //进程的CPU时间片到期 #define SIGXFSZ 25 //文件大小的超出上限 #define SIGVTALRM 26 //虚拟时钟超时 #define SIGPROF 27 //profile时钟超时 #define SIGWINCH 28 //窗口大小改变 #define SIGIO 29 //I/O相关 #define SIGPOLL 29 // #define SIGPWR 30 //电源故障,关机 #define SIGSYS 31 //系统调用中参数错,如系统调用号非法 #define SIGUNUSED SIGSYS//不使用 #define _NSIG 65 信号产生 信号来源分为硬件类和软件类: 硬件类 用户输入:比如在终端上按下组合键ctrl+C,产生SIGINT信号; 硬件异常:CPU检测到内存非法访问等异常,通知内核生成相应信号,并发送给发生事件的进程; 软件类 通过系统调用,发送signal信号:kill(),raise(),sigqueue(),alarm(),setitimer(),abort() kill 命令就是一个发送信号的工具,用于向进程或进程组发送信号.例如: kill 9 PID (SIGKILL)来杀死PID 进程. sigqueue():只能向一个进程发送信号,不能像进程组发送信号;主要针对实时信号提出,与sigaction()组合使用,当然也支持非实时信号的发送; alarm():用于调用进程指定时间后发出SIGALARM信号; setitimer():设置定时器,计时达到后给进程发送SIGALRM信号,功能比alarm更强大; abort():向进程发送SIGABORT信号,默认进程会异常退出。 raise():用于向进程自身发送信号; 信号与进程相关结构体 typedef struct ProcessCB {//PCB中关于信号的信息 UINTPTR sigHandler; /**< signal handler */ //捕捉信号后的处理函数 sigset_t sigShare; /**< signal share bit */ //信号共享位,64个信号各站一位 }LosProcessCB; typedef unsigned _Int64 sigset_t; //一个64位的变量,每个信号代表一位. struct sigaction {//信号处理机制结构体 union { void (*sa_handler)(int); //信号处理函数——普通版 void (*sa_sigaction)(int, siginfo_t *, void *);//信号处理函数——高级版 } __sa_handler; sigset_t sa_mask;//指定信号处理程序执行过程中需要阻塞的信号; int sa_flags; //标示位 // SA_RESTART:使被信号打断的syscall重新发起。 // SA_NOCLDSTOP:使父进程在它的子进程暂停或继续运行时不会收到 SIGCHLD 信号。 // SA_NOCLDWAIT:使父进程在它的子进程退出时不会收到SIGCHLD信号,这时子进程如果退出也不会成为僵 尸进程。 // SA_NODEFER:使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号。 // SA_RESETHAND:信号处理之后重新设置为默认的处理方式。 // SA_SIGINFO:使用sa_sigaction成员而不是sa_handler作为信号处理函数。 void (*sa_restorer)(void); }; typedef struct sigaction sigaction_t; 解读 每个信号都对应一个位. 信号从1开始编号 [1 ~ 64] 对应 sigShare的[0 ~ 63]位,所以中间会差一个.记住这点,后续代码会提到. sigHandler信号处理函数的设置过程,由系统调用sigaction(用户空间) -> OsSigAction(内核空间)得到 #include <signal.h> int sigaction(int signo, const struct sigaction *act, struct sigaction *oact); int OsSigAction(int sig, const sigaction_t *act, sigaction_t *oact) { UINTPTR addr; sigaction_t action; if (!GOOD_SIGNO(sig) || sig < 1 || act == NULL) { return -EINVAL; } //将数据从用户空间拷贝到内核空间 if (LOS_ArchCopyFromUser(&action, act, sizeof(sigaction_t)) != LOS_OK) { return -EFAULT; } if (sig == SIGSYS) {//系统调用中参数错,如系统调用号非法 addr = OsGetSigHandler();//获取进程信号处理函数 if (addr == 0) {//进程没有设置信号处理函数时 OsSetSigHandler((unsigned long)(UINTPTR)action.sa_handler);//设置进程信号处理函数——普通版 return LOS_OK; } return -EINVAL; } return LOS_OK; } sigaction()第一个参数是要捕捉的信号; 第二个参数与sigaction函数同名的结构体, 结构体内定义了信号处理方法;第三个为输出参数,将信号的当前的sigaction结构地址输出.但鸿蒙显然没有认真对待第三个参数. sigaction()可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回-1。signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体. 鸿蒙目前支持信号处理函数——普通版,sa_handler表示自定义函数捕捉信号,或者说用户进程向内核注册了一个信号处理函数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。 当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。 sa_flags字段包含一些选项,具体看注释 sa_sigaction是实时信号的处理函数,union二选一.鸿蒙暂时不支持这种方式. 信号与任务相关结构体 typedef struct {//TCB中关于信号的信息 sig_cb sig; //信号控制块,用于异步通信,类似于 linux singal模块 } LosTaskCB; typedef struct {//信号控制块(描述符) sigset_t sigFlag; //不屏蔽的信号标签集 sigset_t sigPendFlag; //信号阻塞标签集,记录因哪些信号被阻塞 sigset_t sigprocmask; /* Signals that are blocked */ //进程屏蔽了哪些信号 sq_queue_t sigactionq; //信号捕捉队列 LOS_DL_LIST waitList; //等待链表,上面挂的是等待信号到来的任务, 可查找 OsTaskWait(&sigcb->waitList, timeout, TRUE) 理解 sigset_t sigwaitmask; /* Waiting for pending signals */ //任务在等待阻塞信号 siginfo_t sigunbinfo; /* Signal info when task unblocked */ //任务解锁时的信号信息 sig_switch_context context; //信号切换上下文, 用于保存切换现场, 比如发生系统调用时的返回,涉及同一个任务的两个栈进行切换 } sig_cb; 解读 系列篇已多次说过,进程只是管理资源的容器,真正让cpu干活的是任务task,所以发给进程的信号最终还是需要分发给具体任务来处理.所以能想到的是关于任务部分会更复杂. context信号处理很复杂的原因在于信号的发起在用户空间,发送需要系统调用,而处理信号的函数又是用户空间提供的, 所以需要反复的切换任务上下文.而且还有硬中断的问题,比如 ctrl + c ,需要从硬中断中回调用户空间的信号处理函数,处理完了再回到内核空间,最后回到用户空间.没听懂吧,我自己都说晕了,所以需要专门的一篇来说清楚信号的处理问题.本篇不展开说. sig_cb结构体是任务处理信号的结构体,要响应,屏蔽哪些信号等等都由它完成,这个结构体虽不复杂,但是很绕,很难搞清楚它们之间的区别.笔者是经过一番痛苦的阅读理解后才明白各自的含义.并想通过用打比方的例子试图让大家明白. 以下用追女孩打比方理解.任务相当于某个男,没错说的就是屏幕前的你,除了苦逼的码农谁会有耐心能坚持看到这里.64个信号对应64个女孩.允许一男同时追多个女孩,女孩也可同时被多个男追.女孩也可以主动追男的.理解如下: waitList等待信号的任务链表,上面挂的是因等待信号而被阻塞的任务.众男在排队追各自心爱的女孩们,处于无所事事的挂起的状态,等待女孩们的出现. sigwaitmask任务在等待的信号集合,只有这些信号能唤醒任务.相当于列出喜欢的各位女孩,只要出现一位就能让你满血复活. sigprocmask指任务对哪些信号不感冒.来了也不处理.相当于列出不喜欢的各位女孩,请她们别来骚扰你,嘚瑟. sigPendFlag信号到大但并未唤醒任务.相当于喜欢你的女孩来追你,但她不在你喜欢的列表内,结果是不搭理人家继续等喜欢的出现. sigFlag记录不屏蔽的信号集合,相当于你并不反感的女孩们.记录来过的那些女孩(除掉你不喜欢的). 信号发送过程 用户进程调用kill()的过程如下: kill(pid_t pid, int sig) - 系统调用 | 用户空间 --------------------------------------------------------------------------------------- | 内核空间 SysKill(...) |---> OsKillLock(...) |---> OsKill(.., OS_USER_KILL_PERMISSION) |---> OsDispatch() //鉴权,向进程发送信号 |---> OsSigProcessSend() //选择任务发送信号 |---> OsSigProcessForeachChild(..,ForEachTaskCB handler,..) |---> SigProcessKillSigHandler() //处理 SIGKILL |---> OsTaskWake() //唤醒所有等待任务 |---> OsSigEmptySet() //清空信号等待集 |---> SigProcessSignalHandler() |---> OsTcbDispatch() //向目标任务发送信号 |---> OsTaskWake() //唤醒任务 |---> OsSigEmptySet() //清空信号等待集 流程 通过 系统调用 kill 陷入内核空间 因为是用户态进程,使用OS_USER_KILL_PERMISSION权限发送信号 #define OS_KERNEL_KILL_PERMISSION 0U //内核态 kill 权限 #define OS_USER_KILL_PERMISSION 3U //用户态 kill 权限 鉴权之后进程轮询任务组,向目标任务发送信号.这里分三种情况: SIGKILL信号,将所有等待任务唤醒,拉入就绪队列等待被调度执行,并情况信号等待集 非SIGKILL信号时,将通过sigwaitmask和sigprocmask过滤,找到一个任务向它发送信号OsTcbDispatch. 代码细节 int OsKill(pid_t pid, int sig, int permission) { siginfo_t info; int ret; /* Make sure that the para is valid */ if (!GOOD_SIGNO(sig) || pid < 0) {//有效信号 [0,64] return -EINVAL; } if (OsProcessIDUserCheckInvalid(pid)) {//检查参数进程 return -ESRCH; } /* Create the siginfo structure */ //创建信号结构体 info.si_signo = sig; //信号编号 info.si_code = SI_USER; //来自用户进程信号 info.si_value.sival_ptr = NULL; /* Send the signal */ ret = OsDispatch(pid, &info, permission);//发送信号 return ret; } OsDispatch() 函数的代码如下: //信号分发 int OsDispatch(pid_t pid, siginfo_t *info, int permission) { LosProcessCB *spcb = OS_PCB_FROM_PID(pid);//找到这个进程 if (OsProcessIsUnused(spcb)) {//进程是否还在使用,不一定是当前进程但必须是个有效进程 return -ESRCH; } #ifdef LOSCFG_SECURITY_CAPABILITY //启用能力安全模式 LosProcessCB *current = OsCurrProcessGet();//获取当前进程 /* If the process you want to kill had been inactive, but still exist. should return LOS_OK */ if (OsProcessIsInactive(spcb)) {//如果要终止的进程处于非活动状态,但仍然存在,应该返回OK return LOS_OK; } /* Kernel process always has kill permission and user process should check permission *///内核进程总是有kill权限,用户进程需要检查权限 if (OsProcessIsUserMode(current) && !(current->processStatus & OS_PROCESS_FLAG_EXIT)) {//用户进程检查能力范围 if ((current != spcb) && (!IsCapPermit(CAP_KILL)) && (current->user->userID != spcb->user->userID)) { return -EPERM; } } #endif if ((permission == OS_USER_KILL_PERMISSION) && (OsSignalPermissionToCheck(spcb) < 0)) { return -EPERM; } return OsSigProcessSend(spcb, info);//给参数进程发送信号 } //给参数进程发送参数信号 int OsSigProcessSend(LosProcessCB *spcb, siginfo_t *sigInfo) { int ret; struct ProcessSignalInfo info = { .sigInfo = sigInfo, //信号内容 .defaultTcb = NULL, //以下四个值将在OsSigProcessForeachChild中根据条件完善 .unblockedTcb = NULL, .awakenedTcb = NULL, .receivedTcb = NULL }; //总之是要从进程中找个至少一个任务来接受这个信号,优先级 //awakenedTcb > receivedTcb > unblockedTcb > defaultTcb /* visit all taskcb and dispatch signal */ //访问所有任务和分发信号 if ((info.sigInfo != NULL) && (info.sigInfo->si_signo == SIGKILL)) {//需要干掉进程时 SIGKILL = 9, #linux kill 9 14 (void)OsSigProcessForeachChild(spcb, SigProcessKillSigHandler, &info);//进程要被干掉了,通知所有task做善后处理 OsSigAddSet(&spcb->sigShare, info.sigInfo->si_signo); OsWaitSignalToWakeProcess(spcb);//等待信号唤醒进程 return 0; } else { ret = OsSigProcessForeachChild(spcb, SigProcessSignalHandler, &info);//进程通知所有task处理信号 } if (ret < 0) { return ret; } SigProcessLoadTcb(&info, sigInfo); return 0; } //让进程的每一个task执行参数函数 int OsSigProcessForeachChild(LosProcessCB *spcb, ForEachTaskCB handler, void *arg) { int ret; /* Visit the main thread last (if present) */ LosTaskCB *taskCB = NULL;//遍历进程的 threadList 链表,里面存放的都是task节点 LOS_DL_LIST_FOR_EACH_ENTRY(taskCB, &(spcb->threadSiblingList), LosTaskCB, threadList) {//遍历进程的任务列表 ret = handler(taskCB, arg);//回调参数函数 OS_RETURN_IF(ret != 0, ret);//这个宏的意思就是只有ret = 0时,啥也不处理.其余就返回 ret } return LOS_OK; } 解读 OsSigProcessSend 如果是 SIGKILL信号,让spcb的所有任务执行SigProcessKillSigHandler函数,查看旗下的所有任务是否又在等待这个信号的,如果有就将任务唤醒,放在就绪队列等待被调度执行. //进程收到 SIGKILL 信号后,通知任务tcb处理. static int SigProcessKillSigHandler(LosTaskCB *tcb, void *arg) { struct ProcessSignalInfo *info = (struct ProcessSignalInfo *)arg;//转参 if ((tcb != NULL) && (info != NULL) && (info->sigInfo != NULL)) {//进程有信号 sig_cb *sigcb = &tcb->sig; if (!LOS_ListEmpty(&sigcb->waitList) && OsSigIsMember(&sigcb->sigwaitmask, info->sigInfo->si_signo)) {//如果任务在等待这个信号 OsTaskWake(tcb);//唤醒这个任务,加入进程的就绪队列,并不申请调度 OsSigEmptySet(&sigcb->sigwaitmask);//清空信号等待位,不等任何信号了.因为这是SIGKILL信号 } } return 0; } 非SIGKILL信号,让spcb的所有任务执行SigProcessSignalHandler函数 static int SigProcessSignalHandler(LosTaskCB *tcb, void *arg) { struct ProcessSignalInfo *info = (struct ProcessSignalInfo *)arg;//先把参数解出来 int ret; int isMember; if (tcb == NULL) { return 0; } /* If the default tcb is not setted, then set this one as default. */ if (!info->defaultTcb) {//如果没有默认发送方的任务,即默认参数任务. info->defaultTcb = tcb; } isMember = OsSigIsMember(&tcb->sig.sigwaitmask, info->sigInfo->si_signo);//任务是否在等待这个信号 if (isMember && (!info->awakenedTcb)) {//是在等待,并尚未向该任务时发送信号时 /* This means the task is waiting for this signal. Stop looking for it and use this tcb. * The requirement is: if more than one task in this task group is waiting for the signal, * then only one indeterminate task in the group will receive the signal. */ ret = OsTcbDispatch(tcb, info->sigInfo);//发送信号,注意这是给其他任务发送信号,tcb不是当前任务 OS_RETURN_IF(ret < 0, ret);//这种写法很有意思 /* set this tcb as awakenedTcb */ info->awakenedTcb = tcb; OS_RETURN_IF(info->receivedTcb != NULL, SIG_STOP_VISIT); /* Stop search */ } /* Is this signal unblocked on this thread? */ isMember = OsSigIsMember(&tcb->sig.sigprocmask, info->sigInfo->si_signo);//任务是否屏蔽了这个信号 if ((!isMember) && (!info->receivedTcb) && (tcb != info->awakenedTcb)) {//没有屏蔽,有唤醒任务没有接收任务. /* if unblockedTcb of this signal is not setted, then set it. */ if (!info->unblockedTcb) { info->unblockedTcb = tcb; } ret = OsTcbDispatch(tcb, info->sigInfo);//向任务发送信号 OS_RETURN_IF(ret < 0, ret); /* set this tcb as receivedTcb */ info->receivedTcb = tcb;//设置这个任务为接收任务 OS_RETURN_IF(info->awakenedTcb != NULL, SIG_STOP_VISIT); /* Stop search */ } return 0; /* Keep searching */ } 函数的意思是,当进程中有多个任务在等待这个信号时,发送信号给第一个等待的任务awakenedTcb. 如果没有任务在等待信号,那就从不屏蔽这个信号的任务集中随机找一个receivedTcb接受信号. 只要不屏蔽 unblockedTcb就有值,随机的. 如果上面的都不满足,信号发送给defaultTcb. 寻找发送任务的优先级是 awakenedTcb > receivedTcb > unblockedTcb > defaultTcb 信号相关函数 信号集操作函数 sigemptyset(sigset_t *set):信号集全部清0; sigfillset(sigset_t *set): 信号集全部置1,则信号集包含linux支持的64种信号; sigaddset(sigset_t *set, int signum):向信号集中加入signum信号; sigdelset(sigset_t *set, int signum):向信号集中删除signum信号; sigismember(const sigset_t *set, int signum):判定信号signum是否存在信号集中。 信号阻塞函数 sigprocmask(int how, const sigset_t *set, sigset_t *oldset)); 不同how参数,实现不同功能 SIG_BLOCK:将set指向信号集中的信号,添加到进程阻塞信号集; SIG_UNBLOCK:将set指向信号集中的信号,从进程阻塞信号集删除; SIG_SETMASK:将set指向信号集中的信号,设置成进程阻塞信号集; sigpending(sigset_t *set)):获取已发送到进程,却被阻塞的所有信号; sigsuspend(const sigset_t *mask)):用mask代替进程的原有掩码,并暂停进程执行,直到收到信号再恢复原有掩码并继续执行进程。 鸿蒙源码百篇博客 往期回顾 v48.xx (信号生产篇) | 生产异步通讯信号的过程 < csdn | 51cto | harmony > v47.xx (进程回收篇) | 进程在临终前如何向老祖宗托孤 < csdn | 51cto | harmony > v46.xx (特殊进程篇) | 龙生龙,凤生凤,老鼠生儿会打洞 < csdn | 51cto | harmony > v45.xx (fork篇) | fork是如何做到调用一次,返回两次的 ? < csdn | 51cto | harmony > v44.xx (中断管理篇) | 硬中断的实现<>观察者模式 < csdn | 51cto | harmony > v43.xx (中断概念篇) | 外人眼中权势滔天的当红海公公 < csdn | 51cto | harmony > v42.xx (中断切换篇) | 中断切换到底在切换什么? < csdn | 51cto | harmony > v41.xx (任务切换篇) | 汇编告诉任务到底在切换什么 < csdn | 51cto | harmony > v40.xx (汇编汇总篇) | 所有的汇编代码都在这里 < csdn | 51cto | harmony > v39.xx (异常接管篇) | 社会很单纯,复杂的是人 < csdn | 51cto | harmony > v38.xx (寄存器篇) | arm所有寄存器一网打尽,不再神秘 < csdn | 51cto | harmony > v37.xx (系统调用篇) | 系统调用到底经历了什么 < csdn | 51cto | harmony > v36.xx (工作模式篇) | cpu是韦小宝,有哪七个老婆? < csdn | 51cto | harmony > v35.xx (时间管理篇) | tick是操作系统的基本时间单位 < csdn | 51cto | harmony > v34.xx (原子操作篇) | 是谁在为原子操作保驾护航? < csdn | 51cto | harmony > v33.xx (消息队列篇) | 进程间如何异步解耦传递大数据 ? < csdn | 51cto | harmony > v32.xx (cpu篇) | 内核是如何描述cpu的? < csdn | 51cto | harmony > v31.xx (定时器篇) | 内核最高优先级任务是谁? < csdn | 51cto | harmony > v30.xx (事件控制篇) | 任务间多对多的同步方案 < csdn | 51cto | harmony > v29.xx (信号量篇) | 信号量解决任务同步问题 < csdn | 51cto | harmony > v28.xx (进程通讯篇) | 进程间通讯有哪九大方式? < csdn | 51cto | harmony > v27.xx (互斥锁篇) | 互斥锁比自旋锁可丰满许多 < csdn | 51cto | harmony > v26.xx (自旋锁篇) | 真的好想为自旋锁立贞节牌坊! < csdn | 51cto | harmony > v25.xx (并发并行篇) | 怎么记住并发并行的区别? < csdn | 51cto | harmony > v24.xx (进程概念篇) | 进程在管理哪些资源? < csdn | 51cto | harmony > v23.xx (汇编传参篇) | 汇编如何传递复杂的参数? < csdn | 51cto | harmony > v22.xx (汇编基础篇) | cpu在哪里打卡上班? < csdn | 51cto | harmony > v21.xx (线程概念篇) | 是谁在不断的折腾cpu? < csdn | 51cto | harmony > v20.xx (用栈方式篇) | 栈是构建底层运行的基础 < csdn | 51cto | harmony > v19.xx (位图管理篇) | 为何进程和线程优先级都是32个? < csdn | 51cto | harmony > v18.xx (源码结构篇) | 梳理内核源文件的作用和含义 < csdn | 51cto | harmony > v17.xx (物理内存篇) | 这样记伙伴算法永远不会忘 < csdn | 51cto | harmony > v16.xx (内存规则篇) | 内存管理到底在管什么? < csdn | 51cto | harmony > v15.xx (内存映射篇) | 什么是内存最重要的实现基础 ? < csdn | 51cto | harmony > v14.xx (内存汇编篇) | 什么是虚拟内存的实现基础? < csdn | 51cto | harmony > v13.xx (源码注释篇) | 热爱是所有的理由和答案 < csdn | 51cto | harmony > v12.xx (内存管理篇) | 虚拟内存全景图是怎样的? < csdn | 51cto | harmony > v11.xx (内存分配篇) | 内存有哪些分配方式? < csdn | 51cto | harmony > v10.xx (内存主奴篇) | 紫禁城的主子和奴才如何相处? < csdn | 51cto | harmony > v09.xx (调度故事篇) | 用故事说内核调度过程 < csdn | 51cto | harmony > v08.xx (总目录) | 百万汉字注解 百篇博客分析 < csdn | 51cto | harmony > v07.xx (调度机制篇) | 任务是如何被调度执行的? < csdn | 51cto | harmony > v06.xx (调度队列篇) | 内核有多少个调度队列? < csdn | 51cto | harmony > v05.xx (任务管理篇) | 任务池是如何管理的? < csdn | 51cto | harmony > v04.xx (任务调度篇) | 任务是内核调度的单元 < csdn | 51cto | harmony > v03.xx (时钟任务篇) | 调度最大的源动力来自哪里? < csdn | 51cto | harmony > v02.xx (进程管理篇) | 进程是内核资源管理单元 < csdn | 51cto | harmony > v01.xx (双向链表篇) | 谁是内核最重要结构体? < csdn | 51cto | harmony > 参与贡献 Fork 本仓库 >> 新建 Feat_xxx 分支 >> 提交代码注解 >> 新建 Pull Request 新建 Issue 喜欢请「点赞+关注+收藏」 各大站点搜 「鸿蒙内核源码分析」.欢迎转载,请注明出处. 进入 >> oschina | csdn | 51cto | 简书 | 掘金 | harmony

优秀的个人博客,低调大师

鸿蒙内核源码分析(信号量篇) | 信号量解决任务同步问题 | 中文注解HarmonyOS源码 | v29.01

百万汉字注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< Gitee | Github | CSDN | Coding > 百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,多站点每日同步更新< OSCHINA | CSDN | WeHarmony > 本篇说清楚信号量 读本篇之前建议先读鸿蒙内核源码分析(总目录)其他篇幅. 基本概念 信号量(Semaphore) 是一种实现任务间通信的机制,可以实现任务间同步或共享资源的互斥访问。 一个信号量的数据结构中,通常有一个计数值,用于对有效资源数的计数,表示剩下的可被使用的共享资源数,其值的含义分两种情况: 0,表示该信号量当前不可获取,因此可能存在正在等待该信号量的任务。 正值,表示该信号量当前可被获取。 以同步为目的的信号量和以互斥为目的的信号量在使用上有如下不同: 用作互斥时,初始信号量计数值不为0,表示可用的共享资源个数。在需要使用共享资源前,先获取信号量,然后使用一个共享资源,使用完毕后释放信号量。这样在共享资源被取完,即信号量计数减至0时,其他需要获取信号量的任务将被阻塞,从而保证了共享资源的互斥访问。另外,当共享资源数为1时,建议使用二值信号量,一种类似于互斥锁的机制。 用作同步时,初始信号量计数值为0。任务1获取信号量而阻塞,直到任务2或者某中断释放信号量,任务1才得以进入Ready或Running态,从而达到了任务间的同步。 信号量运作原理 信号量初始化,为配置的N个信号量申请内存(N值可以由用户自行配置,通过 LOSCFG_BASE_IPC_SEM_LIMIT 宏实现),并把所有信号量初始化成未使用,加入到未使用链表中供系统使用。 信号量创建,从未使用的信号量链表中获取一个信号量,并设定初值。 信号量申请,若其计数器值大于0,则直接减1返回成功。否则任务阻塞,等待其它任务释放该信号量, 等待的超时时间可设定。当任务被一个信号量阻塞时,将该任务挂到信号量等待任务队列的队尾。 信号量释放,若没有任务等待该信号量,则直接将计数器加1返回。否则唤醒该信号量等待任务队列上的第一个任务。 信号量删除,将正在使用的信号量置为未使用信号量,并挂回到未使用链表。 信号量允许多个任务在同一时刻访问共享资源,但会限制同一时刻访问此资源的最大任务数目。 当访问资源的任务数达到该资源允许的最大数量时,会阻塞其他试图获取该资源的任务,直到有任务释放该信号量。 信号量长什么样? typedef struct { UINT8 semStat; /**< Semaphore state *///信号量的状态 UINT16 semCount; /**< Number of available semaphores *///有效信号量的数量 UINT16 maxSemCount; /**< Max number of available semaphores *///有效信号量的最大数量 UINT32 semID; /**< Semaphore control structure ID *///信号量索引号 LOS_DL_LIST semList; /**< Queue of tasks that are waiting on a semaphore *///等待信号量的任务队列,任务通过阻塞节点挂上去 } LosSemCB; semList,这又是一个双向链表, 双向链表是内核最重要的结构体, 可前往 鸿蒙内核源码分析(总目录) 查看双向链表篇, LOS_DL_LIST像狗皮膏药一样牢牢的寄生在宿主结构体上semList上挂的是未来所有等待这个信号量的任务. 初始化信号量模块 #ifndef LOSCFG_BASE_IPC_SEM_LIMIT #define LOSCFG_BASE_IPC_SEM_LIMIT 1024 //信号量的最大个数 #endif LITE_OS_SEC_TEXT_INIT UINT32 OsSemInit(VOID)//信号量初始化 { LosSemCB *semNode = NULL; UINT32 index; LOS_ListInit(&g_unusedSemList);//初始 /* system resident memory, don't free */ g_allSem = (LosSemCB *)LOS_MemAlloc(m_aucSysMem0, (LOSCFG_BASE_IPC_SEM_LIMIT * sizeof(LosSemCB)));//分配信号池 if (g_allSem == NULL) { return LOS_ERRNO_SEM_NO_MEMORY; } for (index = 0; index < LOSCFG_BASE_IPC_SEM_LIMIT; index++) { semNode = ((LosSemCB *)g_allSem) + index;//拿信号控制块, 可以直接g_allSem[index]来嘛 semNode->semID = SET_SEM_ID(0, index);//保存ID semNode->semStat = OS_SEM_UNUSED;//标记未使用 LOS_ListTailInsert(&g_unusedSemList, &semNode->semList);//通过semList把 信号块挂到空闲链表上 } if (OsSemDbgInitHook() != LOS_OK) { return LOS_ERRNO_SEM_NO_MEMORY; } return LOS_OK; } 分析如下: 初始化创建了信号量池来统一管理信号量, 默认 1024 个信号量 信号ID范围从 [0,1023] 未分配使用的信号量都挂到了全局变量 g_unusedSemList 上. 小建议:鸿蒙内核其他池(如进程池,任务池)都采用free来命名空闲链表,而此处使用unused,命名风格不太严谨,有待改善. 创建信号量 LITE_OS_SEC_TEXT_INIT UINT32 OsSemCreate(UINT16 count, UINT16 maxCount, UINT32 *semHandle) { unusedSem = LOS_DL_LIST_FIRST(&g_unusedSemList);//从未使用信号量池中取首个 LOS_ListDelete(unusedSem);//从空闲链表上摘除 semCreated = GET_SEM_LIST(unusedSem);//通过semList挂到链表上的,这里也要通过它把LosSemCB头查到. 进程,线程等结构体也都是这么干的. semCreated->semCount = count;//设置数量 semCreated->semStat = OS_SEM_USED;//设置可用状态 semCreated->maxSemCount = maxCount;//设置最大信号数量 LOS_ListInit(&semCreated->semList);//初始化链表,后续阻塞任务通过task->pendList挂到semList链表上,就知道哪些任务在等它了. *semHandle = semCreated->semID;//参数带走 semID OsSemDbgUpdateHook(semCreated->semID, OsCurrTaskGet()->taskEntry, count); return LOS_OK; ERR_HANDLER: OS_RETURN_ERROR_P2(errLine, errNo); } 分析如下: 从未使用的空闲链表中拿首个信号量供分配使用. 信号量的最大数量和信号量个数都由参数指定. 信号量状态由 OS_SEM_UNUSED 变成了 OS_SEM_USED semHandle带走信号量ID,外部由此知道成功创建了一个编号为 *semHandle 的信号量 申请信号量 LITE_OS_SEC_TEXT UINT32 LOS_SemPend(UINT32 semHandle, UINT32 timeout) { UINT32 intSave; LosSemCB *semPended = GET_SEM(semHandle);//通过ID拿到信号体 UINT32 retErr = LOS_OK; LosTaskCB *runTask = NULL; if (GET_SEM_INDEX(semHandle) >= (UINT32)LOSCFG_BASE_IPC_SEM_LIMIT) { OS_RETURN_ERROR(LOS_ERRNO_SEM_INVALID); } if (OS_INT_ACTIVE) { PRINT_ERR("!!!LOS_ERRNO_SEM_PEND_INTERR!!!\n"); OsBackTrace(); return LOS_ERRNO_SEM_PEND_INTERR; } runTask = OsCurrTaskGet();//获取当前任务 if (runTask->taskStatus & OS_TASK_FLAG_SYSTEM_TASK) { OsBackTrace(); return LOS_ERRNO_SEM_PEND_IN_SYSTEM_TASK; } SCHEDULER_LOCK(intSave); if ((semPended->semStat == OS_SEM_UNUSED) || (semPended->semID != semHandle)) { retErr = LOS_ERRNO_SEM_INVALID; goto OUT; } /* Update the operate time, no matter the actual Pend success or not */ OsSemDbgTimeUpdateHook(semHandle); if (semPended->semCount > 0) {//还有资源可用,返回肯定得成功,semCount=0时代表没资源了,task会必须去睡眠了 semPended->semCount--;//资源少了一个 goto OUT;//注意这里 retErr = LOS_OK ,所以返回是OK的 } else if (!timeout) { retErr = LOS_ERRNO_SEM_UNAVAILABLE; goto OUT; } if (!OsPreemptableInSched()) {//不能申请调度 (不能调度的原因是因为没有持有调度任务自旋锁) PRINT_ERR("!!!LOS_ERRNO_SEM_PEND_IN_LOCK!!!\n"); OsBackTrace(); retErr = LOS_ERRNO_SEM_PEND_IN_LOCK; goto OUT; } runTask->taskSem = (VOID *)semPended;//标记当前任务在等这个信号量 retErr = OsTaskWait(&semPended->semList, timeout, TRUE);//任务进入等待状态,当前任务会挂到semList上,并在其中切换任务上下文 if (retErr == LOS_ERRNO_TSK_TIMEOUT) {//注意:这里是涉及到task切换的,把自己挂起,唤醒其他task runTask->taskSem = NULL; retErr = LOS_ERRNO_SEM_TIMEOUT; } OUT: SCHEDULER_UNLOCK(intSave); return retErr; } 分析如下: 这个函数有点复杂,大量的goto,但别被它绕晕了,盯着返回值看. 先说结果只有一种情况下申请信号量能成功(即 retErr == LOS_OK) if (semPended->semCount > 0) {//还有资源可用,返回肯定得成功,semCount=0时代表没资源了,task会必须去睡眠了 semPended->semCount--;//资源少了一个 goto OUT;//注意这里 retErr = LOS_OK ,所以返回是OK的 } 其余申请失败的原因有: 信号量ID超出范围(默认1024) 中断发生期间 系统任务 信号量状态不对,信号量ID不匹配 以上都是异常的判断,再说正常情况下 semPended->semCount = 0时的情况,没有资源了怎么办? 任务进入 OsTaskWait 睡眠状态,怎么睡,睡多久,由参数 timeout 定 timeout 值分以下三种模式: 无阻塞模式:即任务申请信号量时,入参 timeout 等于0。若当前信号量计数值不为0,则申请成功,否则立即返回申请失败。 永久阻塞模式:即任务申请信号量时,入参 timeout 等于0xFFFFFFFF。若当前信号量计数值不为0,则申请成功。 否则该任务进入阻塞态,系统切换到就绪任务中优先级最高者继续执行。任务进入阻塞态后,直到有其他任务释放该信号量,阻塞任务才会重新得以执行。 定时阻塞模式:即任务申请信号量时,0<timeout<0xFFFFFFFF。若当前信号量计数值不为0,则申请成功。 否则,该任务进入阻塞态,系统切换到就绪任务中优先级最高者继续执行。任务进入阻塞态后, 超时前如果有其他任务释放该信号量,则该任务可成功获取信号量继续执行,若超时前未获取到信号量,接口将返回超时错误码。 在 OsTaskWait 中,任务将被挂入semList链表,semList上挂的都是等待这个信号量的任务. 释放信号量 LITE_OS_SEC_TEXT UINT32 OsSemPostUnsafe(UINT32 semHandle, BOOL *needSched) { LosSemCB *semPosted = NULL; LosTaskCB *resumedTask = NULL; if (GET_SEM_INDEX(semHandle) >= LOSCFG_BASE_IPC_SEM_LIMIT) { return LOS_ERRNO_SEM_INVALID; } semPosted = GET_SEM(semHandle); if ((semPosted->semID != semHandle) || (semPosted->semStat == OS_SEM_UNUSED)) { return LOS_ERRNO_SEM_INVALID; } /* Update the operate time, no matter the actual Post success or not */ OsSemDbgTimeUpdateHook(semHandle); if (semPosted->semCount == OS_SEM_COUNT_MAX) {//当前信号资源不能大于最大资源量 return LOS_ERRNO_SEM_OVERFLOW; } if (!LOS_ListEmpty(&semPosted->semList)) {//当前有任务挂在semList上,要去唤醒任务 resumedTask = OS_TCB_FROM_PENDLIST(LOS_DL_LIST_FIRST(&(semPosted->semList)));//semList上面挂的都是task->pendlist节点,取第一个task下来唤醒 resumedTask->taskSem = NULL;//任务不用等信号了,重新变成NULL值 OsTaskWake(resumedTask);//唤醒任务,注意resumedTask一定不是当前任务,OsTaskWake里面并不会自己切换任务上下文,只是设置状态 if (needSched != NULL) {//参数不为空,就返回需要调度的标签 *needSched = TRUE;//TRUE代表需要调度 } } else {//当前没有任务挂在semList上, semPosted->semCount++;//信号资源多一个 } return LOS_OK; } LITE_OS_SEC_TEXT UINT32 LOS_SemPost(UINT32 semHandle) { UINT32 intSave; UINT32 ret; BOOL needSched = FALSE; SCHEDULER_LOCK(intSave); ret = OsSemPostUnsafe(semHandle, &needSched); SCHEDULER_UNLOCK(intSave); if (needSched) {//需要调度的情况 LOS_MpSchedule(OS_MP_CPU_ALL);//向所有CPU发送调度指令 LOS_Schedule();////发起调度 } return ret; } 分析如下: 注意看在什么情况下 semPosted->semCount 才会 ++ ,是在LOS_ListEmpty为真的时候,semList是等待这个信号量的任务. semList上的任务是在OsTaskWait中挂入的.都在等这个信号. 每次OsSemPost都会唤醒semList链表上一个任务,直到semList为空. 掌握信号量的核心是理解 LOS_SemPend 和 LOS_SemPost 官方代码 /* 任务ID */ static UINT32 g_testTaskId01; static UINT32 g_testTaskId02; /* 测试任务优先级 */ #define TASK_PRIO_TEST 5 /* 信号量结构体id */ static UINT32 g_semId; VOID Example_SemTask1(VOID) { UINT32 ret; printf("Example_SemTask1 try get sem g_semId ,timeout 10 ticks.\n"); /* 定时阻塞模式申请信号量,定时时间为10ticks */ ret = LOS_SemPend(g_semId, 10); /*申请到信号量*/ if (ret == LOS_OK) { LOS_SemPost(g_semId); return; } /* 定时时间到,未申请到信号量 */ if (ret == LOS_ERRNO_SEM_TIMEOUT) { printf("Example_SemTask1 timeout and try get sem g_semId wait forever.\n"); /*永久阻塞模式申请信号量*/ ret = LOS_SemPend(g_semId, LOS_WAIT_FOREVER); printf("Example_SemTask1 wait_forever and get sem g_semId .\n"); if (ret == LOS_OK) { LOS_SemPost(g_semId); return; } } } VOID Example_SemTask2(VOID) { UINT32 ret; printf("Example_SemTask2 try get sem g_semId wait forever.\n"); /* 永久阻塞模式申请信号量 */ ret = LOS_SemPend(g_semId, LOS_WAIT_FOREVER); if (ret == LOS_OK) { printf("Example_SemTask2 get sem g_semId and then delay 20ticks .\n"); } /* 任务休眠20 ticks */ LOS_TaskDelay(20); printf("Example_SemTask2 post sem g_semId .\n"); /* 释放信号量 */ LOS_SemPost(g_semId); return; } UINT32 ExampleTaskEntry(VOID) { UINT32 ret; TSK_INIT_PARAM_S task1; TSK_INIT_PARAM_S task2; /* 创建信号量 */ LOS_SemCreate(0,&g_semId); /* 锁任务调度 */ LOS_TaskLock(); /*创建任务1*/ (VOID)memset_s(&task1, sizeof(TSK_INIT_PARAM_S), 0, sizeof(TSK_INIT_PARAM_S)); task1.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_SemTask1; task1.pcName = "TestTsk1"; task1.uwStackSize = OS_TSK_DEFAULT_STACK_SIZE; task1.usTaskPrio = TASK_PRIO_TEST; ret = LOS_TaskCreate(&g_testTaskId01, &task1); if (ret != LOS_OK) { printf("task1 create failed .\n"); return LOS_NOK; } /* 创建任务2 */ (VOID)memset_s(&task2, sizeof(TSK_INIT_PARAM_S), 0, sizeof(TSK_INIT_PARAM_S)); task2.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_SemTask2; task2.pcName = "TestTsk2"; task2.uwStackSize = OS_TSK_DEFAULT_STACK_SIZE; task2.usTaskPrio = (TASK_PRIO_TEST - 1); ret = LOS_TaskCreate(&g_testTaskId02, &task2); if (ret != LOS_OK) { printf("task2 create failed .\n"); return LOS_NOK; } /* 解锁任务调度 */ LOS_TaskUnlock(); ret = LOS_SemPost(g_semId); /* 任务休眠40 ticks */ LOS_TaskDelay(40); /* 删除信号量 */ LOS_SemDelete(g_semId); /* 删除任务1 */ ret = LOS_TaskDelete(g_testTaskId01); if (ret != LOS_OK) { printf("task1 delete failed .\n"); return LOS_NOK; } /* 删除任务2 */ ret = LOS_TaskDelete(g_testTaskId02); if (ret != LOS_OK) { printf("task2 delete failed .\n"); return LOS_NOK; } return LOS_OK; } 运行结果: Example_SemTask2 try get sem g_semId wait forever. Example_SemTask1 try get sem g_semId ,timeout 10 ticks. Example_SemTask2 get sem g_semId and then delay 20ticks . Example_SemTask1 timeout and try get sem g_semId wait forever. Example_SemTask2 post sem g_semId . Example_SemTask1 wait_forever and get sem g_semId . 喜欢就请收藏吧 各大站点搜 "鸿蒙内核源码分析" ,快速找到组织. 百万汉字注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< Gitee | Github | CSDN | Coding > 百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,多站点每日同步更新< OSCHINA | CSDN | WeHarmony >

优秀的个人博客,低调大师

鸿蒙内核源码分析(信号量篇) | 信号量解决任务同步问题 | 中文注解HarmonyOS源码 | v29.02

百万汉字注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< Gitee | Github | CSDN | Coding > 百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,多站点每日同步更新< OSCHINA | CSDN | WeHarmony > 本篇说清楚信号量 读本篇之前建议先读鸿蒙内核源码分析(总目录)其他篇幅. 基本概念 信号量(Semaphore) 是一种实现任务间通信的机制,可以实现任务间同步或共享资源的互斥访问。 一个信号量的数据结构中,通常有一个计数值,用于对有效资源数的计数,表示剩下的可被使用的共享资源数,其值的含义分两种情况: 0,表示该信号量当前不可获取,因此可能存在正在等待该信号量的任务。 正值,表示该信号量当前可被获取。 以同步为目的的信号量和以互斥为目的的信号量在使用上有如下不同: 用作互斥时,初始信号量计数值不为0,表示可用的共享资源个数。在需要使用共享资源前,先获取信号量,然后使用一个共享资源,使用完毕后释放信号量。这样在共享资源被取完,即信号量计数减至0时,其他需要获取信号量的任务将被阻塞,从而保证了共享资源的互斥访问。另外,当共享资源数为1时,建议使用二值信号量,一种类似于互斥锁的机制。 用作同步时,初始信号量计数值为0。任务1获取信号量而阻塞,直到任务2或者某中断释放信号量,任务1才得以进入Ready或Running态,从而达到了任务间的同步。 信号量运作原理 信号量初始化,为配置的N个信号量申请内存(N值可以由用户自行配置,通过 LOSCFG_BASE_IPC_SEM_LIMIT 宏实现),并把所有信号量初始化成未使用,加入到未使用链表中供系统使用。 信号量创建,从未使用的信号量链表中获取一个信号量,并设定初值。 信号量申请,若其计数器值大于0,则直接减1返回成功。否则任务阻塞,等待其它任务释放该信号量, 等待的超时时间可设定。当任务被一个信号量阻塞时,将该任务挂到信号量等待任务队列的队尾。 信号量释放,若没有任务等待该信号量,则直接将计数器加1返回。否则唤醒该信号量等待任务队列上的第一个任务。 信号量删除,将正在使用的信号量置为未使用信号量,并挂回到未使用链表。 信号量允许多个任务在同一时刻访问共享资源,但会限制同一时刻访问此资源的最大任务数目。 当访问资源的任务数达到该资源允许的最大数量时,会阻塞其他试图获取该资源的任务,直到有任务释放该信号量。 信号量长什么样? typedef struct { UINT8 semStat; /**< Semaphore state *///信号量的状态 UINT16 semCount; /**< Number of available semaphores *///有效信号量的数量 UINT16 maxSemCount; /**< Max number of available semaphores *///有效信号量的最大数量 UINT32 semID; /**< Semaphore control structure ID *///信号量索引号 LOS_DL_LIST semList; /**< Queue of tasks that are waiting on a semaphore *///等待信号量的任务队列,任务通过阻塞节点挂上去 } LosSemCB; semList,这又是一个双向链表, 双向链表是内核最重要的结构体, 可前往 鸿蒙内核源码分析(总目录) 查看双向链表篇, LOS_DL_LIST像狗皮膏药一样牢牢的寄生在宿主结构体上semList上挂的是未来所有等待这个信号量的任务. 初始化信号量模块 #ifndef LOSCFG_BASE_IPC_SEM_LIMIT #define LOSCFG_BASE_IPC_SEM_LIMIT 1024 //信号量的最大个数 #endif LITE_OS_SEC_TEXT_INIT UINT32 OsSemInit(VOID)//信号量初始化 { LosSemCB *semNode = NULL; UINT32 index; LOS_ListInit(&g_unusedSemList);//初始 /* system resident memory, don't free */ g_allSem = (LosSemCB *)LOS_MemAlloc(m_aucSysMem0, (LOSCFG_BASE_IPC_SEM_LIMIT * sizeof(LosSemCB)));//分配信号池 if (g_allSem == NULL) { return LOS_ERRNO_SEM_NO_MEMORY; } for (index = 0; index < LOSCFG_BASE_IPC_SEM_LIMIT; index++) { semNode = ((LosSemCB *)g_allSem) + index;//拿信号控制块, 可以直接g_allSem[index]来嘛 semNode->semID = SET_SEM_ID(0, index);//保存ID semNode->semStat = OS_SEM_UNUSED;//标记未使用 LOS_ListTailInsert(&g_unusedSemList, &semNode->semList);//通过semList把 信号块挂到空闲链表上 } if (OsSemDbgInitHook() != LOS_OK) { return LOS_ERRNO_SEM_NO_MEMORY; } return LOS_OK; } 分析如下: 初始化创建了信号量池来统一管理信号量, 默认 1024 个信号量 信号ID范围从 [0,1023] 未分配使用的信号量都挂到了全局变量 g_unusedSemList 上. 小建议:鸿蒙内核其他池(如进程池,任务池)都采用free来命名空闲链表,而此处使用unused,命名风格不太严谨,有待改善. 创建信号量 LITE_OS_SEC_TEXT_INIT UINT32 OsSemCreate(UINT16 count, UINT16 maxCount, UINT32 *semHandle) { unusedSem = LOS_DL_LIST_FIRST(&g_unusedSemList);//从未使用信号量池中取首个 LOS_ListDelete(unusedSem);//从空闲链表上摘除 semCreated = GET_SEM_LIST(unusedSem);//通过semList挂到链表上的,这里也要通过它把LosSemCB头查到. 进程,线程等结构体也都是这么干的. semCreated->semCount = count;//设置数量 semCreated->semStat = OS_SEM_USED;//设置可用状态 semCreated->maxSemCount = maxCount;//设置最大信号数量 LOS_ListInit(&semCreated->semList);//初始化链表,后续阻塞任务通过task->pendList挂到semList链表上,就知道哪些任务在等它了. *semHandle = semCreated->semID;//参数带走 semID OsSemDbgUpdateHook(semCreated->semID, OsCurrTaskGet()->taskEntry, count); return LOS_OK; ERR_HANDLER: OS_RETURN_ERROR_P2(errLine, errNo); } 分析如下: 从未使用的空闲链表中拿首个信号量供分配使用. 信号量的最大数量和信号量个数都由参数指定. 信号量状态由 OS_SEM_UNUSED 变成了 OS_SEM_USED semHandle带走信号量ID,外部由此知道成功创建了一个编号为 *semHandle 的信号量 申请信号量 LITE_OS_SEC_TEXT UINT32 LOS_SemPend(UINT32 semHandle, UINT32 timeout) { UINT32 intSave; LosSemCB *semPended = GET_SEM(semHandle);//通过ID拿到信号体 UINT32 retErr = LOS_OK; LosTaskCB *runTask = NULL; if (GET_SEM_INDEX(semHandle) >= (UINT32)LOSCFG_BASE_IPC_SEM_LIMIT) { OS_RETURN_ERROR(LOS_ERRNO_SEM_INVALID); } if (OS_INT_ACTIVE) { PRINT_ERR("!!!LOS_ERRNO_SEM_PEND_INTERR!!!\n"); OsBackTrace(); return LOS_ERRNO_SEM_PEND_INTERR; } runTask = OsCurrTaskGet();//获取当前任务 if (runTask->taskStatus & OS_TASK_FLAG_SYSTEM_TASK) { OsBackTrace(); return LOS_ERRNO_SEM_PEND_IN_SYSTEM_TASK; } SCHEDULER_LOCK(intSave); if ((semPended->semStat == OS_SEM_UNUSED) || (semPended->semID != semHandle)) { retErr = LOS_ERRNO_SEM_INVALID; goto OUT; } /* Update the operate time, no matter the actual Pend success or not */ OsSemDbgTimeUpdateHook(semHandle); if (semPended->semCount > 0) {//还有资源可用,返回肯定得成功,semCount=0时代表没资源了,task会必须去睡眠了 semPended->semCount--;//资源少了一个 goto OUT;//注意这里 retErr = LOS_OK ,所以返回是OK的 } else if (!timeout) { retErr = LOS_ERRNO_SEM_UNAVAILABLE; goto OUT; } if (!OsPreemptableInSched()) {//不能申请调度 (不能调度的原因是因为没有持有调度任务自旋锁) PRINT_ERR("!!!LOS_ERRNO_SEM_PEND_IN_LOCK!!!\n"); OsBackTrace(); retErr = LOS_ERRNO_SEM_PEND_IN_LOCK; goto OUT; } runTask->taskSem = (VOID *)semPended;//标记当前任务在等这个信号量 retErr = OsTaskWait(&semPended->semList, timeout, TRUE);//任务进入等待状态,当前任务会挂到semList上,并在其中切换任务上下文 if (retErr == LOS_ERRNO_TSK_TIMEOUT) {//注意:这里是涉及到task切换的,把自己挂起,唤醒其他task runTask->taskSem = NULL; retErr = LOS_ERRNO_SEM_TIMEOUT; } OUT: SCHEDULER_UNLOCK(intSave); return retErr; } 分析如下: 这个函数有点复杂,大量的goto,但别被它绕晕了,盯着返回值看. 先说结果只有一种情况下申请信号量能成功(即 retErr == LOS_OK) if (semPended->semCount > 0) {//还有资源可用,返回肯定得成功,semCount=0时代表没资源了,task会必须去睡眠了 semPended->semCount--;//资源少了一个 goto OUT;//注意这里 retErr = LOS_OK ,所以返回是OK的 } 其余申请失败的原因有: 信号量ID超出范围(默认1024) 中断发生期间 系统任务 信号量状态不对,信号量ID不匹配 以上都是异常的判断,再说正常情况下 semPended->semCount = 0时的情况,没有资源了怎么办? 任务进入 OsTaskWait 睡眠状态,怎么睡,睡多久,由参数 timeout 定 timeout 值分以下三种模式: 无阻塞模式:即任务申请信号量时,入参 timeout 等于0。若当前信号量计数值不为0,则申请成功,否则立即返回申请失败。 永久阻塞模式:即任务申请信号量时,入参 timeout 等于0xFFFFFFFF。若当前信号量计数值不为0,则申请成功。 否则该任务进入阻塞态,系统切换到就绪任务中优先级最高者继续执行。任务进入阻塞态后,直到有其他任务释放该信号量,阻塞任务才会重新得以执行。 定时阻塞模式:即任务申请信号量时,0<timeout<0xFFFFFFFF。若当前信号量计数值不为0,则申请成功。 否则,该任务进入阻塞态,系统切换到就绪任务中优先级最高者继续执行。任务进入阻塞态后, 超时前如果有其他任务释放该信号量,则该任务可成功获取信号量继续执行,若超时前未获取到信号量,接口将返回超时错误码。 在 OsTaskWait 中,任务将被挂入semList链表,semList上挂的都是等待这个信号量的任务. 释放信号量 LITE_OS_SEC_TEXT UINT32 OsSemPostUnsafe(UINT32 semHandle, BOOL *needSched) { LosSemCB *semPosted = NULL; LosTaskCB *resumedTask = NULL; if (GET_SEM_INDEX(semHandle) >= LOSCFG_BASE_IPC_SEM_LIMIT) { return LOS_ERRNO_SEM_INVALID; } semPosted = GET_SEM(semHandle); if ((semPosted->semID != semHandle) || (semPosted->semStat == OS_SEM_UNUSED)) { return LOS_ERRNO_SEM_INVALID; } /* Update the operate time, no matter the actual Post success or not */ OsSemDbgTimeUpdateHook(semHandle); if (semPosted->semCount == OS_SEM_COUNT_MAX) {//当前信号资源不能大于最大资源量 return LOS_ERRNO_SEM_OVERFLOW; } if (!LOS_ListEmpty(&semPosted->semList)) {//当前有任务挂在semList上,要去唤醒任务 resumedTask = OS_TCB_FROM_PENDLIST(LOS_DL_LIST_FIRST(&(semPosted->semList)));//semList上面挂的都是task->pendlist节点,取第一个task下来唤醒 resumedTask->taskSem = NULL;//任务不用等信号了,重新变成NULL值 OsTaskWake(resumedTask);//唤醒任务,注意resumedTask一定不是当前任务,OsTaskWake里面并不会自己切换任务上下文,只是设置状态 if (needSched != NULL) {//参数不为空,就返回需要调度的标签 *needSched = TRUE;//TRUE代表需要调度 } } else {//当前没有任务挂在semList上, semPosted->semCount++;//信号资源多一个 } return LOS_OK; } LITE_OS_SEC_TEXT UINT32 LOS_SemPost(UINT32 semHandle) { UINT32 intSave; UINT32 ret; BOOL needSched = FALSE; SCHEDULER_LOCK(intSave); ret = OsSemPostUnsafe(semHandle, &needSched); SCHEDULER_UNLOCK(intSave); if (needSched) {//需要调度的情况 LOS_MpSchedule(OS_MP_CPU_ALL);//向所有CPU发送调度指令 LOS_Schedule();////发起调度 } return ret; } 分析如下: 注意看在什么情况下 semPosted->semCount 才会 ++ ,是在LOS_ListEmpty为真的时候,semList是等待这个信号量的任务. semList上的任务是在OsTaskWait中挂入的.都在等这个信号. 每次OsSemPost都会唤醒semList链表上一个任务,直到semList为空. 掌握信号量的核心是理解 LOS_SemPend 和 LOS_SemPost 编程示例 本实例实现如下功能: 测试任务Example_TaskEntry创建一个信号量,锁任务调度,创建两个任务Example_SemTask1、Example_SemTask2,Example_SemTask2优先级高于Example_SemTask1,两个任务中申请同一信号量,解锁任务调度后两任务阻塞,测试任务Example_TaskEntry释放信号量。 Example_SemTask2得到信号量,被调度,然后任务休眠20Tick,Example_SemTask2延迟,Example_SemTask1被唤醒。 Example_SemTask1定时阻塞模式申请信号量,等待时间为10Tick,因信号量仍被Example_SemTask2持有,Example_SemTask1挂起,10Tick后仍未得到信号量,Example_SemTask1被唤醒,试图以永久阻塞模式申请信号量,Example_SemTask1挂起。 20Tick后Example_SemTask2唤醒, 释放信号量后,Example_SemTask1得到信号量被调度运行,最后释放信号量。 Example_SemTask1执行完,40Tick后任务Example_TaskEntry被唤醒,执行删除信号量,删除两个任务。 /* 任务ID */ static UINT32 g_testTaskId01; static UINT32 g_testTaskId02; /* 测试任务优先级 */ #define TASK_PRIO_TEST 5 /* 信号量结构体id */ static UINT32 g_semId; VOID Example_SemTask1(VOID) { UINT32 ret; printf("Example_SemTask1 try get sem g_semId ,timeout 10 ticks.\n"); /* 定时阻塞模式申请信号量,定时时间为10ticks */ ret = LOS_SemPend(g_semId, 10); /*申请到信号量*/ if (ret == LOS_OK) { LOS_SemPost(g_semId); return; } /* 定时时间到,未申请到信号量 */ if (ret == LOS_ERRNO_SEM_TIMEOUT) { printf("Example_SemTask1 timeout and try get sem g_semId wait forever.\n"); /*永久阻塞模式申请信号量*/ ret = LOS_SemPend(g_semId, LOS_WAIT_FOREVER); printf("Example_SemTask1 wait_forever and get sem g_semId .\n"); if (ret == LOS_OK) { LOS_SemPost(g_semId); return; } } } VOID Example_SemTask2(VOID) { UINT32 ret; printf("Example_SemTask2 try get sem g_semId wait forever.\n"); /* 永久阻塞模式申请信号量 */ ret = LOS_SemPend(g_semId, LOS_WAIT_FOREVER); if (ret == LOS_OK) { printf("Example_SemTask2 get sem g_semId and then delay 20ticks .\n"); } /* 任务休眠20 ticks */ LOS_TaskDelay(20); printf("Example_SemTask2 post sem g_semId .\n"); /* 释放信号量 */ LOS_SemPost(g_semId); return; } UINT32 ExampleTaskEntry(VOID) { UINT32 ret; TSK_INIT_PARAM_S task1; TSK_INIT_PARAM_S task2; /* 创建信号量 */ LOS_SemCreate(0,&g_semId); /* 锁任务调度 */ LOS_TaskLock(); /*创建任务1*/ (VOID)memset_s(&task1, sizeof(TSK_INIT_PARAM_S), 0, sizeof(TSK_INIT_PARAM_S)); task1.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_SemTask1; task1.pcName = "TestTsk1"; task1.uwStackSize = OS_TSK_DEFAULT_STACK_SIZE; task1.usTaskPrio = TASK_PRIO_TEST; ret = LOS_TaskCreate(&g_testTaskId01, &task1); if (ret != LOS_OK) { printf("task1 create failed .\n"); return LOS_NOK; } /* 创建任务2 */ (VOID)memset_s(&task2, sizeof(TSK_INIT_PARAM_S), 0, sizeof(TSK_INIT_PARAM_S)); task2.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_SemTask2; task2.pcName = "TestTsk2"; task2.uwStackSize = OS_TSK_DEFAULT_STACK_SIZE; task2.usTaskPrio = (TASK_PRIO_TEST - 1); ret = LOS_TaskCreate(&g_testTaskId02, &task2); if (ret != LOS_OK) { printf("task2 create failed .\n"); return LOS_NOK; } /* 解锁任务调度 */ LOS_TaskUnlock(); ret = LOS_SemPost(g_semId); /* 任务休眠40 ticks */ LOS_TaskDelay(40); /* 删除信号量 */ LOS_SemDelete(g_semId); /* 删除任务1 */ ret = LOS_TaskDelete(g_testTaskId01); if (ret != LOS_OK) { printf("task1 delete failed .\n"); return LOS_NOK; } /* 删除任务2 */ ret = LOS_TaskDelete(g_testTaskId02); if (ret != LOS_OK) { printf("task2 delete failed .\n"); return LOS_NOK; } return LOS_OK; } 运行结果: Example_SemTask2 try get sem g_semId wait forever. Example_SemTask1 try get sem g_semId ,timeout 10 ticks. Example_SemTask2 get sem g_semId and then delay 20ticks . Example_SemTask1 timeout and try get sem g_semId wait forever. Example_SemTask2 post sem g_semId . Example_SemTask1 wait_forever and get sem g_semId . 喜欢就请收藏吧 各大站点搜 "鸿蒙内核源码分析" ,快速找到组织. 百万汉字注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< Gitee | Github | CSDN | Coding > 百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,多站点每日同步更新< OSCHINA | CSDN | WeHarmony >

优秀的个人博客,低调大师

| 中文注解HarmonyOS源码 | v28.01

鸿蒙内核源码中文注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆, 四大源码仓每日同步更新< Gitee | Github | CSDN | Coding > 鸿蒙内核源码分析博客 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点每日同步更新< OSCHINA | CSDN | WeHarmony > 本篇开始介绍进程通讯(IPC) 读本篇之前建议先读鸿蒙内核源码分析(总目录)其他篇幅. 进程间为何要通讯 ? 鸿蒙内核默认支持 64个进程 和 128个任务. 内核设计尽量不去打扰它们,让各人过好各自的日子, 但大家毕竟在一口锅里吃饭, 不可能不与外界联系, 联系就得有方法,有规矩. 举两个应用场景说明下通讯的必要性: 一.被动式 广为熟知的shell命令 kill 9 13 ,是通过 shell任务给 13号进程发送一个干掉它的信号. #define SIGKILL 9 //常用的命令 kill 9 13 这是被动式通讯的场景,至于为什么要干掉你,原因可能很多啊,很可能是检测到13占用内存太多了,也可能13太低调长期不活跃,启动新进程发现没位置了,得收了你.反正系统必须得有对付你的抓手. 二.主动式的 ,比如要访问某些公共资源(全局变量,消息队列),而资源有限或具有排他性,别人正在使用导致你不能用, 所以需统一管理,要用就必须要先申请,按规矩办事,毕竟和谐社会没规矩不成方圆. 总之大概有以下几种需求需要通讯: (1).数据传输: 一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几M字节之间 (2).共享数据: 多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到。 (3).通知事件: 一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。 (4).资源共享: 多个进程之间共享同样的资源。为了作到这一点,需要内核提供锁和同步机制。 (5).进程控制: 有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。 内核目录和系列篇更新 内核有个专门的IPC目录,详见如下. 可直接点击查看注解源码. 进程间通讯方式 查看源码注解点击一下 .c 文件 1.管道pipe(fs_syscall.c) 管道是一种最基本的IPC机制,作用于有血缘关系的进程之间,完成数据传递。 调用pipe系统函数即可创建一个管道。有如下特质: 其本质是一个伪文件(实为内核缓冲区) 由两个文件描述符引用,一个表示读端,一个表示写端。 规定数据从管道的写端流入管道,从读端流出。 管道的原理: 管道实为内核使用环形队列机制,借助内核缓冲区(4k)实现。 管道的局限性: ① 数据自己读不能自己写。 ② 数据一旦被读走,便不在管道中存在,不可反复读取。 ③ 由于管道采用半双工通信方式。因此,数据只能在一个方向上流动。 ④ 只能在有公共祖先的进程间使用管道。 常见的通信方式有,单工通信、半双工通信、全双工通信。 鸿蒙的管道实现很简单, 详细看SysPipe函数. 2.信号(los_signal.c) 信号是用于进程间互相通信或者操作的一种机制,信号可以在任何时候发给某一进程,而无需知道该进程的状态。 如果该进程当前并未处于执行状态,则该信号就由内核保存起来,直到该进程被调度执行并传递给它为止。 如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程。 软中断信号(signal,又简称为信号)用来通知进程发生了异步事件。在软件层次上是对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是进程间通信机制中唯一的异步通信机制,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。 进程之间可以互相通过系统调用kill发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。信号机制除了基本通知功能外,还可以传递附加信息。 #define SIGHUP 1 //终端挂起或者控制进程终止 #define SIGINT 2 //键盘中断(如break键被按下) #define SIGQUIT 3 //键盘的退出键被按下 #define SIGILL 4 //非法指令 #define SIGTRAP 5 //跟踪陷阱(trace trap),启动进程,跟踪代码的执行 #define SIGABRT 6 //由abort(3)发出的退出指令 #define SIGIOT SIGABRT #define SIGBUS 7 //总线错误 #define SIGFPE 8 //浮点异常 #define SIGKILL 9 //常用的命令 kill 9 13 #define SIGUSR1 10 //用户自定义信号1 #define SIGSEGV 11 //无效的内存引用, 段违例(segmentation violation),进程试图去访问其虚地址空间以外的位置 #define SIGUSR2 12 //用户自定义信号2 #define SIGPIPE 13 //向某个非读管道中写入数据 #define SIGALRM 14 //由alarm(2)发出的信号,默认行为为进程终止 #define SIGTERM 15 //软件终止(software termination) #define SIGSTKFLT 16 #define SIGCHLD 17 //子进程结束信号 #define SIGCONT 18 //进程继续(曾被停止的进程) #define SIGSTOP 19 //终止进程 #define SIGTSTP 20 //控制终端(tty)上 按下停止键 3.消息队列(los_queue.c) 基本概念 队列又称消息队列,是一种常用于任务间通信的数据结构。队列接收来自任务或中断的 不固定长度消息,并根据不同的接口确定传递的消息是否存放在队列空间中。 任务能够从队列里面读取消息,当队列中的消息为空时,挂起读取任务;当队列中有新消息时, 挂起的读取任务被唤醒并处理新消息。任务也能够往队列里写入消息,当队列已经写满消息时, 挂起写入任务;当队列中有空闲消息节点时,挂起的写入任务被唤醒并写入消息。如果将 读队列和写队列的超时时间设置为0,则不会挂起任务,接口会直接返回,这就是非阻塞模式。 消息队列提供了异步处理机制,允许将一个消息放入队列,但不立即处理。同时队列还有缓冲消息的作用。 队列特性 消息以先进先出的方式排队,支持异步读写。 读队列和写队列都支持超时机制。 每读取一条消息,就会将该消息节点设置为空闲。 发送消息类型由通信双方约定,可以允许不同长度(不超过队列的消息节点大小)的消息。 一个任务能够从任意一个消息队列接收和发送消息。 多个任务能够从同一个消息队列接收和发送消息。 创建队列时所需的队列空间,默认支持接口内系统自行动态申请内存的方式,同时也支持将用户分配的队列空间作为接口入参传入的方式。 4.共享内存(shm.c) 共享内存是进程间通信中最简单的方式之一。共享内存允许两个或更多进程访问同一块内存 不同进程返回了指向同一个物理内存区域的指针。当一个进程改变了这块地址中的内容的时候, 其它进程都会察觉到这个更改。 (共享内存篇)| 正在更新中... 5.信号量(los_sem.c) 基本概念 信号量(Semaphore)是一种实现任务间通信的机制,可以实现任务间同步或共享资源的互斥访问。 一个信号量的数据结构中,通常有一个计数值,用于对有效资源数的计数,表示剩下的可被使用的共享资源数,其值的含义分两种情况: 0,表示该信号量当前不可获取,因此可能存在正在等待该信号量的任务。 正值,表示该信号量当前可被获取。 以同步为目的的信号量和以互斥为目的的信号量在使用上有如下不同: 用作互斥时,初始信号量计数值不为0,表示可用的共享资源个数。在需要使用共享资源前,先获取信号量, 然后使用一个共享资源,使用完毕后释放信号量。这样在共享资源被取完,即信号量计数减至0时,其他 需要获取信号量的任务将被阻塞,从而保证了共享资源的互斥访问。另外,当共享资源数为1时, 建议使用二值信号量,一种类似于互斥锁的机制。 用作同步时,初始信号量计数值为0。任务1获取信号量而阻塞,直到任务2或者某中断释放信号量, 任务1才得以进入Ready或Running态,从而达到了任务间的同步。 使用场景 在多任务系统中,信号量是一种非常灵活的同步方式,可以运用在多种场合中,实现锁、同步、资源计数等功能, 也能方便的用于任务与任务,中断与任务的同步中。信号量常用于协助一组相互竞争的任务访问共享资源。 (信号量篇)|正在更新中... 6.互斥锁 (los_mux.c) : 基本概念 互斥锁又称互斥型信号量,是一种特殊的二值性信号量,用于实现对临界资源的独占式处理。 另外,互斥锁可以解决信号量存在的优先级翻转问题。 任意时刻互斥锁只有两种状态,开锁或闭锁。当任务持有时,这个任务获得该互斥锁的所有权, 互斥锁处于闭锁状态。当该任务释放锁后,任务失去该互斥锁的所有权,互斥锁处于开锁状态。 当一个任务持有互斥锁时,其他任务不能再对该互斥锁进行开锁或持有。 详见: (互斥锁篇) | 为何任务会因拥有锁而被迫改变优先级 ? 7.快锁 (los_futex.c) futex 是Fast Userspace muTexes的缩写(快速用户空间互斥体),是一种用户态和内核态混合的同步机制。首先,同步的进程间通过mmap共享一段内存,futex变量就位于这段共享的内存中且操作是原子的,当进程尝试进入互斥区或者退出互斥区的时候,先去查看共享内存中的futex变量,如果没有竞争发生,则只修改futex,而不用再执行系统调用了。当通过访问futex变量告诉进程有竞争发生,则还是得执行系统调用去完成相应的处理(wait 或者 wake up)。 8.事件 (los_event.c) 基本概念 事件(Event)是一种任务间通信的机制,可用于任务间的同步。 多任务环境下,任务之间往往需要同步操作,一个等待即是一个同步。事件可以提供一对多、多对多的同步操作。 一对多同步模型:一个任务等待多个事件的触发。可以是任意一个事件发生时唤醒任务处理事件,也可以是几个事件都发生后才唤醒任务处理事件。 多对多同步模型:多个任务等待多个事件的触发。 事件特点 任务通过创建事件控制块来触发事件或等待事件。 事件间相互独立,内部实现为一个32位无符号整型,每一位标识一种事件类型。第25位不可用,因此最多可支持31种事件类型。 事件仅用于任务间的同步,不提供数据传输功能。 多次向事件控制块写入同一事件类型,在被清零前等效于只写入一次。 多个任务可以对同一事件进行读写操作。 支持事件读写超时机制。 事件可应用于多种任务同步场景,在某些同步场景下可替代信号量。 使用场景 队列用于任务间通信,可以实现消息的异步处理。同时消息的发送方和接收方不需要彼此联系,两者间是解耦的。 (事件驱动篇)|正在更新中... 9.文件消息队列 (hm_liteipc.c) 基于文件实现的消息队列,特点是队列中消息数量多(256个),传递消息内容大(可达到1K) #define IPC_MSG_DATA_SZ_MAX 1024 //最大的消息内容 1K ,posix最大消息内容 64个字节 #define IPC_MSG_OBJECT_NUM_MAX 256 //最大的消息数量256 ,posix最大消息数量 16个 (消息队列篇)|正在更新中... 以上为鸿蒙内核的九种进程间通讯方式,源码注解已基本完成,可前往Fork查看. 每一种通讯方式博客将详细说明,正在更新中... 喜欢就请收藏吧 各大站点搜 "鸿蒙内核源码分析" ,快速找到组织. 鸿蒙内核源码中文注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆, 四大源码仓每日同步更新< Gitee | Github | CSDN | Coding > 鸿蒙内核源码分析博客 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点每日同步更新< OSCHINA | CSDN | WeHarmony >

优秀的个人博客,低调大师

鸿蒙内核源码分析(自旋锁篇) | 汇编到令人心碎的自旋锁 | 中文注解HarmonyOS源码 | v26.01

鸿蒙内核源码中文注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆, 四大源码仓每日同步更新< Gitee| Github| CSDN| Coding > 鸿蒙内核源码分析博客 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点每日同步更新< CSDN| 开源中国| WeHarmony > 本篇说清楚自旋锁 读本篇之前建议先读鸿蒙内核源码分析(总目录)进程+线程篇. 概述 自旋锁顾名思义,是一把自动旋转的锁,这很像厕所里锁,进入前标记是绿色可用的,进入格子间后,手一带,里面的锁转个圈,外面标记变成了红色表示在使用,外面的只能等待.这是形象的比喻,但实际也是如此. 在多CPU核环境中,由于使用相同的内存空间,存在对同一资源进行访问的情况,所以需要互斥访问机制来保证同一时刻只有一个核进行操作,自旋锁就是这样的一种机制。 自旋锁是指当一个线程在获取锁时,如果锁已经被其它线程获取,那么该线程将循环等待,并不断判断是否能够成功获取锁,直到获取到锁才会退出循环。因此建议保护耗时较短的操作,防止对系统整体性能有显的影响。 自旋锁与互斥锁比较类似,它们都是为了解决对共享资源的互斥使用问题。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个持有者。但是两者在调度机制上略有不同,对于互斥锁,如果锁已经被占用,锁申请者会被阻塞;但是自旋锁不会引起调用者阻塞,会一直循环检测自旋锁是否已经被释放。 虽然都是共享资源竞争,但自旋锁强调的是CPU核间的竞争,而互斥量强调的是任务(包括同一CPU核)之间的竞争. 自旋锁使用流程 自旋锁用于多CPU核的情况,解决的是CPU之间竞争资源的问题.使用流程很简单,三步走。 创建自旋锁:使用LOS_SpinInit初始化自旋锁,或者使用SPIN_LOCK_INIT初始化静态内存的自旋锁。 申请自旋锁:使用接口LOS_SpinLock/LOS_SpinTrylock/LOS_SpinLockSave申请指定的自旋锁,申请成功就继续往后执行锁保护的代码;申请失败在自旋锁申请中忙等,直到申请到自旋锁为止。 释放自旋锁:使用LOS_SpinUnlock/LOS_SpinUnlockRestore接口释放自旋锁。锁保护代码执行完毕后,释放对应的自旋锁,以便其他核申请自旋锁。 自旋锁长什么样? typedef struct Spinlock {//自旋锁结构体 size_t rawLock;//记录次数 #if (LOSCFG_KERNEL_SMP_LOCKDEP == YES) // 死锁检测模块开关 UINT32 cpuid; //持有锁的CPU VOID *owner; //持有锁任务 const CHAR *name; //锁名称 #endif } SPIN_LOCK_S; 结构体很简单,里面有个宏,用于死锁检测,默认情况下是关闭的.所以真正的被使用的变量只有rawLock一个.但C语言代码中找不到变量的变化过程,而是通过一段汇编代码来实现.看完本篇会明白也只能通过汇编代码来实现自旋锁. 几个关键C函数 #if (LOSCFG_KERNEL_SMP == YES) //多CPU核情况下讨论自旋锁才有意义 //申请指定的自旋锁,如果无法获取锁,会一直循环等待 LITE_OS_SEC_ALW_INLINE STATIC INLINE VOID LOS_SpinLock(SPIN_LOCK_S *lock) { LOS_TaskLock();//1.先告诉CPU记录有个任务上了锁 LOCKDEP_CHECK_IN(lock);//2.检查自旋锁 ArchSpinLock(&lock->rawLock);//3.自旋锁工作主体,一段汇编代码 LOCKDEP_RECORD(lock);//4.记录自旋锁 } //尝试申请指定的自旋锁,如果无法获取锁,直接返回失败,而不会一直循环等待 LITE_OS_SEC_ALW_INLINE STATIC INLINE INT32 LOS_SpinTrylock(SPIN_LOCK_S *lock) { LOS_TaskLock(); INT32 ret = ArchSpinTrylock(&lock->rawLock); if (ret == LOS_OK) { LOCKDEP_CHECK_IN(lock); LOCKDEP_RECORD(lock); } else { LOS_TaskUnlock(); } return ret; } //释放指定的自旋锁 LITE_OS_SEC_ALW_INLINE STATIC INLINE VOID LOS_SpinUnlock(SPIN_LOCK_S *lock) { LOCKDEP_CHECK_OUT(lock); ArchSpinUnlock(&lock->rawLock);//注意ArchSpinUnlock是一个汇编函数 见于 los_dispatch.s /* restore the scheduler flag */ //恢复调度标签 LOS_TaskUnlock(); } #endif 代码也是很简单,真正涉及自旋锁的部分就是这三个函数. ArchSpinLock(&lock->rawLock); ArchSpinTrylock(&lock->rawLock) ArchSpinUnlock(&lock->rawLock); 可以说掌握了这三个函数就掌握了自旋锁,但这三个函数全由汇编实现.见于los_dispatch.S文件 因为系列篇已有两篇讲过汇编代码,所以很容易理解这三段代码.函数的参数此时由 r0记录,即r0保存了lock->rawLock的地址. 下面逐一说明自旋锁的汇编代码. ArchSpinLock 汇编代码 FUNCTION(ArchSpinLock) @对变量上锁 mov r1, #1 @r1=1 1: @循环的作用,因SEV是广播事件.不一定lock->rawLock的值已经改变了 ldrex r2, [r0] @r0 = &lock->rawLock, 即 r2 = lock->rawLock cmp r2, #0 @r2和0比较 wfene @不相等时,说明资源被占用,CPU核进入睡眠状态 strexeq r2, r1, [r0]@此时CPU被重新唤醒,尝试令lock->rawLock=1,成功写入则r2=0 cmpeq r2, #0 @再来比较r2是否等于0,如果相等则获取到了锁 bne 1b @如果不相等,继续进入循环 dmb @用DMB指令来隔离,以保证缓冲中的数据已经落实到RAM中 bx lr @此时是一定拿到锁了,跳回调用ArchSpinLock函数 看懂了这段汇编代码就理解了自旋锁实现的真正机制,为什么一定要用汇编来实现. 因为CPU宁愿睡眠也非拿要到锁不可的, 注意这里可不是让线程睡眠,而是让CPU进入睡眠状态,能让CPU进入睡眠的只能通过汇编实现.C语言根本就写不出让CPU真正睡眠的代码. ArchSpinTrylock 汇编代码 如果不看下面这段汇编代码,你根本不可能知道 ArchSpinTrylock 和 ArchSpinLock的真正区别是什么. FUNCTION(ArchSpinTrylock) @对变量尝试上锁 mov r1, #1 @r1=1 mov r2, r0 @r2 = r0 ldrex r0, [r2] @r2 = &lock->rawLock, 即 r0 = lock->rawLock cmp r0, #0 @r0和0比较 strexeq r0, r1, [r2] @尝试令lock->rawLock=1,成功写入则r0=0,否则 r0 =1 dmb @数据存储隔离,以保证缓冲中的数据已经落实到RAM中 bx lr @跳回调用ArchSpinTrylock函数 比较两段汇编代码可知,ArchSpinTrylock即没有循环也不会让CPU进入睡眠,直接返回了,而ArchSpinLock会睡了醒, 醒了睡,一直守到丈夫( lock->rawLock = 0的广播事件发生)回来才肯罢休. 笔者注释到这段代码时,那真是心潮澎湃, 心碎了老一地, 真想给 ArchSpinLock 立一个贞节牌坊 ! ArchSpinUnlock 汇编代码 FUNCTION(ArchSpinUnlock) @释放锁 mov r1, #0 @r1=0 dmb @数据存储隔离,以保证缓冲中的数据已经落实到RAM中 str r1, [r0] @令lock->rawLock = 0 dsb @数据同步隔离 sev @给各CPU广播事件,唤醒沉睡的CPU们 bx lr @跳回调用ArchSpinUnlock函数 } 代码中涉及到几个不常用的汇编指令,一一说明: 汇编指令之 WFI / WFE / SEV WFI(Wait for interrupt):等待中断到来指令. WFI一般用于cpuidle,WFI 指令是在处理器发生中断或类似异常之前不需要做任何事情。 在鸿蒙源码分析系列篇(总目录)线程篇中已说过,每个CPU都有自己的idle任务,CPU没事干的时候就待在里面,就一个死循环守着WFI指令,有中断来了就触发CPU起床干活. 中断分硬中断和软中断,系统调用就是通过软中断实现的,而设备类的就属于硬中断,都能触发CPU干活. 具体看下CPU空闲的时候在干嘛,代码超级简单: LITE_OS_SEC_TEXT WEAK VOID OsIdleTask(VOID) //CPU没事干的时候待在这里 { while (1) {//只有一个死循环 Wfi();//WFI指令:arm core 立即进入low-power standby state,等待中断,进入休眠模式。 } } WFE(Wait for event):等待事件的到来指令.WFE 指令是在SEV指令生成事件之前不需要执行任何操作,所以用WFE的地方,后续一定会对应一个SEV的指令去唤醒它. WFE的一个典型使用场景,是用在自旋锁中,spinlock的功能,是在不同CPU core之间,保护共享资源。使用WFE的流程是: 1.开始之初资源空闲 2.CPU核1 访问资源,持有锁,获得资源 3.CPU核2 访问资源,此时资源不空闲,执行WFE指令,让core进入low-power state(睡眠) 4.CPU核1 释放资源,释放锁,释放资源,同时执行SEV指令,唤醒CPU核2 5.CPU核2 获得资源 另外说一下 以往的自旋锁,在获得不到资源时,让CPU核进入死循环,而通过插入WFE指令,则大大节省功耗. SEV(send event):发送事件指令,SEV是一条广播指令,它会将事件发送到多处理器系统中的所有处理器,以唤醒沉睡的CPU. SEV和WFE的实现很像设计模式的观察者模式. 汇编指令之 LDREX / STREX LDREX用来读取内存中的值,并标记对该段内存的独占访问: LDREX Rx, [Ry] 上面的指令意味着,读取寄存器Ry指向的4字节内存值,将其保存到Rx寄存器中,同时标记对Ry指向内存区域的独占访问。 如果执行LDREX指令的时候发现已经被标记为独占访问了,并不会对指令的执行产生影响。 而STREX在更新内存数值时,会检查该段内存是否已经被标记为独占访问,并以此来决定是否更新内存中的值: STREX Rx, Ry, [Rz] 如果执行这条指令的时候发现已经被标记为独占访问了,则将寄存器Ry中的值更新到寄存器Rz指向的内存,并将寄存器Rx设置成0。指令执行成功后,会将独占访问标记位清除。 而如果执行这条指令的时候发现没有设置独占标记,则不会更新内存,且将寄存器Rx的值设置成1。 一旦某条STREX指令执行成功后,以后再对同一段内存尝试使用STREX指令更新的时候,会发现独占标记已经被清空了,就不能再更新了,从而实现独占访问的机制。 总结 自旋锁用于解决CPU核间竞争资源的问题 因为自旋锁会让CPU陷入睡眠状态,所以锁的代码不能太长,否则容易导致意外出现,也影响性能. 必须由汇编代码实现,因为C语言写不出让CPU进入真正睡眠,核间竞争的代码. 喜欢就请收藏吧 各大站点搜 "鸿蒙内核源码分析" ,快速找到组织. 鸿蒙内核源码中文注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆, 四大源码仓每日同步更新< Gitee| Github| CSDN| Coding > 鸿蒙内核源码分析博客 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点每日同步更新< CSDN| 开源中国| WeHarmony >

资源下载

更多资源
优质分享App

优质分享App

近一个月的开发和优化,本站点的第一个app全新上线。该app采用极致压缩,本体才4.36MB。系统里面做了大量数据访问、缓存优化。方便用户在手机上查看文章。后续会推出HarmonyOS的适配版本。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。