鸿蒙内核源码分析(中断切换篇) | 汇编注解中断切换实现全过程

开发
文章由鸿蒙社区产出,想要了解更多内容请前往:51CTO和华为官方战略合作共建的鸿蒙技术社区https://harmonyos.51cto.com

[[389420]]

想了解更多内容,请访问:

51CTO和华为官方合作共建的鸿蒙技术社区

https://harmonyos.51cto.com

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

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

● 中断管理篇 用自上而下的方式,从C语言中断注册管理开始往下跟踪代码细节,一直到汇编调用的4个HalIrqHandler OsTaskProcSignal OsSchedPreempt OsSaveSignalContextIrqC函数为止.可前往鸿蒙内核源码分析(总目录)查看.

● 中断切换篇(本篇) 用自下而上的方式,从汇编开始处往上跟踪代码细节.说清楚保存和恢复TaskIrqContext,以及调用HalIrqHandler的入口.

中断环境下的任务切换

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

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

● 在中断环境下,从当前线程切换到目标线程,这种方式也称为硬切换.不由软件控制的被动式切换.哪些情况下会出现硬切换呢?

◊ 由硬件产生的中断,比如 鼠标,键盘外部设备每次点击和敲打,屏幕的触摸,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: 
  • 1.
  • 2.
  • 3.
  • 4.

● 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; 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.

● 两个结构体很简单,目的更简单,就是用来保存寄存器现场的值的. 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 结束恢复中断现场(内核模式下) ...... 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.

逐句解读

● 跳转到 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  
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

● 这段代码都是跳转到C语言去执行,分别是 HalIrqHandler OsTaskProcSignal OsSchedPreempt OsSaveSignalContextIrq C语言部分内容很多,将在中断管理篇中说明.

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

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

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

参与贡献

访问注解仓库地址

Fork 本仓库 >> 新建 Feat_xxx 分支 >> 提交代码注解 >> 新建 Pull Request

● 新建 Issue

想了解更多内容,请访问:

51CTO和华为官方合作共建的鸿蒙技术社区

https://harmonyos.51cto.com

 

责任编辑:jianghua 来源: 鸿蒙社区
相关推荐

2021-03-30 15:30:44

鸿蒙HarmonyOS应用开发

2021-03-11 11:14:39

鸿蒙HarmonyOS应用

2021-04-01 09:38:02

鸿蒙HarmonyOS应用

2012-11-06 10:19:18

Java自定义加载Java类

2011-02-22 10:46:02

Samba配置

2021-03-11 16:07:40

鸿蒙HarmonyOS应用开发

2021-05-11 09:54:55

鸿蒙HarmonyOS应用

2009-12-08 17:56:16

WCF配置

2011-04-18 15:56:10

软件测试

2009-04-13 12:37:18

2011-01-21 17:51:52

2011-09-06 15:38:20

QT安装

2022-08-22 08:45:57

Kafka网络层源码实现

2010-06-17 13:10:09

Linux Grub修

2010-03-01 17:01:03

Python编程技巧

2010-03-10 13:24:45

Zend Debugg

2009-06-10 16:55:42

cygwin netb安装

2010-11-19 10:11:49

Oracle物化视图

2012-11-23 22:47:36

2011-03-11 10:39:02

YUM安装LAMP
点赞
收藏

51CTO技术栈公众号