摘要:調度,Schedule也稱爲Dispatch,是操做系統的一個重要模塊,它負責選擇系統要處理的下一個任務。調度模塊須要協調處於就緒狀態的任務對資源的競爭,按優先級策略從就緒隊列中獲取高優先級的任務,給予資源使用權。
本文分享自華爲雲社區《LiteOS內核源碼分析系列六 -任務及調度(5)-任務LOS_Schedule》,原文做者:zhushy 。git
本文咱們來一塊兒學習下LiteOS調度模塊的源代碼,文中所涉及的源代碼,都可以在LiteOS
開源站點https://gitee.com/LiteOS/LiteOS 獲取。調度源代碼分佈以下:sql
包括調度模塊的私有頭文件kernelbaseincludelos_sched_pri.h、C源代碼文件kernelbaseschedsched_sqlos_sched.c,這個對應單鏈表就緒隊列。還有個`調度源代碼文件kernelbaseschedsched_mqlos_sched.c,對應多鏈表就緒隊列。本文主要剖析對應單鏈表就緒隊列的調度文件代碼,使用多鏈表就緒隊列的調度代碼相似。segmentfault
調度模塊的彙編函數有OsStartToRun、OsTaskSchedule等,根據不一樣的CPU架構,分佈在下述文件裏: archarmcortex_msrcdispatch.S、archarmcortex_a_rsrcdispatch.S、archarm64srcdispatch.S。架構
本文以STM32F769IDISCOVERY爲例,分析一下Cortex-M核的調度模塊的源代碼。咱們先看看調度頭文件kernelbaseincludelos_sched_pri.h中定義的宏函數、枚舉、和內聯函數。函數
kernelbaseincludelos_sched_pri.h定義的宏函數、枚舉、內聯函數。源碼分析
UINT32 g_taskScheduled是kernelbaselos_task.c定義的全局變量,標記內核是否開啓調度,每一位表明不一樣的CPU核的調度開啓狀態。學習
⑴處定義的宏函數OS_SCHEDULER_SET(cpuid)開啓cpuid核的調度。⑵處宏函數OS_SCHEDULER_CLR(cpuid)是前者的反向操做,關閉cpuid核的調度。⑶處宏判斷當前核是否開啓調度。⑷處的枚舉用於標記是否發起了請求調度。當須要調度,又暫不具有調度條件的時候,標記下狀態,等具有調度的條件時,再去調度。ui
⑴ #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;
有2個內聯函數用於檢查是否能夠調度,即函數STATIC INLINE BOOL OsPreemptable(VOID)和STATIC INLINE BOOL OsPreemptableInSched(VOID)。區別是,前者判斷是否能夠搶佔調度時,先關中斷,避免當前的任務遷移到其餘核,返回錯誤的是否能夠搶佔調度狀態。spa
咱們看下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; }
函數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; }
函數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(); }
這一小節,咱們看看kernelbaseschedsched_sqlos_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)。
咱們分析下這些調度接口的源代碼。
搶佔調度函數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:%xn", 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); }
搶佔調度函數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); }
函數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
文件archarmcortex_msrcdispatch.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中的同名宏定義,數值也同樣,表示任務運行狀態,
函數OsStartToRun在文件kernelinitlos_init.c中的運行函數VOID OsStart(VOID)啓動系統階段調用,傳入的參數爲就緒隊列中最高優秀級的LosTaskCB *taskCB。咱們接下來分析下該函數的彙編代碼。
⑴處設置PendSV異常優先級爲OS_NVIC_PENDSV_PRI,PendSV異常通常設置爲最低。全局變量g_oldTask、g_runTask定義在archarmcortex_msrctask.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指向任務棧的棧指針,任務棧如今保存的是上下文,對應定義在archarmcortex_mincludearchtask.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
彙編函數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
接下來,咱們分析下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
分析中斷源代碼的時候,提到過開關中斷函數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/Lite... 。