調度,Schedule也稱爲Dispatch,是操作系統的一個重要模塊,它負責選擇系統要處理的下一個任務。調度模塊需要協調處於就緒狀態的任務對資源的競爭,按優先級策略從就緒隊列中獲取高優先級的任務,給予資源使用權。
下面,我們剖析下任務調度模塊的源代碼,若涉及開發板部分,以開發板工程targets\cortex-m7_nucleo_f767zi_gcc\爲例進行源碼分析。
1、調度模塊的重要函數
文件kernel\src\los_sched.c中定義了調度模塊的幾個重要的函數,我們來分析下源碼。
1.1 調度初始化函數
調度初始化函數UINT32 OsSchedInit(VOID)在任務初始化函數UINT32 OsTaskInit(VOID)中調用。⑴處會初始化任務就緒隊列,⑵處初始化任務排序鏈表,⑶處初始化調度響應時間全局變量爲最大值OS_SCHED_MAX_RESPONSE_TIME。
- UINT32 OsSchedInit(VOID)
- {
- UINT16 pri;
- ⑴ for (pri = 0; pri < OS_PRIORITY_QUEUE_NUM; pri++) {
- LOS_ListInit(&g_priQueueList[pri]);
- }
- g_queueBitmap = 0;
- ⑵ g_taskSortLinkList = OsGetSortLinkAttribute(OS_SORT_LINK_TASK);
- if (g_taskSortLinkList == NULL) {
- return LOS_NOK;
- }
- OsSortLinkInit(g_taskSortLinkList);
- ⑶ g_schedResponseTime = OS_SCHED_MAX_RESPONSE_TIME;
- return LOS_OK;
- }
1.2 任務調度函數
任務調度函數VOID LOS_Schedule(VOID)是出鏡率較高的一個函數。當系統完成初始化開始調度,並且沒有鎖任務調度時,會調用函數HalTaskSchedule()進行任務調度。該函數定義在kernel\arch\arm\cortex-m7\gcc\los_dispatch.S,由彙編語言實現,後文會詳細分析。
- VOID LOS_Schedule(VOID)
- {
- if (g_taskScheduled && LOS_CHECK_SCHEDULE) {
- HalTaskSchedule();
- }
- }
1.3 開啓調度函數
函數VOID OsSchedStart(VOID)被kernel\src\los_init.c:UINT32 LOS_Start(VOID)-->kernel\arch\arm\cortex-m7\gcc\los_context.c:UINT32 HalStartSchedule(OS_TICK_HANDLER handler)函數依次調用,在系統初始化時開啓任務調度。我們看下該函數的源碼,⑴處調用函數獲取就緒隊列中優先級最高的任務,⑵把該任務狀態設置爲運行狀態,接着把當前運行任務和新任務都設置爲就緒隊列中優先級最高的那個任務。⑶處設置任務調度啓動狀態全局變量爲1,標記任務調度已經開啓。⑷處設置新任務的開始運行時間,然後把新任務從就緒隊列中出隊。⑸處設置全局變量。⑹處調用函數設置該任務的運行過期時間。
- VOID OsSchedStart(VOID)
- {
- (VOID)LOS_IntLock();
- ⑴ LosTaskCB *newTask = OsGetTopTask();
- ⑵ newTask->taskStatus |= OS_TASK_STATUS_RUNNING;
- g_losTask.newTask = newTask;
- g_losTask.runTask = g_losTask.newTask;
- ⑶ g_taskScheduled = 1;
- ⑷ newTask->startTime = OsGetCurrSchedTimeCycle();
- OsSchedTaskDeQueue(newTask);
- ⑸ g_schedResponseTime = OS_SCHED_MAX_RESPONSE_TIME;
- g_schedResponseID = OS_INVALID;
- ⑹ OsSchedSetNextExpireTime(newTask->startTime, newTask->taskID, newTask->startTime + newTask->timeSlice);
- PRINTK("Entering scheduler\n");
- }
1.4 任務調度切換函數
任務切換函數用於實現任務切換,被文件kernel\arch\arm\cortex-m7\gcc\los_dispatch.S中的彙編函數HalPendSV調用。我們分析下該函數的源代碼。
⑴處獲取當前運行的任務,然後調用函數減去其運行的時間片,開始運行時間設置爲當前時間。⑵如果任務處於阻塞等待狀態或延遲狀態,則把其加入任務排序鏈表。⑶如果任務不是處於阻塞掛起狀態、不是處於阻塞狀態,則把其加入就緒隊列。⑷處獲取就緒隊列中優先級最高的任務,⑸處如果當前運行任務和就緒隊列彙總優先級最高的任務不是同一個任務,把當前任務狀態設置爲非運行狀態,新任務設置爲運行狀態,並設置新任務的開始時間爲當前任務的開始時間,然後執行⑹標記是否需要任務切換。⑺處把新任務從就緒隊列中出隊,⑻處計算新任務的運行結束時間,然後執行⑼設置任務到期時間。
- BOOL OsSchedTaskSwitch(VOID)
- {
- UINT64 endTime;
- BOOL isTaskSwitch = FALSE;
- ⑴ LosTaskCB *runTask = g_losTask.runTask;
- OsTimeSliceUpdate(runTask, OsGetCurrSchedTimeCycle());
- ⑵ if (runTask->taskStatus & (OS_TASK_STATUS_PEND_TIME | OS_TASK_STATUS_DELAY)) {
- OsAdd2SortLink(&runTask->sortList, runTask->startTime, runTask->waitTimes, OS_SORT_LINK_TASK);
- } else if (!(runTask->taskStatus & (OS_TASK_STATUS_PEND | OS_TASK_STATUS_SUSPEND | OS_TASK_STATUS_UNUSED))) {
- ⑶ OsSchedTaskEnQueue(runTask);
- }
- ⑷ LosTaskCB *newTask = OsGetTopTask();
- g_losTask.newTask = newTask;
- if (runTask != newTask) {
- #if (LOSCFG_BASE_CORE_TSK_MONITOR == 1)
- OsTaskSwitchCheck();
- #endif
- ⑸ runTask->taskStatus &= ~OS_TASK_STATUS_RUNNING;
- newTask->taskStatus |= OS_TASK_STATUS_RUNNING;
- newTask->startTime = runTask->startTime;
- ⑹ isTaskSwitch = TRUE;
- OsHookCall(LOS_HOOK_TYPE_TASK_SWITCHEDIN);
- }
- ⑺ OsSchedTaskDeQueue(newTask);
- ⑻ if (newTask->taskID != g_idleTaskID) {
- endTime = newTask->startTime + newTask->timeSlice;
- } else {
- endTime = OS_SCHED_MAX_RESPONSE_TIME;
- }
- ⑼ OsSchedSetNextExpireTime(newTask->startTime, newTask->taskID, endTime);
- return isTaskSwitch;
- }
2、調度模塊彙編函數
文件kernel\arch\arm\cortex-m7\gcc\los_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.h中的同名宏定義,數值也一樣,表示任務運行狀態,
2.1 HalStartToRun彙編函數
開始運行函數HalStartToRun被文件kernel\arch\arm\cortex-m7\gcc\los_context.c中的開始調度函數HalStartSchedule在系統啓動階段調用。我們接下來分析下該函數的彙編代碼。
⑴處設置PendSV異常優先級爲OS_NVIC_PENDSV_PRI,PendSV異常一般設置爲最低。⑵處往控制寄存器CONTROL寫入二進制的10,表示使用PSP棧,特權級的線程模式。⑶處把全局變量地址加載到寄存器r1。因爲UINT16 taskStatus是LosTaskCB結構體的第二個成員變量,⑷處[r1 , #4]把地址加4個字節來獲取當前運行任務的狀態,此時寄存器r0數值爲0x4,即就緒狀態OS_TASK_STATUS_READY。
⑸處把[r0]的值即任務的棧指針taskCB->stackPointer加載到寄存器R12,現在R12指向任務棧的棧指針,任務棧現在保存的是上下文,對應定義在kernel\arch\arm\cortex-m7\gcc\los_arch_context.h中的結構體TaskContext。如果支持浮點寄存器,則執行⑹,把R12加100個字節,其中包含S16到S31共16個4字節,R4到R11及uwPriMask共9個4字節的長度,執行指令後,R12指向任務棧中上下文的UINT32 uwR0位置。
⑺處代碼把任務棧上下文中的UINT32 uwR0-uwR3, UINT32 uwR12; UINT32 uwLR; UINT32 uwPC; UINT32 uwxPSR;共8個成員變量數值分別加載到寄存器R0-R7,其中R5對應UINT32 uwLR,R6對應UINT32 uwPC,此時寄存器R12指向任務棧上下文的UINT32 uwxPSR。然後執行下一個指令,指針繼續加72字節(=18個4字節長度),即對應S0到S15及UINT32 FPSCR; UINT32 NO_NAME等上下文的18個成員。此時,寄存器R12指向任務棧的棧底,緊接着執行⑻把寄存器R12寫入寄存器psp。
如果不支持浮點寄存器,則執行⑼,從棧指針加36字節,然後寄存器R12指向任務棧中上下文的UINT32 uwR0位置。接着把上下文中的寄存器信息加載到寄存器R0-R7,緊接着把寄存器R12寫入寄存器psp。
最後,執行⑽處指令,把寄存器R5寫入lr寄存器,開中斷,然後跳轉到R6對應的上下文的PC對應的函數VOID OsTaskEntry(UINT32 taskID),去執行任務的入口函數。
- .type HalStartToRun, %function
- .global HalStartToRun
- HalStartToRun:
- .fnstart
- .cantunwind
- ⑴ ldr r4, =OS_NVIC_SYSPRI2
- ldr r5, =OS_NVIC_PENDSV_PRI
- str r5, [r4]
- ⑵ mov r0, #2
- msr CONTROL, r0
- ⑶ ldr r1, =g_losTask
- ⑷ ldr r0, [r1, #4]
- ⑸ ldr r12, [r0]
- #if ((defined(__FPU_PRESENT) && (__FPU_PRESENT == 1U)) && \
- (defined(__FPU_USED) && (__FPU_USED == 1U)))
- ⑹ add r12, r12, #100
- ⑺ ldmfd r12!, {r0-r7}
- add r12, r12, #72
- ⑻ msr psp, r12
- vpush {S0}
- vpop {S0}
- #else
- ⑼ add r12, r12, #36
- ldmfd r12!, {r0-r7}
- msr psp, r12
- #endif
- ⑽ mov lr, r5
- //MSR xPSR, R7
- cpsie I
- bx r6
- .fnend
2.2 OsTaskSchedule彙編函數
彙編函數HalTaskSchedule實現新老任務的切換調度。從上文可以知道,被任務調度函數VOID LOS_Schedule(VOID)調用。我們看看這個彙編函數的源代碼,首先往中斷控制狀態寄存器OS_NVIC_INT_CTRL中的OS_NVIC_PENDSVSET位置1,觸發PendSV異常。執行完畢HalTaskSchedule函數,返回上層調用函數。PendSV異常的回調函數是HalPendSV彙編函數,下文會分析此函數。彙編函數HalTaskSchedule如下:
- .type HalTaskSchedule, %function
- .global HalTaskSchedule
- HalTaskSchedule:
- .fnstart
- .cantunwind
- ldr r0, =OS_NVIC_INT_CTRL
- ldr r1, =OS_NVIC_PENDSVSET
- str r1, [r0]
- dsb
- isb
- bx lr
- .fnend
3.4 HalPendSV彙編函數
接下來,我們分析下HalPendSV彙編函數的源代碼。⑴處把寄存器PRIMASK數值寫入寄存器r12,備份中斷的開關狀態,然後執行指令cpsid I屏蔽全局中斷。⑵處把寄存器r12、lr入棧,然後調用上文分析過的任務切換函數OsSchedTaskSwitch。函數執行完畢,執行⑶處指令出棧,恢復寄存器r12、lr數值。⑷處比較寄存器r0即任務切換函數OsSchedTaskSwitch的返回值與0,然後執行⑸使用r0寄存器保存lr寄存器的值,如果⑷處的比較不相等,則執行⑹跳轉到標籤TaskContextSwitch進行任務上下文切換。⑺處恢復中斷狀態,然後返回。
我們來看下需要任務上下文切換的情況,接着看標籤TaskContextSwitch。⑻處從r0寄存器恢復lr寄存器的值。⑼處使用r0寄存器指示棧指針,然後把寄存器r4-r12的數值壓入當前任務棧。如果支持浮點寄存器,還需要執行⑽,把寄存器d8-d15的數值壓入當前任務棧,r0爲任務棧指針。
⑾處指令把全局變量g_losTask地址加載到寄存器r5,⑿獲取當前運行任務的棧指針,然後更新當前運行任務的棧指針。⒀處指令獲取新任務newTask的地址,接着的指令把新任務地址賦值給當前運行任務,即runTask = newTask。⒁處指令把r1寄存器表示新任務的棧指針。如果支持浮點,⒂指令把新任務棧中的數據加載到寄存器d8-d15寄存器,繼續執行後續指令繼續加載數據到r4-r12寄存器,然後執行⒃處指令更新psp任務棧指針。⒄處指令恢復中斷狀態,然後執行跳轉指令,後續繼續執行C代碼VOID OsTaskEntry(UINT32 taskId)進入任務執行入口函數。
- .type HalPendSV, %function
- .global HalPendSV
- HalPendSV:
- .fnstart
- .cantunwind
- ⑴ mrs r12, PRIMASK
- cpsid I
- HalTaskSwitch:
- ⑵ push {r12, lr}
- blx OsSchedTaskSwitch
- ⑶ pop {r12, lr}
- ⑷ cmp r0, #0
- ⑸ mov r0, lr
- ⑹ bne TaskContextSwitch
- ⑺ msr PRIMASK, r12
- bx lr
- TaskContextSwitch:
- ⑻ mov lr, r0
- ⑼ mrs r0, psp
- stmfd r0!, {r4-r12}
- #if ((defined(__FPU_PRESENT) && (__FPU_PRESENT == 1U)) && \
- (defined(__FPU_USED) && (__FPU_USED == 1U)))
- ⑽ vstmdb r0!, {d8-d15}
- #endif
- ⑾ ldr r5, =g_losTask
- ⑿ ldr r6, [r5]
- str r0, [r6]
- ⒀ ldr r0, [r5, #4]
- str r0, [r5]
- ⒁ ldr r1, [r0]
- #if ((defined(__FPU_PRESENT) && (__FPU_PRESENT == 1U)) && \
- (defined(__FPU_USED) && (__FPU_USED == 1U)))
- ⒂ vldmia r1!, {d8-d15}
- #endif
- ldmfd r1!, {r4-r12}
- ⒃ msr psp, r1
- ⒄ msr PRIMASK, r12
- bx lr
- .fnend
小結
本文帶領大家一起剖析了鴻蒙輕內核調度模塊的源代碼,包含調用接口及底層的彙編函數實現。