Liteos-A任务调度之任务上下文切换 原创

落子摘星
发布于 2022-9-6 16:47
浏览
0收藏

       任务调度要调度的就是任务所拥有的CPU资源,其中最主要的就是寄存器,包括通用寄存器和状态寄存器,操作系统刚开始启动时任务调度的代码主要是加载第一个任务,然后随着操作系统的运行需求开始运行不同的任务,即就开始了任务的切换,任务切换是任务调度的核心,它的主要工作就是保存当前任务的现场,加载新任务的现场并运行,而这部分的工作只能由汇编完成,los_dispatch.S就是承担Liteos-A任务切换的汇编源文件,它使用的是32位的ARM汇编。

一、加载任务现场

       任务切换时应先保存任务现场,然后再加载任务现场,但是在liteos-a启动开始,还未进行任务切换时,系统需要有一个任务运行,这时就会调用加载任务现场接口——OsTaskContextLoad,这样在任务切换调用保存任务现场时逻辑就会连上,所以第一个先说明OsTaskContextLoad接口,另外,操作系统加载的第一个任务的现场就是这篇博客中初始化的现场:Liteos-A任务调度之OsTaskStackInit函数

       加载任务现场是一个出栈操作,所以加载之前让我们先看看栈里的内容:
Liteos-A任务调度之任务上下文切换-鸿蒙开发者社区
       这就是初始化时栈中的情况,在加载新任务现场时就会从初始化好的结构体中加载到寄存器中。

