上文提到uC/OS进程调度的前两个主题:何时进行调度、如何选择下一个活动进程。本文来分析最后一个主题,即如何实现进程切换。
从上文的分析可知,uC/OS在实现调度时,涉及的最核心的函数只有三个:OSStartHighRdy()、OS_Sched()及OSIntCtxSw(),它们分别对应系统启动时、进程上下文时、中断上下文时的进程切换。
这些函数的实现基本都是体系结构相关的,需要在移植操作系统时实现。这里以CK-CPU为例来讲解它们的具体实现方法。
在实现进程切换时,最重要的是关注新旧进程栈的状态,以及栈中保存的PC指针的值。
系统启动时实现切换
在main()中调用OSStart()启动系统时,标志系统运行状态的OSRunning变量为0,第一个进程的栈已经由OSTaskStkInit()初始化为如下结构:
OSStart()会调用OSStartHighRdy()启动第一个进程。
=============== os_cpu_a.S 87 92 ====================
OSStartHighRdy:
lrw r1,OSRunning // Set OSRunning to (1)
movi r2,1
st.b r2,(r1)
br OSCtxSwReturn
首先把OSRunning变量设为1,从而开启操作系统的各个功能,然后跳转到OSCtxSwReturn。
=============== os_cpu_a.S 137 158 ====================
OSCtxSwReturn:
lrw r1,OSTCBHighRdy // Get highest priority task and make
ld.w r3,(r1) // it the current task
lrw r4,OSTCBCur
st.w r3,(r4)
ld.w sp,(r3) // Get current task stack pointer
jbsr OSTaskSwHook // Call task switch hook
ld.w r1,(sp,0) // Get the PC for the task
mtcr r1,EPC
ld.w r1,(sp,4) // Get the PSR for the task
mtcr r1,EPSR
addi sp,8 // Increment SP past the PC and PSR
ldm r1-r15,(sp) // Load R0-R13 from the stack
addi sp,32 // Increment SP past the registers
addi sp,28 // Increment SP past the registers
rte // Return to new task
- 让当前任务指针指向最高优先级任务,等价于
OSTCBCur = OSTCBHighRdy; - 获取当前任务栈指针,等价于
SP = *OSTCBHighRdy;任务控制块TCB结构体的第一个变量OSTCBStkPtr用于保存进程的栈顶指针,这样直接通过指向TCB的指针就能获取任务的栈顶。这与Linux寻址内核栈的设计有异曲同工的妙处。 - 调用一个用户自定义的钩子函数。
- SP指向当前任务栈的栈顶,偏移0将进程的入口地址写入到EPC影子寄存器中。
- 接下来用SP偏移4的值写入到影子状态寄存器EPSR中。
- 依次从栈中弹出15个值,分别写入R1-R15寄存器。注意这时R2中为p_args,R15中为OS_TaskReturn。
- 调用rte指令返回,这时硬件自动把从影子寄存器EPSR和EPC的值拷贝入PSR和PC,然后从PC处开始执行。由于PC当前指向的是进程的入口地址,所以新进程得以执行。
这里还要注意一个细节,uC/OS规定进程入口函数有一个指针类型的参数,而CK-CPU规定由R2保存C语言的第一个参数,所以p_args要保存在R2中才能正好完成函数调用。
在进程上下文中实现切换
在某个进程正在运行过程中,要实现从旧进程切换到新进程时,通过直接或间接调用OS_Sched()实现切换。
=============== os_core.c 1630 1653 ====================
void OS_Sched (void)
{
#if OS_CRITICAL_METHOD == 3u /* Allocate storage for CPU status register */
OS_CPU_SR cpu_sr = 0u;
#endif
OS_ENTER_CRITICAL();
if (OSIntNesting == 0u) { /* Schedule only if all ISRs done and ... */
if (OSLockNesting == 0u) { /* ... scheduler is not locked */
OS_SchedNew();
OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];
if (OSPrioHighRdy != OSPrioCur) { /* No Ctx Sw if current task is highest rdy */
#if OS_TASK_PROFILE_EN > 0u
OSTCBHighRdy->OSTCBCtxSwCtr++; /* Inc. # of context switches to this task */
#endif
OSCtxSwCtr++; /* Increment context switch counter */
OS_TASK_SW(); /* Perform a context switch */
}
}
}
OS_EXIT_CRITICAL();
}
代码的核心有两个,一是调用OS_SchedNew()选出一个要切换至的新进程存入OSPrioHighRdy变量,二是调用OS_TASK_SW()实现切换。前者的原理在上文中已有描述,这里来看后一个函数。
=============== os_cpu.h 75 75 ====================
#define OS_TASK_SW() asm("TRAP 0")
它是一个移植需要实现的函数。在CK-CPU上使用TRAP指令进入异常处理中,其服务函数在中断向量表中指向了OSCtxSw。
=============== os_cpu_a.S 102 121 ====================
OSCtxSw:
subi sp,32 // Decrement SP to save registers
subi sp,28 // Decrement SP to save registers
stm r1-r15,(sp) // Save all registers to the stack
subi sp,8 // Decrement SP to save PC and PSR
mfcr r1,EPC // Save the PC for the current task
addi r1,2 // Add 2 to PC to get past TRAP
// instruction when returning
st.w r1,(sp,0)
mfcr r1,EPSR // Save the PSR for the current task
st.w r1,(sp,4)
lrw r2,OSTCBCur // Save the current task SP in the TCB
ld.w r3,(r2)
st.w sp,(r3)
br OSIntCtxSw
- 保护现场,把R1-R15压入当前旧进程栈。
- 根据CK-CPU手册,在异常服务程序中,EPC影子寄存器指向trap指令,我们将其加2后压栈以指向下一条指令。这是因为我们希望下次切回该进程时,执行trap的下一条指令。
- 把EPSR影子状态寄存器压栈,它与PSR寄存器内容一致。
- 把当前任务的栈顶指针SP保存入控制块TCB结构体的第一个变量,等价于
*OSTCBHighRdy = SP;这也利用了TCB结构体变量排列的特性。 - 调用OSIntCtxSw。
由此可见,旧进程栈中保存着当前被切出时的各寄存器状态,当前栈顶部分的结构如下图所示。同样,新进程要么是新创建的,要么是之前曾经运行但被切出的,因此它的当前栈的状态也是与图中一样的。
OSIntCtxSw与在中断上下文中实现切换是同一段代码,我们继续分析。
在中断上下文中实现切换
在中断的退出阶段,如有必要,会调用OSIntCtxSw实现从中断前的旧进程切换到新进程。与在进程上下文中实现切换唯一不同的是,旧进程的栈是在发生中断时由硬件自动保存的。由于我们在实现OSCtxSw时参考了CK-CPU硬件手册,使得这两种情况下旧进程的栈都是一模一样的,因此在切换时几乎没有不同,事实上它们共享了约85%的代码,即上面分析过的OSCtxSwReturn。
我们来看OSIntCtxSw的实现:
=============== os_cpu_a.S 131 137 ====================
OSIntCtxSw:
lrw r1,OSPrioHighRdy // Copy the highest priority to the
lrw r2,OSPrioCur // current
ld.b r3,(r1)
st.b r3,(r2)
OSCtxSwReturn:
它先给代表当前任务优先级的OSPrioCur变量赋值为OSPrioHighRdy,然后就开始执行OSCtxSwReturn切换到新进程。这个过程与第一节描述的完全一致,可以返回去看到新进程是如何启动的。
与Linux比较
Linux的进程切换主体是schedule()函数,核心是switch_to()函数,它也是体系相关的,因此一般也用汇编实现。后面有机会我们会单独对它进行仔细分析。
本文深入探讨uC/OS的进程切换实现,包括系统启动时、进程上下文和中断上下文的切换过程,涉及OSCtxSw、OSStartHighRdy等关键函数,并与Linux的进程切换进行对比。
&spm=1001.2101.3001.5002&articleId=51318220&d=1&t=3&u=aa6e90f0acd14f1f88833f2146eb8d3e)
6768

被折叠的 条评论
为什么被折叠?



