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
- 讓當前任務指標指向最高優先順序任務,等價於
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()函式,它也是體系相關的,因此一般也用匯編實現。後面有機會我們會單獨對它進行仔細分析。