OsTaskContextLoad:
    /* clear the flag of ldrex */
    CLREX                            //清除独占位

    /* switch to new task's sp */   
    LDR     SP, [R0]                //加载新任务的栈指针地址到SP中,R0保存的是新任务的栈指针

    /* restore fpu registers */
    POP_FPU_REGS    R2             //浮点寄存器组出栈

    LDMFD   SP!, {R4-R11}          //R4~R11出栈
    LDR     R3, [SP, #(11 * 4)]    //将地址为当前SP+11*4中的内容加载的R3中,其实就是栈中的状态寄存器CPSR
    AND     R0, R3, #CPSR_MASK_MODE //从CPSR中获取CPU的工作模式
    CMP     R0, #CPSR_USER_MODE    //判断是否是用户模式
    BNE     OsKernelTaskLoad       //如果不是用户模式,跳转到OsKernelTaskLoad

    MVN     R2, #CPSR_INT_DISABLE  //如果是用户模式,需要开中断
    AND     R3, R3, R2             //将开中断的值写入R3
    STR     R3, [SP, #(11 * 4)]    //将R3的值保存到栈中CPSR的位置

#ifdef LOSCFG_KERNEL_SMP           //涉及到多核,暂不说明
    BL      OsSchedToUserReleaseLock
#endif

    /* jump sp, reserved */
    ADD     SP, SP, #(2 * 4)      //跳过reserved1和reserved2的出栈
    LDMFD   SP, {R13, R14}^       //R13、R14出栈,但是SP并不移动(因为有^)
    ADD     SP, SP, #(2 * 4)      //SP向栈底移动两个单位
    LDMFD   SP!, {R0-R3, R12, LR} //R0-R3, R12, LR出栈
    RFEIA   SP!                   //PC和CPSR同时出栈,加载新任务完成

OsKernelTaskLoad:
    ADD     SP, SP, #(4 * 4)      //跳过reserved1、reserved2、USP、ULR
    LDMFD   SP!, {R0-R3, R12, LR} //R0-R3, R12, LR出栈
    RFEIA   SP!                   //PC和CPSR同时出栈,加载新任务完成

       可以看到,加载新任务现场实际上就是liteos-a任务上下文结构体TaskContext的成员的出栈。
       有几个需要注意的问题

1.CLREX指令

       这个可以参考:CLREX指令
       <font color=blue>该指令的作用就是在独占访问结束时,清除cpu中本地处理器针对某块内存区域的独占访问标志(核中的某个状态寄存器),以防在未清除时的其他操作,对系统产生影响。对于是否同时清除全局的独占访问标志,需要在设计cpu时的架构师决定。

       这个描述我也不太深刻的理解,如果有大佬知到,欢迎指教。

2.关于浮点寄存器的出栈

       浮点寄存器的出栈是一个宏:

.macro POP_FPU_REGS reg1
#if !defined(LOSCFG_ARCH_FPU_DISABLE)
    VPOP    {D0-D15}
#if defined(LOSCFG_ARCH_FPU_VFP_D32)
    VPOP    {D16-D31}
#endif
    POP     {\reg1}
    VMSR    FPSCR, \reg1
    POP     {\reg1}
    VMSR    FPEXC, \reg1
#endif
.endm

       由代码可知,如果!defined(LOSCFG_ARCH_FPU_DISABLE),则出栈D0-D15,如果defined(LOSCFG_ARCH_FPU_VFP_D32),那么D16-D31也出栈,所以上面的堆栈图中的栈顶有可能是D0-D15,也可能是D0-D31。

3.关于CPU的模式

       ARM有七种工作模式,在用户和非用户模式下加载新任务现场的代码是不一样的,在用户模式下需要打开中断,因为用户模式的优先级较低,要允许被中断打断,如果被中断打断,则还需要保存当前的SP和LR,也就是任务上下文结构体TaskContext中的USP和ULR。
       关于ARM架构CPU的七种工作模式可见:ARM的七种工作模式介绍

二、任务上下文切换

完整的任务上下文切换代码如下,其中OsTaskContextLoad接口不再做注释:

/*
 * R0: new task
 * R1: run task
 * 保存正在运行的任务
 * OsTaskSchedule有两个参数,是从C环境传过来的,
 * typedef struct {
 *  LosTaskCB *runTask;
 *  LosTaskCB *newTask;
 * } LosTask;
 * 传到汇编环境,new task地址保存在R0中,run task地址保存在R1中
 */
OsTaskSchedule:
    MRS      R2, CPSR
    STMFD    SP!, {R2}  //CPSR入栈
    STMFD    SP!, {LR}  //返回地址入栈=PC-4
    STMFD    SP!, {LR}  //LR寄存器的值入栈
    STMFD    SP!, {R12} //R12入栈

    /* jump R0 - R3 USP, ULR reserved */
    SUB      SP, SP, #(8 * 4)  //跳过R0~R3、USP、ULR的入栈,但保留空间

    /* push R4 - R11*/
    STMFD    SP!, {R4-R11}  //R4~R11入栈

    /* save fpu registers */
    PUSH_FPU_REGS   R2  //浮点寄存器入栈

    /* store sp on running task */
    STR     SP, [R1]  //最后当前任务地址入栈

//加载新任务
OsTaskContextLoad:
    /* clear the flag of ldrex */
    CLREX

    /* switch to new task's sp */
    LDR     SP, [R0]

    /* restore fpu registers */
    POP_FPU_REGS    R2

    LDMFD   SP!, {R4-R11}
    LDR     R3, [SP, #(11 * 4)]
    AND     R0, R3, #CPSR_MASK_MODE
    CMP     R0, #CPSR_USER_MODE
    BNE     OsKernelTaskLoad

    MVN     R2, #CPSR_INT_DISABLE
    AND     R3, R3, R2
    STR     R3, [SP, #(11 * 4)]

#ifdef LOSCFG_KERNEL_SMP
    BL      OsSchedToUserReleaseLock
#endif

    /* jump sp, reserved */
    ADD     SP, SP, #(2 * 4)
    LDMFD   SP, {R13, R14}^
    ADD     SP, SP, #(2 * 4)
    LDMFD   SP!, {R0-R3, R12, LR}
    RFEIA   SP!

OsKernelTaskLoad:
    ADD     SP, SP, #(4 * 4)
    LDMFD   SP!, {R0-R3, R12, LR}
    RFEIA   SP!

       需要说明的一点是,保存任务现场的时候保存了两次LR的值,为了方便理解,我分析一下下图:
Liteos-A任务调度之任务上下文切换-鸿蒙开发者社区
       一条指令的生命周期(此指在ARM流水线CPU中)有三个阶段,取指、译码、和执行,在同一时钟周期中,CPU不同的器件会处理不同的阶段,上图中,CPU的各器件处在指令pc-8的执行阶段、指令pc-4的译码阶段、指令pc的取指阶段,所以当任务被打断时,当前阶段的译码阶段,也就是PC-4这条指令还未被执行,这条指令会被保存在LR中,等任务被重新恢复的时候会重新取指并译码执行,这就是第一个LR的入栈。而第二个LR的入栈就是任务的LR寄存器入栈,无须多说。

       后面的代码就是继续保存当前任务的寄存器,无需多说。

       水平有限,如有任何想法,欢迎交流!

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
2
收藏
回复
举报
回复
    相关推荐