您的位置:首页 > 其它

uC/OS 的进程调度(下)

2016-05-04 21:29 477 查看
上文提到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()函数,它也是体系相关的,因此一般也用汇编实现。后面有机会我们会单独对它进行仔细分析。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: