LiteOS內核源碼分析:任務LOS_Schedule

摘要:調度,Schedule也稱爲Dispatch,是操做系統的一個重要模塊,它負責選擇系統要處理的下一個任務。調度模塊須要協調處於就緒狀態的任務對資源的競爭,按優先級策略從就緒隊列中獲取高優先級的任務,給予資源使用權。

本文分享自華爲雲社區《LiteOS內核源碼分析系列六 -任務及調度(5)-任務LOS_Schedule》,原文做者:zhushy 。git

本文咱們來一塊兒學習下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中定義的宏函數、枚舉、和內聯函數。學習

一、調度模塊宏函數和內聯函數

kernel\base\include\los_sched_pri.h定義的宏函數、枚舉、內聯函數。ui

1.1 宏函數和枚舉

UINT32 g_taskScheduled是kernel\base\los_task.c定義的全局變量,標記內核是否開啓調度,每一位表明不一樣的CPU核的調度開啓狀態。url

⑴處定義的宏函數OS_SCHEDULER_SET(cpuid)開啓cpuid核的調度。⑵處宏函數OS_SCHEDULER_CLR(cpuid)是前者的反向操做,關閉cpuid核的調度。⑶處宏判斷當前核是否開啓調度。⑷處的枚舉用於標記是否發起了請求調度。當須要調度,又暫不具有調度條件的時候,標記下狀態,等具有調度的條件時,再去調度。spa

⑴  #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。.net

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();
}

二、調度模塊經常使用接口

這一小節,咱們看看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

三、調度模塊彙編函數

文件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 

點擊關注,第一時間瞭解華爲雲新鮮技術~

相關文章
相關標籤/搜索