v42.04 鸿蒙内核源码分析(中断切换篇) | 系统因中断活力四射 | 百篇博客分析OpenHarmony源码

原创
2021/03/18 19:48
阅读数 9.3K

子曰:“知者不惑,仁者不忧,勇者不惧。” 《论语》:子罕篇

在这里插入图片描述

百篇博客系列篇.本篇为: v42.xx 鸿蒙内核源码分析(中断切换篇) | 系统因中断活力四射

硬件架构相关篇为:

关于中断部分系列篇将用三篇详细说明整个过程.

  • 中断概念篇 中断概念很多,比如中断控制器,中断源,中断向量,中断共享,中断处理程序等等.本篇做一次整理.先了解透概念才好理解中断过程.用海公公打比方说明白中断各个概念.可前往鸿蒙内核源码分析(总目录)查看.

  • 中断管理篇 从中断初始化HalIrqInit开始,到注册中断的LOS_HwiCreate函数,到消费中断函数的 HalIrqHandler,剖析鸿蒙内核实现中断的过程,很像设计模式中的观察者模式. 可前往鸿蒙内核源码分析(总目录)查看.

  • 中断切换篇(本篇) 用自下而上的方式,从中断源头纯汇编代码往上跟踪代码细节.说清楚保存和恢复中断现场TaskIrqContext过程.

中断环境下的任务切换

在鸿蒙的内核线程就是任务,系列篇中说的任务和线程当一个东西去理解.

一般二种场景下需要切换任务上下文:

  • 在中断环境下,从当前线程切换到目标线程,这种方式也称为硬切换.它们通常由硬件产生或是软件发生异常时的被动式切换.哪些情况下会出现硬切换呢?

    • 中断源可分外部和内部中断源两大类,比如 鼠标,键盘外部设备每次点击和敲打,屏幕的触摸,USB的插拔等等这些都是外部中断源.存储器越限、缺页,核间中断,断点中断等等属于内部中断源.由此产生的硬切换都需要换栈运行,硬切换硬在需切换工作模式(中断模式).所以会比线程环境下的切换更复杂点,但道理还是一样要保存和恢复切换现场寄存器的值, 而保存寄存器顺序格式结构体叫:任务中断上下文(TaskIrqContext).
  • 在线程环境下,从当前线程切换到目标线程,这种方式也称为软切换,能由软件控制的自主式切换.哪些情况下会出现软切换呢?

    • 运行的线程申请某种资源(比如各种锁,读/写消息队列)失败时,需要主动释放CPU的控制权,将自己挂入等待队列,调度算法重新调度新任务运行.
    • 每隔10ms就执行一次的OsTickHandler节拍处理函数,检测到任务的时间片用完了,就发起任务的重新调度,切换到新任务运行.
    • 不管是内核态的任务还是用户态的任务,于切换而言是统一处理,一视同仁的,因为切换是需要换栈运行,寄存器有限,需要频繁的复用,这就需要将当前寄存器值先保存到任务自己的栈中,以便别人用完了轮到自己再用时恢复寄存器当时的值,确保老任务还能继续跑下去. 而保存寄存器顺序格式结构体叫:任务上下文(TaskContext).

本篇说清楚在中断环境下切换(硬切换)的实现过程.线程切换(软切换)实现过程已在鸿蒙内核源码分析(总目录)任务切换篇中详细说明.

ARM的七种工作模式中,有两个是和中断相关.

  • 普通中断模式(irq):一般中断模式也叫普通中断模式,用于处理一般的中断请求,通常在硬件产生中断信号之后自动进入该模式,该模式可以自由访问系统硬件资源。
  • 快速中断模式(fiq):快速中断模式是相对一般中断模式而言的,用来处理高优先级中断的模式,处理对时间要求比较紧急的中断请求,主要用于高速数据传输及通道处理中。

此处分析普通中断模式下的任务切换过程.

普通中断模式相关寄存器

这张图一定要刻在脑海里,系列篇会多次拿出来,目的是为了能牢记它. 在这里插入图片描述

  • 普通中断模式(图中IRQ列)是一种异常模式,有自己独立运行的栈空间.一个(IRQ)中断发生后,硬件会将CPSR寄存器工作模式置为IRQ模式.并跳转到入口地址OsIrqHandler执行.
#define OS_EXC_IRQ_STACK_SIZE    64 //中断模式栈大小 64个字节
__irq_stack:
    .space OS_EXC_IRQ_STACK_SIZE * CORE_NUM
__irq_stack_top:
  • OsIrqHandler汇编代码实现过程,就干了三件事:
    • 1.保存任务中断上下文TaskIrqContext
    • 2.执行中断处理程序HalIrqHandler,这是个C函数,由汇编调用
    • 3.恢复任务中断上下文TaskIrqContext,返回被中断的任务继续执行

