LiteOS內核源碼分析:任務LOS_Schedule


摘要:調度,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 。

 

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM