uC/OS 的进程调度(下)

本文深入探讨uC/OS的进程切换实现,包括系统启动时、进程上下文和中断上下文的切换过程,涉及OSCtxSw、OSStartHighRdy等关键函数,并与Linux的进程切换进行对比。

上文提到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
  1. 让当前任务指针指向最高优先级任务,等价于OSTCBCur = OSTCBHighRdy;
  2. 获取当前任务栈指针,等价于SP = *OSTCBHighRdy; 任务控制块TCB结构体的第一个变量OSTCBStkPtr用于保存进程的栈顶指针,这样直接通过指向TCB的指针就能获取任务的栈顶。这与Linux寻址内核栈的设计有异曲同工的妙处。
  3. 调用一个用户自定义的钩子函数。
  4. SP指向当前任务栈的栈顶,偏移0将进程的入口地址写入到EPC影子寄存器中。
  5. 接下来用SP偏移4的值写入到影子状态寄存器EPSR中。
  6. 依次从栈中弹出15个值,分别写入R1-R15寄存器。注意这时R2中为p_args,R15中为OS_TaskReturn。
  7. 调用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
  1. 保护现场,把R1-R15压入当前旧进程栈。
  2. 根据CK-CPU手册,在异常服务程序中,EPC影子寄存器指向trap指令,我们将其加2后压栈以指向下一条指令。这是因为我们希望下次切回该进程时,执行trap的下一条指令。
  3. 把EPSR影子状态寄存器压栈,它与PSR寄存器内容一致。
  4. 把当前任务的栈顶指针SP保存入控制块TCB结构体的第一个变量,等价于*OSTCBHighRdy = SP;这也利用了TCB结构体变量排列的特性。
  5. 调用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()函数,它也是体系相关的,因此一般也用汇编实现。后面有机会我们会单独对它进行仔细分析。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值