TaskIrqContext 和 TaskContext

先看本篇结构体 TaskIrqContext

#define TASK_IRQ_CONTEXT \
        unsigned int R0;     \
        unsigned int R1;     \
        unsigned int R2;     \
        unsigned int R3;     \
        unsigned int R12;    \
        unsigned int USP;    \
        unsigned int ULR;    \
        unsigned int CPSR;   \
        unsigned int PC;

typedef struct {//任务中断上下文
#if !defined(LOSCFG_ARCH_FPU_DISABLE)
    UINT64 D[FP_REGS_NUM]; /* D0-D31 */
    UINT32 regFPSCR;       /* FPSCR */
    UINT32 regFPEXC;       /* FPEXC */
#endif
    UINT32 resved;
    TASK_IRQ_CONTEXT
} TaskIrqContext;
typedef struct {//任务上下文,已在任务切换篇中详细说明,放在此处是为了对比  
#if !defined(LOSCFG_ARCH_FPU_DISABLE)
    UINT64 D[FP_REGS_NUM]; /* D0-D31 */
    UINT32 regFPSCR;       /* FPSCR */
    UINT32 regFPEXC;       /* FPEXC */
#endif
    UINT32 resved;          /* It's stack 8 aligned */
    UINT32 regPSR;          //保存CPSR寄存器
    UINT32 R[GEN_REGS_NUM]; /* R0-R12 */
    UINT32 SP;              /* R13 */
    UINT32 LR;              /* R14 */
    UINT32 PC;              /* R15 */
} TaskContext;
  • 两个结构体很简单,目的更简单,就是用来保存寄存器现场的值的. TaskContext把17个寄存器全部保存了,TaskIrqContext保存的少些,在栈中并没有保存R4-R11寄存器的值,这说明在整个中断处理过程中,都不会用到R4-R11寄存器.不会用到就不会改变,当然就没必要保存了.这也说明内核开发者的严谨程度,不造成时间和空间上的一丁点浪费.效率的提升是从细节处入手的,每个小地方优化那么一丢丢,整体性能就上来了.
  • TaskIrqContext中有两个变量有点奇怪 unsigned int USP; unsigned int ULR; 指的是用户模式下的SP和LR值, 这个要怎么理解? 因为对一个正运行的任务而言,中断的到来是颗不定时炸弹,无法预知,也无法提前准备,中断一来它立即被打断,压根没有时间去保存现场到自己的栈中,那保存工作只能是放在IRQ栈或者SVC栈中.而IRQ栈非常的小,只有64个字节,16个栈空间,指望不上了,就保存在SVC栈中,SVC模式栈可是有 8K空间的.
  • 从接下来的 OsIrqHandler代码中可以看出,鸿蒙内核整个中断的工作其实都是在SVC模式下完成的,而irq的栈只是个过渡栈.具体看汇编代码逐行注解分析.

普通中断处理程序

OsIrqHandler:	@硬中断处理,此时已切换到硬中断栈
    SUB     LR, LR, #4	@记录译码指令地址,以防切换过程丢失指令

    /* push r0-r3 to irq stack */ @irq栈只是个过渡栈
    STMFD   SP, {R0-R3}		@r0-r3寄存器入 irq 栈
    SUB     R0, SP, #(4 * 4)@r0 = sp - 16,目的是记录{R0-R3}4个寄存器保存的开始位置,届时从R3开始出栈
    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
    @TaskIrqContext 开始保存中断现场 ......							
    /* push spsr and pc in svc stack */
    STMFD   SP!, {R1, R2} @实际是将 SPSR,和PC入栈对应TaskIrqContext.PC,TaskIrqContext.CPSR,
    STMFD   SP, {LR}	  @LR再入栈,SP不自增,如果是用户模式,LR值将被 282行:STMFD   SP, {R13, R14}^覆盖  
						  @如果非用户模式,将被 286行:SUB     SP, SP, #(2 * 4) 跳过.
    AND     R3, R1, #CPSR_MASK_MODE	@获取CPU的运行模式
    CMP     R3, #CPSR_USER_MODE		@中断是否发生在用户模式
    BNE     OsIrqFromKernel			@非用户模式不用将USP,ULR保存在TaskIrqContext

    /* push user sp, lr in svc stack */
    STMFD   SP, {R13, R14}^ 		@将用户模式的sp和LR入svc栈

OsIrqFromKernel:	@从内核发起中断
    /* from svc not need save sp and lr */@svc模式下发生的中断不需要保存sp和lr寄存器值
    SUB     SP, SP, #(2 * 4)	@目的是为了留白给 TaskIrqContext.USP,TaskIrqContext.ULR
								@TaskIrqContext.ULR已经在 276行保存了,276行用的是SP而不是SP!,所以此处要跳2个空间
    /* pop r0-r3 from irq stack*/
    LDMFD   R0, {R0-R3}		    @从R0位置依次出栈 

    /* push caller saved regs as trashed regs in svc stack */
    STMFD   SP!, {R0-R3, R12}	@寄存器入栈,对应 TaskIrqContext.R0~R3,R12

    /* 8 bytes stack align */
    SUB     SP, SP, #4			@栈对齐 对应TaskIrqContext.resved

    /*
     * save fpu regs in case in case those been
     * altered in interrupt handlers.
     */
    PUSH_FPU_REGS   R0 @保存fpu regs,以防中断处理程序中的fpu regs被修改。
    @TaskIrqContext 结束保存中断现场......	
    @开始执行真正的中断处理函数了.
#ifdef LOSCFG_IRQ_USE_STANDALONE_STACK @是否使用了独立的IRQ栈
    PUSH    {R4}	@R4先入栈保存,接下来要切换栈,需保存现场
    MOV     R4, SP	@R4=SP
    EXC_SP_SET __svc_stack_top, OS_EXC_SVC_STACK_SIZE, R1, R2 @切换到svc栈
#endif
	/*BLX 带链接和状态切换的跳转*/
    BLX     HalIrqHandler /* 调用硬中断处理程序,无参 ,说明HalIrqHandler在svc栈中执行 */

#ifdef LOSCFG_IRQ_USE_STANDALONE_STACK @是否使用了独立的IRQ栈
    MOV     SP, R4	@恢复现场,sp = R4 
    POP     {R4}	@弹出R4
#endif

    /* process pending signals */ 	@处理挂起信号
    BL      OsTaskProcSignal 		@跳转至C代码 

    /* check if needs to schedule */@检查是否需要调度
    CMP     R0, #0	@是否需要调度,R0为参数保存值
    BLNE    OsSchedPreempt @不相等,即R0非0,一般是 1

    MOV     R0,SP	@参数
    MOV     R1,R7	@参数
    BL      OsSaveSignalContextIrq @跳转至C代码 

    /* restore fpu regs */
    POP_FPU_REGS    R0 @恢复fpu寄存器值

    ADD     SP, SP, #4 @sp = sp + 4 

