摘要:排程,Schedule也稱為Dispatch,是作業系統的一個重要模組,它負責選擇系統要處理的下一個任務。排程模組需要協調處於就緒狀態的任務對資源的競爭,按優先順序策略從就緒佇列中獲取高優先順序的任務,給予資源使用權。
本文分享自華為雲社群《LiteOS核心原始碼分析系列六 -任務及排程(5)-任務LOS_Schedule》,原文作者:zhushy 。
本文我們來一起學習下LiteOS排程模組的原始碼,文中所涉及的原始碼,均可以在LiteOS
開源站點https://gitee.com/LiteOS/LiteOS 獲取。排程原始碼分佈如下:
- LiteOS核心排程原始碼
包括排程模組的私有標頭檔案kernel\base\include\los_sched_pri.h、C原始碼檔案kernel\base\sched\sched_sq\los_sched.c,這個對應單連結串列就緒佇列。還有個`排程原始碼檔案kernel\base\sched\sched_mq\los_sched.c,對應多連結串列就緒佇列。本文主要剖析對應單連結串列就緒佇列的排程檔案程式碼,使用多連結串列就緒佇列的排程程式碼類似。
- 排程模組彙編實現程式碼
排程模組的彙編函式有OsStartToRun、OsTaskSchedule等,根據不同的CPU架構,分佈在下述檔案裡: arch\arm\cortex_m\src\dispatch.S、arch\arm\cortex_a_r\src\dispatch.S、arch\arm64\src\dispatch.S。
本文以STM32F769IDISCOVERY為例,分析一下Cortex-M核的排程模組的原始碼。我們先看看排程標頭檔案kernel\base\include\los_sched_pri.h中定義的巨集函式、列舉、和行內函數。
1、排程模組巨集函式和行內函數
kernel\base\include\los_sched_pri.h定義的巨集函式、列舉、行內函數。
1.1 巨集函式和列舉
UINT32 g_taskScheduled是kernel\base\los_task.c定義的全域性變數,標記核心是否開啟排程,每一位代表不同的CPU核的排程開啟狀態。
⑴處定義的巨集函式OS_SCHEDULER_SET(cpuid)開啟cpuid核的排程。⑵處巨集函式OS_SCHEDULER_CLR(cpuid)是前者的反向操作,關閉cpuid核的排程。⑶處巨集判斷當前核是否開啟排程。⑷處的列舉用於標記是否發起了請求排程。當需要排程,又暫不具備排程條件的時候,標記下狀態,等具備排程的條件時,再去排程。
⑴ #define OS_SCHEDULER_SET(cpuid) do { \ g_taskScheduled |= (1U << (cpuid)); \ } while (0); ⑵ #define OS_SCHEDULER_CLR(cpuid) do { \ g_taskScheduled &= ~(1U << (cpuid)); \ } while (0); ⑶ #define OS_SCHEDULER_ACTIVE (g_taskScheduled & (1U << ArchCurrCpuid())) ⑷ typedef enum { INT_NO_RESCH = 0, /* no needs to schedule */ INT_PEND_RESCH, /* pending schedule flag */ } SchedFlag;
1.2 行內函數
有2個行內函數用於檢查是否可以排程,即函式STATIC INLINE BOOL OsPreemptable(VOID)和STATIC INLINE BOOL OsPreemptableInSched(VOID)。區別是,前者判斷是否可以搶佔排程時,先關中斷,避免當前的任務遷移到其他核,返回錯誤的是否可以搶佔排程狀態。
1.2.1 行內函數STATIC INLINE BOOL OsPreemptable(VOID)
我們看下BOOL OsPreemptable(VOID)函式的原始碼。⑴、⑶屬於關閉、開啟中斷,保護檢查搶佔狀態的操作。⑵處判斷是否可搶佔排程,如果不能排程,則標記下是否需要排程標籤為INT_PEND_RESCH。
STATIC INLINE BOOL OsPreemptable(VOID) { ⑴ UINT32 intSave = LOS_IntLock(); ⑵ BOOL preemptable = (OsPercpuGet()->taskLockCnt == 0); if (!preemptable) { OsPercpuGet()->schedFlag = INT_PEND_RESCH; } ⑶ LOS_IntRestore(intSave); return preemptable; }
1.2.2 行內函數STATIC INLINE BOOL OsPreemptableInSched(VOID)
函式STATIC INLINE BOOL OsPreemptableInSched(VOID)檢查是否可以搶佔排程,檢查的方式是判斷OsPercpuGet()->taskLockCnt的計數,見⑴、⑵處程式碼。如果不能排程,則執行⑶標記下是否需要排程標籤為INT_PEND_RESCH。對於SMP多核,是否可以排程的檢查方式,稍有不同,因為排程持有自旋鎖,計數需要加1,見程式碼。
STATIC INLINE BOOL OsPreemptableInSched(VOID) { BOOL preemptable = FALSE; #ifdef LOSCFG_KERNEL_SMP ⑴ preemptable = (OsPercpuGet()->taskLockCnt == 1); #else ⑵ preemptable = (OsPercpuGet()->taskLockCnt == 0); #endif if (!preemptable) { ⑶ OsPercpuGet()->schedFlag = INT_PEND_RESCH; } return preemptable; }
1.2.3 行內函數STATIC INLINE VOID LOS_Schedule(VOID)
函式STATIC INLINE VOID LOS_Schedule(VOID)用於觸發觸發排程。⑴處程式碼表示,如果系統正在處理中斷,標記下是否需要排程標籤為INT_PEND_RESCH,等待合適時機再排程。然後呼叫VOID OsSchedPreempt(VOID)函式,下午會分析該函式。二者的區別就是多個檢查,判斷是否系統是否正在處理中斷。
STATIC INLINE VOID LOS_Schedule(VOID) { if (OS_INT_ACTIVE) { ⑴ OsPercpuGet()->schedFlag = INT_PEND_RESCH; return; } OsSchedPreempt(); }
2、排程模組常用介面
這一小節,我們看看kernel\base\sched\sched_sq\los_sched.c定義的排程介面,包含VOID OsSchedPreempt(VOID)、VOID OsSchedResched(VOID)兩個主要的排程介面。兩者的區別是,前者需要把當前任務放入就緒佇列內,再呼叫後者觸發呼叫。後者直接從就緒佇列裡獲取下一個任務,然後觸發排程去執行下一個任務。這2個介面都是內部介面,對外提供的排程介面是上一小節分析過的STATIC INLINE VOID LOS_Schedule(VOID),三者有呼叫關係STATIC INLINE VOID LOS_Schedule(VOID)--->VOID OsSchedPreempt(VOID)--->VOID OsSchedResched(VOID)。
我們分析下這些排程介面的原始碼。
2.1 搶佔排程函式VOID OsSchedResched(VOID)
搶佔排程函式VOID OsSchedResched(VOID),我們分析下原始碼。
⑴驗證需要持有任務模組的自旋鎖。⑵處判斷是否支援排程,如果不具備排程的條件,則暫不排程。⑶獲取當前執行任務,從就緒佇列中獲取下一個高優先順序的任務。驗證下一個任務newTask不能為空,並更改其狀態為非就緒狀態。⑷處判斷當前任務和下一個任務不能為同一個,否則返回。這種情況不會發生,當前任務肯定會從優先順序佇列中移除的,二者不可能是同一個。⑸更改2個任務的執行狀態,當前任務設定為非執行狀態,下一個任務設定為執行狀態。⑹處如果支援多核,則更改任務的執行在哪個核。緊接著的一些程式碼屬於排程維測資訊,暫時不管。⑺處如果支援時間片排程,並且下一個新任務的時間片為0,設定為時間片超時時間的最大值LOSCFG_BASE_CORE_TIMESLICE_TIMEOUT。⑻設定下一個任務newTask為當前執行任務,會更新全域性變數g_runTask。然後呼叫匯編函式OsTaskSchedule(newTask, runTask)執行排程,後文分析該彙編函式的實現程式碼。
VOID OsSchedResched(VOID) { LosTaskCB *runTask = NULL; LosTaskCB *newTask = NULL; ⑴ LOS_ASSERT(LOS_SpinHeld(&g_taskSpin)); ⑵ if (!OsPreemptableInSched()) { return; } ⑶ runTask = OsCurrTaskGet(); newTask = OsGetTopTask(); LOS_ASSERT(newTask != NULL); newTask->taskStatus &= ~OS_TASK_STATUS_READY; ⑷ if (runTask == newTask) { return; } ⑸ runTask->taskStatus &= ~OS_TASK_STATUS_RUNNING; newTask->taskStatus |= OS_TASK_STATUS_RUNNING; #ifdef LOSCFG_KERNEL_SMP ⑹ runTask->currCpu = OS_TASK_INVALID_CPUID; newTask->currCpu = ArchCurrCpuid(); #endif OsTaskTimeUpdateHook(runTask->taskId, LOS_TickCountGet()); #ifdef LOSCFG_KERNEL_CPUP OsTaskCycleEndStart(newTask); #endif #ifdef LOSCFG_BASE_CORE_TSK_MONITOR OsTaskSwitchCheck(runTask, newTask); #endif LOS_TRACE(TASK_SWITCH, newTask->taskId, runTask->priority, runTask->taskStatus, newTask->priority, newTask->taskStatus); #ifdef LOSCFG_DEBUG_SCHED_STATISTICS OsSchedStatistics(runTask, newTask); #endif PRINT_TRACE("cpu%u (%s) status: %x -> (%s) status:%x\n", ArchCurrCpuid(), runTask->taskName, runTask->taskStatus, newTask->taskName, newTask->taskStatus); #ifdef LOSCFG_BASE_CORE_TIMESLICE if (newTask->timeSlice == 0) { ⑺ newTask->timeSlice = LOSCFG_BASE_CORE_TIMESLICE_TIMEOUT; } #endif ⑻ OsCurrTaskSet((VOID*)newTask); OsTaskSchedule(newTask, runTask); }
2.2 搶佔排程函式VOID OsSchedPreempt(VOID)
搶佔排程函式VOID OsSchedPreempt(VOID),把當前任務放入就緒佇列,從佇列中獲取高優先順序任務,然後嘗試排程。當鎖排程,或者沒有更高優先順序任務時,排程不會發生。⑴處判斷是否支援排程,如果不具備排程的條件,則暫不排程。⑵獲取當前任務,更改其狀態為非就緒狀態。
如果開啟時間片排程並且當前任務時間片為0,則執行⑶把當前任務放入就緒佇列的尾部,否則執行⑷把當前任務放入就緒佇列的頭部,同等優先順序下可以更早的執行。⑸呼叫函式OsSchedResched()去排程。
VOID OsSchedPreempt(VOID) { LosTaskCB *runTask = NULL; UINT32 intSave; ⑴ if (!OsPreemptable()) { return; } SCHEDULER_LOCK(intSave); ⑵ runTask = OsCurrTaskGet(); runTask->taskStatus |= OS_TASK_STATUS_READY; #ifdef LOSCFG_BASE_CORE_TIMESLICE if (runTask->timeSlice == 0) { ⑶ OsPriQueueEnqueue(&runTask->pendList, runTask->priority); } else { #endif ⑷ OsPriQueueEnqueueHead(&runTask->pendList, runTask->priority); #ifdef LOSCFG_BASE_CORE_TIMESLICE } #endif ⑸ OsSchedResched(); SCHEDULER_UNLOCK(intSave); }
2.3 時間片檢查函式VOID OsTimesliceCheck(VOID)
函式VOID OsTimesliceCheck(VOID)在支援時間片排程時才生效,該函式在tick中斷函式VOID OsTickHandler(VOID)裡呼叫。如果當前執行函式的時間片使用完畢,則觸發排程。⑴處獲取當前執行任務,⑵判斷runTask->timeSlice時間片是否為0,不為0則減1。如果減1後為0,則執行⑶呼叫LOS_Schedule()觸發排程。
#ifdef LOSCFG_BASE_CORE_TIMESLICE LITE_OS_SEC_TEXT VOID OsTimesliceCheck(VOID) { ⑴ LosTaskCB *runTask = OsCurrTaskGet(); ⑵ if (runTask->timeSlice != 0) { runTask->timeSlice--; if (runTask->timeSlice == 0) { ⑶ LOS_Schedule(); } } } #endif
3、排程模組彙編函式
檔案arch\arm\cortex_m\src\dispatch.S定義了排程的彙編函式,我們分析下這些排程介面的原始碼。彙編檔案中定義瞭如下幾個巨集,見註釋。
.equ OS_NVIC_INT_CTRL, 0xE000ED04 ; Interrupt Control State Register,ICSR 中斷控制狀態暫存器 .equ OS_NVIC_SYSPRI2, 0xE000ED20 ; System Handler Priority Register 系統優先順序暫存器 .equ OS_NVIC_PENDSV_PRI, 0xF0F00000 ; PendSV異常優先順序 .equ OS_NVIC_PENDSVSET, 0x10000000 ; ICSR暫存器的PENDSVSET位置1時,會觸發PendSV異常 .equ OS_TASK_STATUS_RUNNING, 0x0010 ; los_task_pri.h中的同名巨集定義,數值也一樣,表示任務執行狀態,
3.1 OsStartToRun彙編函式
函式OsStartToRun在檔案kernel\init\los_init.c中的執行函式VOID OsStart(VOID)啟動系統階段呼叫,傳入的引數為就緒佇列中最高優秀級的LosTaskCB *taskCB。我們接下來分析下該函式的彙編程式碼。
⑴處設定PendSV異常優先順序為OS_NVIC_PENDSV_PRI,PendSV異常一般設定為最低。全域性變數g_oldTask、g_runTask定義在arch\arm\cortex_m\src\task.c檔案內,分別記錄上一次執行的任務、和當前執行的任務。⑵處程式碼把函式OsStartToRun的入參LosTaskCB *taskCB賦值給這2個全域性變數。
⑶處往控制暫存器CONTROL寫入二進位制的10,表示使用PSP棧,特權級的執行緒模式。UINT16 taskStatus是LosTaskCB結構體的第二個成員變數,⑷處[r0 , #4]獲取任務狀態,此時暫存器r7數值為0x4,即就緒狀態OS_TASK_STATUS_READY。然後把任務狀態改為執行狀態OS_TASK_STATUS_RUNNING。
⑸處把[r0]的值即任務的棧指標taskCB->stackPointer載入到暫存器R12,現在R12指向任務棧的棧指標,任務棧現在儲存的是上下文,對應定義在arch\arm\cortex_m\include\arch\task.h中的結構體TaskContext。往後2行程式碼把R12加36+64=100,共25個4位元組長度,其中包含S16到S31共16個4位元組,R4到R11及PriMask共9個4位元組的長度,當前R12指向任務棧中上下文的UINT32 R0位置,如圖。
⑹處程式碼把任務棧上下文中的UINT32 R0; UINT32 R1; UINT32 R2; UINT32 R3; UINT32 R12; UINT32 LR; UINT32 PC; UINT32 xPSR;的分別載入到暫存器R0-R7,其中R5對應UINT32 LR,R6對應UINT32 PC,此時暫存器R12指向任務棧上下文的UINT32 xPSR。執行⑺處指令,指標繼續加18個4位元組長度,即對應S0到S15及UINT32 FPSCR; UINT32 NO_NAME等上下文的18個成員。此時,暫存器R12指向任務棧的棧底,緊接著把暫存器R12寫入暫存器psp。
最後,執行⑻處指令,把R5寫入lr暫存器,開中斷,然後跳轉到R6對應的上下文的PC對應的函式VOID OsTaskEntry(UINT32 taskId),去執行任務的入口函式。
.type OsStartToRun, %function .global OsStartToRun OsStartToRun: .fnstart .cantunwind ⑴ ldr r4, =OS_NVIC_SYSPRI2 ldr r5, =OS_NVIC_PENDSV_PRI str r5, [r4] ⑵ ldr r1, =g_oldTask str r0, [r1] ldr r1, =g_runTask str r0, [r1] #if defined(LOSCFG_ARCH_CORTEX_M0) movs r1, #2 msr CONTROL, r1 ldrh r7, [r0 , #4] movs r6, #OS_TASK_STATUS_RUNNING strh r6, [r0 , #4] ldr r3, [r0] adds r3, r3, #36 ldmfd r3!, {r0-r2} adds r3, r3, #4 ldmfd r3!, {R4-R7} msr psp, r3 subs r3, r3, #20 ldr r3, [r3] #else ⑶ mov r1, #2 msr CONTROL, r1 ⑷ ldrh r7, [r0 , #4] mov r8, #OS_TASK_STATUS_RUNNING strh r8, [r0 , #4] ⑸ ldr r12, [r0] ADD r12, r12, #36 #if !defined(LOSCFG_ARCH_CORTEX_M3) ADD r12, r12, #64 #endif ⑹ ldmfd r12!, {R0-R7} #if !defined(LOSCFG_ARCH_CORTEX_M3) ⑺ add r12, r12, #72 #endif msr psp, r12 #if !defined(LOSCFG_ARCH_CORTEX_M3) vpush {s0}; vpop {s0}; #endif #endif ⑻ mov lr, r5 cpsie I bx r6 .fnend
3.2 OsTaskSchedule彙編函式
彙編函式OsTaskSchedule實現新老任務的切換排程。從上文分析搶佔排程函式VOID OsSchedResched(VOID)時可以知道,傳入了2個引數,分別是新任務LosTaskCB *newTask和當前執行的任務LosTaskCB *runTask,對於Cortex-M核,這2個引數在該彙編函式中沒有使用到。在執行彙編函式OsTaskSchedule前,全域性變數g_runTask被賦值為要切換執行的新任務LosTaskCB *newTask。
我們看看這個彙編函式的原始碼,首先往中斷控制狀態暫存器OS_NVIC_INT_CTRL中的OS_NVIC_PENDSVSET位置1,觸發PendSV異常。執行完畢osTaskSchedule函式,返回上層函式搶佔排程函式VOID OsSchedResched(VOID)。PendSV異常的回撥函式是osPendSV彙編函式,下文會分析此函式。彙編函式OsTaskSchedule如下:
.type OsTaskSchedule, %function .global OsTaskSchedule OsTaskSchedule: .fnstart .cantunwind ldr r2, =OS_NVIC_INT_CTRL ldr r3, =OS_NVIC_PENDSVSET str r3, [r2] bx lr .fnend
3.3 osPendSV彙編函式
接下來,我們分析下osPendSV彙編函式的原始碼。⑴處把暫存器PRIMASK數值寫入暫存器r12,備份中斷的開關狀態,然後執行指令cpsid I遮蔽全域性中斷。⑵處把當前任務棧的棧指標載入到暫存器r0。⑶處把暫存器r4-r12的數值壓入當前任務棧,執行⑷把暫存器d8-d15的數值壓入當前任務棧,r0為任務棧指標。
⑸處指令把g_oldTask指標地址載入到r5暫存器,然後下一條指令把g_oldTask指標指向的記憶體地址值載入到暫存器r1,然後使用暫存器r0數值更新g_oldTask任務的棧指標。
⑹處指令把g_runTask指標地址載入到r0暫存器,然後下一條指令把g_runTask指標指向的記憶體地址值載入到暫存器r0。此時,r5為上一個任務g_oldTask的指標地址,執行⑺處指令後,g_oldTask、g_runTask都指向新任務。
執行⑻處指令把g_runTask指標指向的記憶體地址值載入到暫存器r1,此時r1暫存器為新任務g_runTask的棧指標。⑼處指令把新任務棧中的資料載入到暫存器d8-d15暫存器,繼續執行後續指令繼續載入資料到r4-r12暫存器,然後執行⑽處指令更新psp任務棧指標。⑾處指令恢復中斷狀態,然後執行跳轉指令,後續繼續執行C程式碼VOID OsTaskEntry(UINT32 taskId)進入任務執行入口函式。
.type osPendSV, %function .global osPendSV osPendSV: .fnstart .cantunwind ⑴ mrs r12, PRIMASK cpsid I TaskSwitch: ⑵ mrs r0, psp #if defined(LOSCFG_ARCH_CORTEX_M0) subs r0, #36 stmia r0!, {r4-r7} mov r3, r8 mov r4, r9 mov r5, r10 mov r6, r11 mov r7, r12 stmia r0!, {r3 - r7} subs r0, #36 #else ⑶ stmfd r0!, {r4-r12} #if !defined(LOSCFG_ARCH_CORTEX_M3) ⑷ vstmdb r0!, {d8-d15} #endif #endif ⑸ ldr r5, =g_oldTask ldr r1, [r5] str r0, [r1] ⑹ ldr r0, =g_runTask ldr r0, [r0] /* g_oldTask = g_runTask */ ⑺ str r0, [r5] ⑻ ldr r1, [r0] #if !defined(LOSCFG_ARCH_CORTEX_M3) && !defined(LOSCFG_ARCH_CORTEX_M0) ⑼ vldmia r1!, {d8-d15} #endif #if defined(LOSCFG_ARCH_CORTEX_M0) adds r1, #16 ldmfd r1!, {r3-r7} mov r8, r3 mov r9, r4 mov r10, r5 mov r11, r6 mov r12, r7 subs r1, #36 ldmfd r1!, {r4-r7} adds r1, #20 #else ldmfd r1!, {r4-r12} #endif ⑽ msr psp, r1 ⑾ msr PRIMASK, r12 bx lr .fnend
3.4 開關中斷彙編函式
分析中斷原始碼的時候,提到過開關中斷函式UINT32 LOS_IntLock(VOID)、UINT32 LOS_IntUnLock(VOID)、VOID LOS_IntRestore(UINT32 intSave)呼叫了彙編函式,這些彙編函式分別是本文要分析的ArchIntLock、ArchIntUnlock、ArchIntRestore。我們看下這些彙編程式碼,PRIMASK暫存器是單一bit的暫存器,置為1後,就關掉所有可遮蔽異常,只剩下NMI和硬Fault異常可以響應。預設值是0,表示沒有關閉中斷。彙編指令cpsid I會設定PRIMASK=1,關閉中斷,指令cpsie I設定PRIMASK=0,開啟中斷。
⑴處ArchIntLock函式把暫存器PRIMASK數值返回並關閉中斷。⑵處ArchIntUnlock函式把暫存器PRIMASK數值返回並開啟中斷。兩個函式的返回結果可以傳遞給⑶處ArchIntRestore函式,把暫存器狀態數值寫入暫存器PRIMASK,用於恢復之前的中斷狀態。不管是ArchIntLock還是ArchIntUnlock,都可以和ArchIntRestore配對使用。
.type ArchIntLock, %function .global ArchIntLock ⑴ ArchIntLock: .fnstart .cantunwind mrs r0, PRIMASK cpsid I bx lr .fnend .type ArchIntUnlock, %function .global ArchIntUnlock ⑵ ArchIntUnlock: .fnstart .cantunwind mrs r0, PRIMASK cpsie I bx lr .fnend .type ArchIntRestore, %function .global ArchIntRestore ⑶ ArchIntRestore: .fnstart .cantunwind msr PRIMASK, r0 bx lr .fnend
小結
本文帶領大家一起剖析了LiteOS排程模組的原始碼,包含呼叫介面及底層的彙編函式實現。感謝閱讀,如有任何問題、建議,都可以留言給我們: https://gitee.com/LiteOS/LiteOS/issues 。