1. 程式人生 > >uC/OS 的程序排程(下)

uC/OS 的程序排程(下)

上文提到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()函式,它也是體系相關的,因此一般也用匯編實現。後面有機會我們會單獨對它進行仔細分析。