OsIrqContextRestore:	@恢复硬中断环境
    LDR     R0, [SP, #(4 * 7)]	@R0 = sp + 7,目的是跳到恢复中断现场TaskIrqContext.CPSR位置,刚好是TaskIrqContext倒数第7的位置.
    MSR     SPSR_cxsf, R0		@恢复spsr 即:spsr = TaskIrqContext.CPSR
    AND     R0, R0, #CPSR_MASK_MODE @掩码找出当前工作模式
    CMP     R0, #CPSR_USER_MODE	@是否为用户模式?
    @TaskIrqContext 开始恢复中断现场 ......	
    LDMFD   SP!, {R0-R3, R12}	@从SP位置依次出栈 对应 TaskIrqContext.R0~R3,R12
								@此时已经恢复了5个寄存器,接来下是TaskIrqContext.USP,TaskIrqContext.ULR
    BNE     OsIrqContextRestoreToKernel @看非用户模式,怎么恢复中断现场.

    /* load user sp and lr, and jump cpsr */
    LDMFD   SP, {R13, R14}^ @出栈,恢复用户模式sp和lr值 即:TaskIrqContext.USP,TaskIrqContext.ULR
    ADD     SP, SP, #(3 * 4) @跳3个位置,跳过 CPSR ,因为上一句不是 SP!,所以跳3个位置,刚好到了保存TaskIrqContext.PC的位置

    /* return to user mode */
    LDMFD   SP!, {PC}^ @回到用户模式,整个中断过程完成
    @TaskIrqContext 结束恢复中断现场(用户模式下) ......	

OsIrqContextRestoreToKernel:@从内核恢复中断
    /* svc mode not load sp */
    ADD     SP, SP, #4 @其实是跳过TaskIrqContext.USP,因为在内核模式下并没有保存这个值,翻看 287行
    LDMFD   SP!, {LR} @弹出LR
    /* jump cpsr and return to svc mode */
    ADD     SP, SP, #4 @跳过cpsr
    LDMFD   SP!, {PC}^ @回到svc模式,整个中断过程完成
    @TaskIrqContext 结束恢复中断现场(内核模式下) ......

逐句解读

  • 跳转到 OsIrqFromKernel硬件会自动切换到__irq_stack执行
  • 1句:SUB LR, LR, #4 在arm执行过程中一般分为取指,译码,执行阶段,而PC是指向取指,正在执行的指令为 PC-8 ,译码指令为PC-4.当中断发生时硬件自动执行 mov lr pc, 中间的PC-4译码指令因为没有寄存器去记录它,就会被丢失掉.所以SUB LR, LR, #4 的结果是lr = PC -4 ,定位到了被中断时译码指令,将在栈中保存这个位置,确保回来后能继续执行.
  • 2句:STMFD SP, {R0-R3} 当前4个寄存器入__irq_stack保存
  • 3句:SUB R0, SP, #(4 * 4) 因为SP没有自增,R0跳到保存R0内容地址
  • 4,5句:读取SPSR,LR寄存器内容,目的是为了后面在SVC栈中保存TaskIrqContext
  • 6句:CPSID i, #0x13禁止中断和切换SVC模式,执行完这条指令后工作模式将切到 SVC模式
  • @TaskIrqContext 开始保存中断现场 ......
  • 中间代码需配合TaskIrqContext来看,不然100%懵逼.结合看就秒懂,代码都已经注释,不再做解释,注解中提到的 翻看276行 是指源码的第276行,请对照注解源码看理解会更透彻. 进入源码注解地址查看
  • @TaskIrqContext 结束保存中断现场 ......
  • TaskIrqContext保存完现场后就真正的开始处理中断了.
	/*BLX 带链接和状态切换的跳转*/
    BLX     HalIrqHandler /* 调用硬中断处理程序,无参 ,说明HalIrqHandler在svc栈中执行 */
#ifdef LOSCFG_IRQ_USE_STANDALONE_STACK @是否使用了独立的IRQ栈
    MOV     SP, R4	@恢复现场,sp = R4 
    POP     {R4}	@弹出R4
#endif
    /* process pending signals */ 	@处理挂起信号
    BL      OsTaskProcSignal 		@跳转至C代码 
    /* check if needs to schedule */@检查是否需要调度
    CMP     R0, #0	@是否需要调度,R0为参数保存值
    BLNE    OsSchedPreempt @不相等,即R0非0,一般是 1
    MOV     R0,SP	@参数
    MOV     R1,R7	@参数
    BL      OsSaveSignalContextIrq @跳转至C代码 
    /* restore fpu regs */
    POP_FPU_REGS    R0 @恢复fpu寄存器值
    ADD     SP, SP, #4 @sp = sp + 4 
  • 这段代码都是跳转到C语言去执行,分别是 HalIrqHandler OsTaskProcSignal OsSchedPreempt OsSaveSignalContextIrq C语言部分内容很多,将在中断管理篇中说明.

  • @TaskIrqContext 开始恢复中断现场 ......

  • 同样的中间代码需配合TaskIrqContext来看,不然100%懵逼.结合看就秒懂,代码都已经注释,不再做解释,注解中提到的 翻看287行 是指源码的第287行,请对照注解源码看理解会更透彻.进入源码注解地址查看

  • @TaskIrqContext 结束恢复中断现场 ......

百篇博客分析.深挖内核地基

  • 给鸿蒙内核源码加注释过程中,整理出以下文章。内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感,容易理解记忆。说别人能听得懂的话很重要! 百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思。更希望让内核变得栩栩如生,倍感亲切.确实有难度,自不量力,但已经出发,回头已是不可能的了。 😛
  • 与代码有bug需不断debug一样,文章和注解内容会存在不少错漏之处,请多包涵,但会反复修正,持续更新,v**.xx 代表文章序号和修改的次数,精雕细琢,言简意赅,力求打造精品内容。

按功能模块:

基础工具 加载运行 进程管理 编译构建
双向链表 位图管理 用栈方式 定时器 原子操作 时间管理 ELF格式 ELF解析 静态链接 重定位 进程映像 进程管理 进程概念 Fork 特殊进程 进程回收 信号生产 信号消费 Shell编辑 Shell解析 编译环境 编译过程 环境脚本 构建工具 gn应用 忍者ninja
进程通讯 内存管理 前因后果 任务管理
自旋锁 互斥锁 进程通讯 信号量 事件控制 消息队列 内存分配 内存管理 内存汇编 内存映射 内存规则 物理内存 总目录 调度故事 内存主奴 源码注释 源码结构 静态站点 时钟任务 任务调度 任务管理 调度队列 调度机制 线程概念 并发并行 CPU 系统调用 任务切换
文件系统 硬件架构
文件概念 文件系统 索引节点 挂载目录 根文件系统 字符设备 VFS 文件句柄 管道文件 汇编基础 汇编传参 工作模式 寄存器 异常接管 汇编汇总 中断切换 中断概念 中断管理

百万汉字注解.精读内核源码

WeHarmony/kernel_liteos_a_note

鸿蒙研究站 | 每天死磕一点点,原创不易,欢迎转载,但请注明出处。

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
打赏
0 评论
1 收藏
1
分享
返回顶部
顶部