0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

鸿蒙内核分析:线程中断环境下的任务切换

鸿蒙系统HarmonyOS 来源:oschina 作者:oschina 2021-03-19 14:34 次阅读

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

中断切换篇用自下而上的方式,从汇编开始处往上跟踪.说清楚保存和恢复TaskIrqContext,以及调用HalIrqHandler的入口,此为中断切换篇.

中断管理篇用自上而下的方式,从C语言中断注册管理开始往下跟踪,一直到汇编调用的4个HalIrqHandlerOsTaskProcSignalOsSchedPreemptOsSaveSignalContextIrqC函数为止.

中断环境下的任务切换

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

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

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

由硬件产生的中断,比如 鼠标,键盘外部设备每次点击和敲打,屏幕的触摸,USB的插拔等等这些都是硬中断.同样的需要切换栈运行,需要复用寄存器,但与软切换不一样的是,硬切换会切换工作模式(中断模式).所以会更复杂点,但道理还是一样要保存和恢复切换现场寄存器的值, 而保存寄存器顺序的结构体叫:任务中断上下文(TaskIrqContext).

在线程环境下,从当前线程切换到目标线程,这种方式也称为软切换,能由软件控制的自主式切换.哪些情况下会出现软切换呢?

运行的线程申请某种资源(比如各种锁,读/写消息队列)失败时,需要主动释放CPU的控制权,将自己挂入等待队列,调度算法重新调度新任务运行.

每隔10ms就执行一次的OsTickHandler节拍处理函数,检测到任务的时间片用完了,就发起任务的重新调度,切换到新任务运行.

不管是内核态的任务还是用户态的任务,于切换而言是统一处理,一视同仁的,因为切换是需要换栈运行,寄存器有限,需要频繁的复用,这就需要将当前寄存器值先保存到任务自己的栈中,以便别人用完了轮到自己再用时恢复寄存器当时的值,确保老任务还能继续跑下去. 而保存寄存器顺序的结构体叫:任务上下文(TaskContext).

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

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

普通中断模式(irq):一般中断模式也叫普通中断模式,用于处理一般的中断请求,通常在硬件产生中断信号之后自动进入该模式,该模式可以自由访问系统硬件资源。

快速中断模式(fiq):快速中断模式是相对一般中断模式而言的,用来处理高优先级中断的模式,处理对时间要求比较紧急的中断请求,主要用于高速数据传输及通道处理中。

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

普通中断模式相关寄存器

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

pIYBAGBURP6AJ00tAAKBis0FJdQ741.png

普通中断模式(图中IRQ列)是一种异常模式,有自己独立运行的栈空间.一个(IRQ)中断发生后,硬件会将CPSR寄存器工作模式位置为IRQ模式.并跳转到入口地址OsIrqHandler执行.

#define OS_EXC_IRQ_STACK_SIZE 64 //64个字节, 16个栈空间

__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;          
    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语言去执行,分别是HalIrqHandlerOsTaskProcSignalOsSchedPreemptOsSaveSignalContextIrqC语言部分内容很多,将在中断管理篇中说明.

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

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

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

编辑:hfy

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • ARM
    ARM
    +关注

    关注

    134

    文章

    9097

    浏览量

    367648
  • 寄存器
    +关注

    关注

    31

    文章

    5343

    浏览量

    120401
  • SVC
    SVC
    +关注

    关注

    0

    文章

    33

    浏览量

    12141
  • 线程
    +关注

    关注

    0

    文章

    505

    浏览量

    19690
  • 鸿蒙系统
    +关注

    关注

    183

    文章

    2634

    浏览量

    66362
