LiteOS核心原始碼分析:任務LOS_Schedule

gamebus發表於2021-09-11
摘要:排程,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位置,如圖。

LiteOS核心原始碼分析:任務LOS_Schedule

⑹處程式碼把任務棧上下文中的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 。

 

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

相關文章