UCOS中上下文切换的具体实现
写在前面
正在学习UCOSIII,在这里试着把上下文切换的具体实现写清楚,也算是一种学习方式吧,所以以下内容如有表述错误的地方请谅解!!
**这里给正在学习UCOSIII的同学推荐一本书《嵌入式实时操作系统UCOS-III》"北京航空航天大学出版社"的,是写UCOSIII的人的翻译本哦!
开始
我们知道UCOSIII在执行任务切换时需要将当前正在占用CPU的任务的现场(CPU的寄存器)保存到该任务的堆栈中,然后将要执行的任务的任务堆栈恢复到现场(把对应寄存器的值恢复),这个过程就是任务切换。
下面以Cortex-M3内核为例讲解上下文切换的具体实现。
这里说一下内核上的一些事情
寄存器
R0-R12通用寄存器组用于数据的存储、R13堆栈指针可以在主堆栈指针(MSP也就是中断堆栈指针)和进程堆栈指针(PSP也就是任务堆栈指针)、R14连接寄存器用于存储返回地址(当呼叫一个子程序时,由R14存储返回地址)、R15程序计数器用于存储当前程序地址、特殊功能寄存器包括程序状态寄存器组(PSRS)、中断屏蔽寄存器组(PRIMASK,FAULTMASK, BASEPRI)、控制寄存器(CONTROL),下图是特殊功能寄存器的功能。
PendSV异常
PendSV(可悬起的系统异常)
PendSV,它是可以像普通的中断一样被悬起。 OS 可以利用它“缓期执行” 一个异常——直到其它重要的任务完成后才执行动作。 悬起 PendSV 的方法是: 手工往 NVIC 的 PendSV 悬起寄存器中写 1。 悬起后, 如果优先级不够高,则将缓期等待执行。
PendSV 的典型使用场合是在上下文切换时(在不同任务之间切换)。
UCOS执行上下文切换的几个函数
1、任务级调度程序
void OSSched (void)
{
CPU_SR_ALLOC(); /*声明保存CPU SR的局部变量*/
if (OSIntNestingCtr > (OS_NESTING_CTR)0) { /* 检查是否在中断服务程序里调用 */
return; /* Yes ... only schedule when no nested ISRs */
}
if (OSSchedLockNestingCtr > (OS_NESTING_CTR)0) { /* 检查调度器是否上锁 */
return; /* Yes */
}
CPU_INT_DIS(); /*保存状态寄存器*/
OSPrioHighRdy = OS_PrioGetHighest(); /* 找到就绪任务列表里优先级最高的任务 */
OSTCBHighRdyPtr = OSRdyList[OSPrioHighRdy].HeadPtr; /*获取任务的任务控制块儿*/
if (OSTCBHighRdyPtr == OSTCBCurPtr) { /* 判断要切换到的任务是不是当前任务 */
CPU_INT_EN(); /* 恢复SR寄存器 */
return;
}
#if OS_CFG_TASK_PROFILE_EN > 0u
OSTCBHighRdyPtr->CtxSwCtr++; /* Inc. # of context switches to this task */
#endif
OSTaskCtxSwCtr++; /* Increment context switch counter */
#if defined(OS_CFG_TLS_TBL_SIZE) && (OS_CFG_TLS_TBL_SIZE > 0u)
OS_TLS_TaskSw();
#endif
OS_TASK_SW(); /* 触发PendSV异常完成上下文切换 */
CPU_INT_EN(); /*恢复SR寄存器*/
}
2、中断级任务调度
void OSIntExit (void)
{
CPU_SR_ALLOC();
if (OSRunning != OS_STATE_OS_RUNNING) { /* Has the OS started? */
return; /* No */
}
CPU_INT_DIS();
if (OSIntNestingCtr == (OS_NESTING_CTR)0) { /* 确保 OSIntNestingCtr值不会向下溢出 */
CPU_INT_EN();
return;
}
OSIntNestingCtr--;
if (OSIntNestingCtr > (OS_NESTING_CTR)0) { /* 此函数是在中断服务程序结束时被调用的同时UCOS是不允许在
中断里执行上下文切换的(为了保证系统实时性)所以在这里
检查中断嵌套是否结束如果还有嵌套的中断则不执行任务调度直接返回 */
CPU_INT_EN(); /* Yes */
return;
}
if (OSSchedLockNestingCtr > (OS_NESTING_CTR)0) { /* 判断调度器是否上锁 */
CPU_INT_EN(); /* Yes */
return;
}
OSPrioHighRdy = OS_PrioGetHighest(); /* 如果以上情况都没有发生则在这里找到就绪表中优先级最高的任务 */
OSTCBHighRdyPtr = OSRdyList[OSPrioHighRdy].HeadPtr; /* 找到已就绪的优先级最高的任务的任务控制块TCB */
if (OSTCBHighRdyPtr == OSTCBCurPtr) { /* 判断要切换的到的任务是否是当前任务 */
CPU_INT_EN(); /* Yes */
return;
}
#if OS_CFG_TASK_PROFILE_EN > 0u
OSTCBHighRdyPtr->CtxSwCtr++; /* Inc. # of context switches for this new task */
#endif
OSTaskCtxSwCtr++; /* 跟踪调度次数??(我猜的有待考证) */
#if defined(OS_CFG_TLS_TBL_SIZE) && (OS_CFG_TLS_TBL_SIZE > 0u)
OS_TLS_TaskSw();
#endif
OSIntCtxSw(); /* 触发PendSV执行中断级任务调度 */
CPU_INT_EN();
}
在上面的两个任务调度函数里我们可以看到真正触发任务调度的就是 OSIntCtxSw()和OS_TASK_SW()(这是一个宏实际调用的就是OSCtxSw())这两个函数,我们再来看一下这两个函数的具体实现(在os_cpu_a.asm文件里)
NVIC_INT_CTRL EQU 0xE000ED04 ; 中断控制状态寄存器。
NVIC_SYSPRI14 EQU 0xE000ED22 ; 系统优先级寄存器(优先级14)。
NVIC_PENDSV_PRI EQU 0xFFFF ; PendSV优先级(最低)
NVIC_PENDSVSET EQU 0x10000000 ; 触发PendSV异常的值。
OSCtxSw
LDR R0, =NVIC_INT_CTRL ; Trigger the PendSV exception (causes context switch)
LDR R1, =NVIC_PENDSVSET
STR R1, [R0]
BX LR
OSIntCtxSw
LDR R0, =NVIC_INT_CTRL ; Trigger the PendSV exception (causes context switch)
LDR R1, =NVIC_PENDSVSET
STR R1, [R0]
BX LR
概述一下这两个函数的功能,即通过往NVIC寄存器里写一个数然后来触发PendSV异常(可以结合下面列出来的几个汇编指令来看)。
这两个函数是一样的但却在不同的情况下被调用(为什么呢???)
下面整理几个指令然后看一下PendSV的中断处理程序
TST
TST指令测试的是:Rn中所有指定bit位是否全为0(指定的bit位是operand2中为1的所有位);
SUBS
SUBS R0,R0,#1 ;R0减1,结果放入R0中,并且影响标志位
STM
STM批量存储指令用于将多个寄存器存储到一片连续的存储器中。
STR
STR R0 , [R1 , #8] ;将R0中的字数据写入以R1+8为地址的存储器中。
STRB
STRB指令用于从源寄存器中将一个8位的字节数据传送到存储器中。该字节数据为源寄存器中的低8位。
LDR
LDR R1,[R2] ;将R2指向的单元中的数据保存再R1中
LDRB
LDRB指令用于从存储器中将一个8位的字节数据传送到目的寄存器中,同时将寄存器的高24位清零。
LDRB R0 , [R1] ;将地址为R1的字节数据读入R0,并将R0的高24位清零。
BLX
BLX 指令从ARM 指令集跳转到指令中所指定的目标地址,并将处理器的工作状态有ARM 状态切换到Thumb 状态,该指令同时将PC 的当前内容保存到寄存器R14 中。因此,当子程序使用Thumb 指令集,而调用者使用ARM 指令集时,可以通过BLX 指令实现子程序的调用和处理器工作状态的切换。同时,子程序的返回可以通过将寄存器R14 值复制到PC 中来完成。
来看看PendSV的中断服务程序里都做了什么事情
; PendSV异常处理函数
; void OS_CPU_PendSVHandler(void)
;
; 注意: 1)PendSV用于执行上下文切换。 这是在Cortex-M3进行上下文切换推荐的一种方法。
; 这是因为Cortex-M3自动保存了一半的任何异常上的处理器上下文,并在从异常返回时恢复相同。
; 所以只需要保存R4-R11并恢复堆栈指针。 使用PendSV异常这种方式意味着上下文保存和恢复
; 是相同的,无论它是从哪里启动的线程或由于中断或异常而发生。
;
; 2)伪码是:
; a)获取进程SP,如果0,则跳过(goto d)保存部分(第一次上下文切换);
; b)在进程堆栈上保存剩余的寄存器 r4-r11;
; c)将进程SP保存在其TCB、OSTCBCurPtr->OSTCBStkPtr=SP;
; d)呼叫OSTask Sw Hook();
; e)获得当前的高度优先级,OSPrioCur = OSPrioHighRdy;
; f)获取当前准备就绪的线程TCB、OSTCBCurPtr=OSTCBHighRdyPtr;
; g)从TCB获得新的进程SP、SP = OSTCBHighRdyPtr->OSTCBStkPtr;
; h)从新的进程堆栈中恢复R4-R11;
; i)执行将恢复剩余上下文的异常返回。
;
; 3)进入Pend SV处理程序时:
; a)以下内容已保存在进程堆栈上(由处理器保存):
; x PSR,PC,LR,R12,R0-R3
; b)处理器模式切换到Handler模式(从线程模式)
; c)堆栈是主栈(从进程堆栈切换)
; d)OSTCBCurPtr指出暂停任务的OS_TCB
; OSTCBHighRdyPtr指的是恢复任务的OS_TCB
;
; 4)由于PendSV被设置为系统中的最低优先级(由OS Start HighRdy()以上),我们
; 知道只有在没有其他异常或中断处于活动状态时才会运行因此,可以安全地假设
; 正在切换的上下文是使用进程堆栈(PSP)。
;********************************************************************************************************
PendSV_Handler
CPSID I ; 防止上下文切换期间中断(关中断)
MRS R0, PSP ; PSP是进程堆栈指针
CBZ R0, PendSVHandler_nosave ; 跳过寄存器保存第一次(判断为零跳转)
;任务是否使用FPU上下文? 如果是的话,推动高vfp寄存器。
TST R14, #0X10
IT EQ
VSTMDBEQ R0!,{S16-S31}
SUBS R0, R0, #0x20 ; 在进程堆栈上保存剩余的regsr4-11
STM R0, {R4-R11}
LDR R1, =OSTCBCurPtr ; OSTCBCurPtr->OSTCBStkPtr = SP;
LDR R1, [R1]
STR R0, [R1] ; R0是正在切换的进程SP
; 此时,整个过程的上下文已经保存
PendSVHandler_nosave
PUSH {R14} ; Save LR exc_return value
LDR R0, =OSTaskSwHook ; OSTaskSwHook();
BLX R0
POP {R14}
LDR R0, =OSPrioCur ; OSPrioCur = OSPrioHighRdy;
LDR R1, =OSPrioHighRdy
LDRB R2, [R1]
STRB R2, [R0]
LDR R0, =OSTCBCurPtr ; OSTCBCurPtr = OSTCBHighRdyPtr;
LDR R1, =OSTCBHighRdyPtr
LDR R2, [R1]
STR R2, [R0]
LDR R0, [R2] ; R0是新的进程SP;SP=OSTCBHighRdyPtr->StkPtr;
LDM R0, {R4-R11} ; 从新进程堆栈中恢复r4-11
ADDS R0, R0, #0x20
;.任务是否使用FPU上下文? 如果是的话,推动高vfp寄存器。
TST R14, #0x10
IT EQ
VLDMIAEQ R0!, {S16-S31}
MSR PSP, R0 ; 用新的进程SP加载到PSP
ORR LR, LR, #0x04 ; 确保异常返回使用进程堆栈
CPSIE I
BX LR ; 异常返回将恢复剩余上下文
END
这里概述以下中断服务程序里的执行流程:
先把中断关了因为在执行上下文切换时是不想被打断的(原子操作)然后获取进程堆栈指针判断是否为零,如果为零的话就代表是第一次执行任务切换也就是在切换之前没有运行的任务也就没有需要保存的任务现场所以不需要保存直接跳转到PendSVHandler_nosave这里执行任务现场的恢复,然后将OSPrioCur的值设置成要切换到的任务优先级(OSPrioCur = OSPrioHighRdy),再然后将OSTCBCurPtr设置成要切换到的任务的任务控制块的指针(OSTCBCurPtr = OSTCBHighRdyPtr)任务控制块的指针就是任务堆栈的指针因为TCB这个结构体的首元素就是任务的堆栈指针,然后执行任务现场的恢复,如果不是第一次任务切换的话那就将任务的现场保存到任务堆栈中去
SUBS R0, R0, #0x20 ; 在进程堆栈上保存剩余的regsr4-11
STM R0, {R4-R11}
LDR R1, =OSTCBCurPtr ; OSTCBCurPtr->OSTCBStkPtr = SP;
LDR R1, [R1]
STR R0, [R1] ; R0是正在切换的进程SP
这个操作应该是先把R4-R11寄存器保存到R0-0x20这个地址当中去(至于这个地址是哪,我也不知到因为不知到前面的VSTMDBEQ R0!,{S16-S31}
这个指令是什么意思还需要具体了解下,知道的大佬欢迎评论区解答!!!)然后获取要被切换的任务的堆栈指针LDR R1, =OSTCBCurPtr
然后再将刚才保存到R0-0x20这个地址的R4-R11寄存器保存到任务堆栈中。(为什么不先获取任务堆栈指针然后再将寄存器直接保存到任务中去呢???,欢迎评论区解答!!!)。
为什么选择用PendSV来执行上下文切换呢
结合PendSV特性和UCOS对实时性的要求来看,(可悬起!!!、实时性!!!)要满足实时性的要求必然不能忍受在响应中断的时候被像被执行上下文切换这样的复杂的任务的中断打断。那把执行上下文切换的中断优先级设置成最低就好啦!但是如果有一个高优先级的中断正在响应然后又需要执行上下文切换的话那执行上下文切换的任务就得不到响应了呀,但是PendSV的特性正好可以解决这个问题,如果有中断正在响应的话那就悬起等中断执行完之后再响应上下文切换的中断请求。(其实上面的表述是有问题因为任务调度函数OSSched()和OSIntExit()都有判断是否在中断里被调用所以是不需要担心打断正在执行的中断的,但是我觉得这样理解也挺好的)
呃呃呃。。。可是前面已经说过了UCOS不允许再中断响应时执行上下文切换啊,还需要考虑这个问题么?
下面截取一段权威指南中的讲解:
本例中并没有使用SVC异常但是不影响理解的过程。
这里提出个疑问
在时间片轮转调度中并没有看到有触发PendSV异常来执行上下文切换的部分呀,那UCOS在执行时间片轮转调度时是如何保存和恢复任务现场的呢???
我猜应该是只是将要执行任务的设置为双向链表的头,这样在执行任务调度时OSSched()和OSIntExit()时当调度到这个优先级任务的时候就会执行双向链表头的那个任务。(待我继续学习学习后再来更新,欢迎评论区解答!!!)。