收藏 人收藏

    评论

    相关推荐

    鸿蒙内核源码Task/线程技术分析

    前言 在鸿蒙内核中,广义上可理解为一个Task就是一个线程 一、怎么理解Task 1. 官方文档是怎么描述线程 基本概念 从系统的角度看,线程
    的头像 发表于 10-18 10:42 2219次阅读
    <b class='flag-5'>鸿蒙</b><b class='flag-5'>内核</b>源码Task/<b class='flag-5'>线程</b>技术<b class='flag-5'>分析</b>

    鸿蒙内核源码的中断环境任务切换

    中断环境任务切换鸿蒙内核
    的头像 发表于 04-30 16:41 2308次阅读
    <b class='flag-5'>鸿蒙</b><b class='flag-5'>内核</b>源码的<b class='flag-5'>中断</b><b class='flag-5'>环境</b><b class='flag-5'>下</b>的<b class='flag-5'>任务</b><b class='flag-5'>切换</b>

    用户级线程内核线程

    线程:不需要内核支持而在用户程序中实现的线程,其不依赖于操作系统核心,应用进程利用线程库提供创建、同步、调度和管理线程的函数来控制用户
    发表于 01-10 15:01

    【HarmonyOS】鸿蒙内核源码分析(调度机制篇)

    任务上下文}函数有点长,笔者留了最重要的几行,看这几行就够了,流程如下: 调度过程要自旋锁,不允许任何中断发生,没错,说的是任何事是不能去打断它,否则后果太严重了,这可是内核切换
    发表于 10-14 14:00

    鸿蒙内核源码分析(调度机制篇):Task是如何被调度执行的

    (); 就是设置启动任务,但此时啥都还没开始呢,Kprocess 进程都没创建,怎么会有大家一般意义上所理解的线程呢。狭义上的后续有 鸿蒙内核源码
    发表于 11-23 10:53

    鸿蒙内核源码分析(Task管理篇):task是内核调度的单元

    有状态,要运行就需要内存空间,就需要被内核算法调度,被选中CPU就去执行代码段指令,CPU要执行就需要告诉它从哪里开始执行,因为是多线程,但只有一个CPU就需要不断的切换任务,那执行会
    发表于 11-23 14:01

    鸿蒙内核源码分析(Task管理篇):task是内核调度的单元

    还原回去就能保证task的连续执行,让用户毫无感知。鸿蒙内核给一个任务执行的时间是 20ms ,也就是说有多任务竞争的情况,一秒钟内最多要
    发表于 11-24 10:24

    RTThread内核线程是如何切换

    1、背景本文章主要说明 rtthread 内核线程是如何切换的,初学者刚从裸机开发接触 RTOS 时难免会有些不适应,明白这部分原理之后就会对 RTOS 有更深的理解。在学习内核
    发表于 10-10 16:50

    请问线程间的切换会不会影响外设的中断响应?

    例如,有一个编码器使用io口中断的方式读取状态然后在程序中有多个线程来回切换,那线程切换时会不会将编码器触发的io口
    发表于 03-23 11:38

    鸿蒙内核源码之线程环境任务切换

    中断环境,从当前线程切换到目标线程,这种方式也称为硬切换
    的头像 发表于 04-25 16:48 1468次阅读
    <b class='flag-5'>鸿蒙</b><b class='flag-5'>内核</b>源码之<b class='flag-5'>线程</b><b class='flag-5'>环境</b><b class='flag-5'>下</b>的<b class='flag-5'>任务</b><b class='flag-5'>切换</b>

    鸿蒙内核源码分析线程环境任务切换

    从代码段哪个位置取指令? 也就是入口地址,main函数是应用程序的入口地址, run()是new一个线程执行的入口地址.高级语言是这么叫,但到了汇编层的叫法就是PC寄存器.给PC寄存器喂妹值,指令就从哪里取.
    的头像 发表于 04-25 15:22 1446次阅读
    <b class='flag-5'>鸿蒙</b><b class='flag-5'>内核</b>源码<b class='flag-5'>分析</b>之<b class='flag-5'>线程</b><b class='flag-5'>环境</b><b class='flag-5'>下</b>的<b class='flag-5'>任务</b><b class='flag-5'>切换</b>

    鸿蒙内核源码分析:引起中断的事件或原因

    通过中断机制,在外设不需要CPU介入时,CPU可以执行其它任务,而当外设需要CPU时,将通过产生中断信号使CPU立即中断当前任务来响应
    的头像 发表于 04-25 09:25 1892次阅读
    <b class='flag-5'>鸿蒙</b><b class='flag-5'>内核</b>源码<b class='flag-5'>分析</b>:引起<b class='flag-5'>中断</b>的事件或原因

    鸿蒙内核源码分析任务环境的事件控制块

    一对多同步模型:一个任务等待多个事件的触发。可以是任意一个事件发生时唤醒任务处理事件,也可以是几个事件都发生后才唤醒任务处理事件。
    的头像 发表于 04-26 14:29 1353次阅读
    <b class='flag-5'>鸿蒙</b><b class='flag-5'>内核</b>源码<b class='flag-5'>分析</b>多<b class='flag-5'>任务</b><b class='flag-5'>环境</b><b class='flag-5'>下</b>的事件控制块

    鸿蒙内核源码分析:task是内核调度的单元

    从系统的角度看,线程是竞争系统资源的最小运行单元。线程可以使用或等待CPU、使用内存空间等系统资源,并独立于其它线程运行。 鸿蒙内核每个进
    发表于 11-23 15:51 22次下载
    <b class='flag-5'>鸿蒙</b><b class='flag-5'>内核</b>源码<b class='flag-5'>分析</b>:task是<b class='flag-5'>内核</b>调度的单元

    鸿蒙内核源码分析:进程和Task的就绪队列对调度的作用

    鸿蒙内核代码中有两个源文件是关于队列的,一个是用于调度的队列,另一个是用于线程间通讯的IPC队列。 鸿蒙内核进程和
    发表于 11-23 15:48 31次下载
    <b class='flag-5'>鸿蒙</b><b class='flag-5'>内核</b>源码<b class='flag-5'>分析</b>:进程和Task的就绪队列对调度的作用