Liteos-A任务调度之任务上下文切换 原创
任务调度要调度的就是任务所拥有的CPU资源,其中最主要的就是寄存器,包括通用寄存器和状态寄存器,操作系统刚开始启动时任务调度的代码主要是加载第一个任务,然后随着操作系统的运行需求开始运行不同的任务,即就开始了任务的切换,任务切换是任务调度的核心,它的主要工作就是保存当前任务的现场,加载新任务的现场并运行,而这部分的工作只能由汇编完成,los_dispatch.S就是承担Liteos-A任务切换的汇编源文件,它使用的是32位的ARM汇编。
一、加载任务现场
任务切换时应先保存任务现场,然后再加载任务现场,但是在liteos-a启动开始,还未进行任务切换时,系统需要有一个任务运行,这时就会调用加载任务现场接口——OsTaskContextLoad,这样在任务切换调用保存任务现场时逻辑就会连上,所以第一个先说明OsTaskContextLoad接口,另外,操作系统加载的第一个任务的现场就是这篇博客中初始化的现场:Liteos-A任务调度之OsTaskStackInit函数
加载任务现场是一个出栈操作,所以加载之前让我们先看看栈里的内容:
这就是初始化时栈中的情况,在加载新任务现场时就会从初始化好的结构体中加载到寄存器中。
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的值,为了方便理解,我分析一下下图:
一条指令的生命周期(此指在ARM流水线CPU中)有三个阶段,取指、译码、和执行,在同一时钟周期中,CPU不同的器件会处理不同的阶段,上图中,CPU的各器件处在指令pc-8的执行阶段、指令pc-4的译码阶段、指令pc的取指阶段,所以当任务被打断时,当前阶段的译码阶段,也就是PC-4这条指令还未被执行,这条指令会被保存在LR中,等任务被重新恢复的时候会重新取指并译码执行,这就是第一个LR的入栈。而第二个LR的入栈就是任务的LR寄存器入栈,无须多说。
后面的代码就是继续保存当前任务的寄存器,无需多说。
水平有限,如有任何想法,欢迎交流!