FreeRTOS 任務(wù)調(diào)度 任務(wù)切換

@(嵌入式)

Freertos

FreeRtos

簡述

前面文章 < FreeRTOS 任務(wù)調(diào)度 任務(wù)創(chuàng)建 > 介紹了 FreeRTOS 中如何創(chuàng)建任務(wù)以及其具體實現(xiàn)。
一般來說, 我們會在程序開始先創(chuàng)建若干個任務(wù), 而此時任務(wù)調(diào)度器還沒又開始運行,因此每一次任務(wù)創(chuàng)建后都會依據(jù)其優(yōu)先級插入到就緒鏈表,同時保證全局變量 pxCurrentTCB 指向當(dāng)前創(chuàng)建的所有任務(wù)中優(yōu)先級最高的一個,但是任務(wù)還沒開始運行。
當(dāng)初始化完畢后,調(diào)用函數(shù) vTaskStartScheduler啟動任務(wù)調(diào)度器開始開始調(diào)度,此時,pxCurrentTCB所指的任務(wù)才開始運行。
所以, 本章,介紹任務(wù)調(diào)度器啟動以及如何進(jìn)行任務(wù)切換。

調(diào)度器涉及平臺底層硬件操作,本文以Cotex-M3 架構(gòu)為例, 具體可以參考 《Cortex-M3權(quán)威指南》(文末附)

分析的源碼版本是 v9.0.0
(為了方便查看,github 上保留了一份源碼Source目錄下的拷貝)

啟動調(diào)度器

創(chuàng)建任務(wù)后,系統(tǒng)不會自動啟動任務(wù)調(diào)度器,需要用戶調(diào)用函數(shù) vTaskStartScheduler 啟動調(diào)度器。 該函數(shù)被調(diào)用后,會先創(chuàng)建系統(tǒng)自己需要用到的任務(wù),比如空閑任務(wù) prvIdleTask,定時器管理的任務(wù)等。 之后, 調(diào)用移植層提供的函數(shù) xPortStartScheduler
代碼解析如下,

void vTaskStartScheduler( void )
{
    BaseType_t xReturn;
    #if( configSUPPORT_STATIC_ALLOCATION == 1 )
    {
        // 采用靜態(tài)內(nèi)存創(chuàng)建空閑任務(wù)
        StaticTask_t *pxIdleTaskTCBBuffer = NULL;
        StackType_t *pxIdleTaskStackBuffer = NULL;
        uint32_t ulIdleTaskStackSize;
        // 獲取靜態(tài)內(nèi)存地址/參數(shù)
        vApplicationGetIdleTaskMemory(
            &pxIdleTaskTCBBuffer, 
            &pxIdleTaskStackBuffer, 
            &ulIdleTaskStackSize );
        // 創(chuàng)建任務(wù)
        // 空閑任務(wù)優(yōu)先級為 0, 也就是其優(yōu)先級最低
        // !! 但是, 設(shè)置了特權(quán)位, 所以其運行在 特權(quán)模式
        xIdleTaskHandle = xTaskCreateStatic(prvIdleTask, "IDLE", 
            ulIdleTaskStackSize, (void *) NULL, 
            (tskIDLE_PRIORITY | portPRIVILEGE_BIT), 
            pxIdleTaskStackBuffer,
            pxIdleTaskTCBBuffer); 
        
        if( xIdleTaskHandle != NULL )
        {
            xReturn = pdPASS;
        }
        else
        {
            xReturn = pdFAIL;
        }
    }
    #else
    {
        // 動態(tài)申請內(nèi)存創(chuàng)建任務(wù)
        xReturn = xTaskCreate(prvIdleTask,
            "IDLE", configMINIMAL_STACK_SIZE,
            (void *)NULL,
            (tskIDLE_PRIORITY | portPRIVILEGE_BIT),
            &xIdleTaskHandle );     
    }
    #endif
    
    // 如果工程使用了軟件定時器, 需要創(chuàng)建定時器任務(wù)進(jìn)行管理
    #if ( configUSE_TIMERS == 1 )
    {
        if( xReturn == pdPASS )
        {
            xReturn = xTimerCreateTimerTask();
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }
    }
    #endif
    
    if( xReturn == pdPASS )
    {
        
        // 關(guān)閉中斷, 避免調(diào)度器運行前節(jié)拍定時器產(chǎn)生中斷
        // 中斷在第一個任務(wù)啟動時恢復(fù)
        portDISABLE_INTERRUPTS();
        
        #if ( configUSE_NEWLIB_REENTRANT == 1 )
        {
            // 如果使用了這個庫
            // 更新第一個任務(wù)的的指針到全局變量
            _impure_ptr = &( pxCurrentTCB->xNewLib_reent );
        }
        #endif
        
        // 初始化變量
        xNextTaskUnblockTime = portMAX_DELAY;
        xSchedulerRunning = pdTRUE;
        xTickCount = ( TickType_t ) 0U;
        
        // 如果啟動統(tǒng)計任務(wù)運行時間, 宏 configGENERATE_RUN_TIME_STATS = 1
        // 需要定義以下宏, 初始化一個定時器用于該功能 
        portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();

        // 設(shè)置系統(tǒng)節(jié)拍計數(shù)器, 啟動任務(wù)
        // 硬件相關(guān), 由系統(tǒng)移植層提供, 下面介紹
        if( xPortStartScheduler() != pdFALSE )
        {
            // 不會運行到這里, 如果調(diào)度器運行正常
        }
        else
        {
            // 當(dāng)調(diào)用 xTaskEndScheduler()才會來到這里
        }
    }
    else
    {
        // 內(nèi)存不足,創(chuàng)建空閑任務(wù)/定時任務(wù)失敗, 調(diào)度器啟動失敗
        configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
    }

    // 預(yù)防編譯器警告
    ( void ) xIdleTaskHandle;
}

移植層調(diào)度器

上面提到, 創(chuàng)建系統(tǒng)所需任務(wù)和初始化相關(guān)靜態(tài)變量后, 系統(tǒng)調(diào)用了 xPortStartScheduler設(shè)置節(jié)拍定時器和啟動第一個任務(wù),開始系統(tǒng)正常運行調(diào)度。 而對于不同架構(gòu)平臺,該函數(shù)的實現(xiàn)可能存在不同,以下, 拿比較常用的 Cotex-M3 架構(gòu)舉例。
對于 M3, 可以在源碼目錄下 /Source/portable/GCC/ARM_CM3/port.c 看到該函數(shù)的實現(xiàn)。

與 FreeRTOS 任務(wù)優(yōu)先級相反, Cotex-M3 優(yōu)先級值越小, 優(yōu)先級越高。 Cotex-M3的優(yōu)先級配置寄存器考慮器件移植而向高位對齊,實際可用的 CPU 會裁掉表達(dá)優(yōu)先級低端的有效位,以減少優(yōu)先級數(shù)。 舉例子說, 加入平臺支持3bit 表示優(yōu)先級,則其優(yōu)先級配置寄存器的高三位可以編程寫入,其他位被屏蔽,不管寫入何值,重新讀回都是0。
另外提供搶占優(yōu)先級和子優(yōu)先級分段配置相關(guān),詳細(xì)閱讀 《Cortex-M3權(quán)威指南》

在系統(tǒng)調(diào)度過程中,主要涉及到的三個異常:

  • SVC 系統(tǒng)服務(wù)調(diào)用
    操作系統(tǒng)通常不讓用戶程序直接訪問硬件,而是通過提供一些系統(tǒng)服務(wù)函數(shù)。 這里主要觸發(fā)后,在異常服務(wù)中啟動第一個任務(wù)
  • PendSV 可懸起系統(tǒng)調(diào)用
    相比 SVC, PenndSV 異常后可能不會馬上響應(yīng), 等到其他高優(yōu)先級中斷處理后才響應(yīng)。 用于上下文切換,同時保證其他中斷可以被及時響應(yīng)處理。
  • SysTick 節(jié)拍定時器
    在沒有高優(yōu)先級任務(wù)強(qiáng)制下,同優(yōu)先級任務(wù)按時間片輪流執(zhí)行,每次SysTick中斷,下一個任務(wù)將獲得一個時間片。
BaseType_t xPortStartScheduler( void )
{
    configASSERT( configMAX_SYSCALL_INTERRUPT_PRIORITY );
    #if( configASSERT_DEFINED == 1 )
    {
        volatile uint32_t ulOriginalPriority;
        // 取出中斷優(yōu)先級寄存器
        volatile uint8_t * const pucFirstUserPriorityRegister = 
            (volatile uint8_t * const) (portNVIC_IP_REGISTERS_OFFSET_16 +
                        portFIRST_USER_INTERRUPT_NUMBER);
        volatile uint8_t ucMaxPriorityValue;

        // 保存原有優(yōu)先級寄存器值
        ulOriginalPriority = *pucFirstUserPriorityRegister;

        // 判斷平臺支持優(yōu)先級位數(shù)
        // 先全寫 1
        *pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;
        // 重新讀回, 不能設(shè)置的位依然是 0
        ucMaxPriorityValue = *pucFirstUserPriorityRegister;
        // 確保用戶設(shè)置優(yōu)先級不會超出范圍
        ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue;
        
        // 判斷有幾個1, 得到對應(yīng)優(yōu)先級數(shù)最大值
        ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS;
        while( ( ucMaxPriorityValue & portTOP_BIT_OF_BYTE ) == portTOP_BIT_OF_BYTE )
        {
            ulMaxPRIGROUPValue--;
            ucMaxPriorityValue <<= ( uint8_t ) 0x01;
        }
        ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT;
        ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK;
        
        // 恢復(fù)優(yōu)先級配置寄存器值
        *pucFirstUserPriorityRegister = ulOriginalPriority;
    }
    #endif /* conifgASSERT_DEFINED */

    // 設(shè)置 PendSV 和 SysTIck 異常優(yōu)先級最低
    // 保證系統(tǒng)會話切換不會阻塞系統(tǒng)其他中斷的響應(yīng)
    portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
    portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;

    // 初始化系統(tǒng)節(jié)拍定時器
    vPortSetupTimerInterrupt();
    // 初始化邊界嵌套計數(shù)器
    uxCriticalNesting = 0;

    // 觸發(fā) svc 異常 啟動第一個任務(wù)
    prvPortStartFirstTask();

    /* Should not get here! */
    prvTaskExitError();
    return 0;
}

啟動第一個任務(wù)

函數(shù)中調(diào)用了 prvPortStartFirstTask 來啟動第一個任務(wù), 該函數(shù)重新初始化了系統(tǒng)的棧指針,表示 FreeRtos 開始接手平臺的控制, 同時通過觸發(fā) SVC 系統(tǒng)調(diào)用,運行第一個任務(wù)。具體實現(xiàn)如下

static void prvPortStartFirstTask( void )
{
    __asm volatile(
    " ldr r0, =0xE000ED08   \n" /*向量表偏移寄存器地址 CotexM3*/
    " ldr r0, [r0]          \n" /*取向量表地址*/
    " ldr r0, [r0]          \n" /*取 MSP 初始值*/
    /*重置msp指針 宣示 系統(tǒng)接管*/
    " msr msp, r0           \n"
    " cpsie i               \n" /*開中斷*/
    " cpsie f               \n" /*開異常*/
    /*流水線相關(guān)*/
    " dsb                   \n" /*數(shù)據(jù)同步隔離*/
    " isb                   \n" /*指令同步隔離*/
    /*觸發(fā)異常 啟動第一個任務(wù)*/
    " svc 0                 \n"
    " nop                   \n"
    );
}

前面創(chuàng)建任務(wù)的文章介紹過, 任務(wù)創(chuàng)建后, 對其棧進(jìn)行了初始化,使其看起來和任務(wù)運行過后被系統(tǒng)中斷切換了一樣。 所以,為了啟動第一個任務(wù),觸發(fā) SVC 異常后,異常處理函數(shù)中直接執(zhí)行現(xiàn)場恢復(fù), 把 pxCurrentTCB "恢復(fù)"到運行狀態(tài)。

(另外,Cotex-M3 具有三級流水線,所以切換任務(wù)的時候需要清除預(yù)取的指令,避免錯誤。)

對于 Cotex-M3 , 其代碼實現(xiàn)如下,

void vPortSVCHandler( void )
{
    __asm volatile (
    /*取 pxCurrentTCB 的地址*/
    "ldr r3, pxCurrentTCBConst2      \n"
    /*取出 pxCurrentTCB 的值 : TCB 地址*/
    "ldr r1, [r3]                    \n" 
    /*取出 TCB 第一項 : 任務(wù)的棧頂 */
    "ldr r0, [r1]                   \n"
    /*恢復(fù)寄存器數(shù)據(jù)*/
    "ldmia r0!, {r4-r11}            \n" 
    /*設(shè)置線程指針: 任務(wù)的棧指針*/
    "msr psp, r0                    \n" 
    /*流水線清洗*/
    "isb                            \n"
    "mov r0, #0                     \n"
    "msr    basepri, r0             \n"
    /*設(shè)置返回后進(jìn)入線程模式*/
    "orr r14, #0xd                  \n"
    "bx r14                         \n"
    "                               \n"
    ".align 4               \n"
    "pxCurrentTCBConst2: .word pxCurrentTCB     \n"
    );
}

異常返回后, 系統(tǒng)進(jìn)入線程模式, 自動從堆?;謴?fù)PC等寄存器,而由于此時棧指針已經(jīng)更新指向?qū)?yīng)準(zhǔn)備運行任務(wù)的棧,所以,程序會從該任務(wù)入口函數(shù)開始執(zhí)行。
到此, 第一個任務(wù)啟動。

前面提到, 第一個任務(wù)啟動通過 SVC 異常, 而后續(xù)的任務(wù)切換, 使用的是 PendSV 異常, 而其對應(yīng)的服務(wù)函數(shù)是 xPortPendSVHandler。 后續(xù)介紹任務(wù)切換再分析。

任務(wù)切換

FreeRTOS 支持時間片輪序和優(yōu)先級搶占。系統(tǒng)調(diào)度器通過調(diào)度算法確定當(dāng)前需要獲得CPU 使用權(quán)的任務(wù)并讓其處于運行狀態(tài)。對于嵌入式系統(tǒng),某些任務(wù)需要獲得快速的響應(yīng),如果使用時間片,該任務(wù)可能無法及時被運行,因此搶占調(diào)度是必須的,高優(yōu)先級的任務(wù)一旦就緒就能及時運行;而對于同優(yōu)先級任務(wù),系統(tǒng)根據(jù)時間片調(diào)度,給予每個任務(wù)相同的運行時間片,保證每個任務(wù)都能獲得CPU 。

  1. 最高優(yōu)先級任務(wù) Task 1 運行,直到其被阻塞或者掛起釋放CPU
  2. 就緒鏈表中最高優(yōu)先級任務(wù)Task 2 開始運行, 直到...
    1. 調(diào)用接口進(jìn)入阻塞或者掛起狀態(tài)
    2. 任務(wù) Task 1 恢復(fù)并搶占 CPU 使用權(quán)
    3. 同優(yōu)先級任務(wù)TASK 3 就緒,時間片調(diào)度
  3. 沒有用戶任務(wù)執(zhí)行,運行系統(tǒng)空閑任務(wù)。

FreeRTOS 在兩種情況下執(zhí)行任務(wù)切換:

  1. 同等級任務(wù)時間片用完,提前掛起觸發(fā)切換
    在 SysTick 節(jié)拍計數(shù)器中斷中觸發(fā)異常
  2. 高優(yōu)先任務(wù)恢復(fù)就緒(如信號量,隊列等阻塞、掛起狀態(tài)下退出)時搶占
    最終都是通過調(diào)用移植層提供的 portYIELD() 宏懸起 PendSV 異常

但是無論何種情況下,都是通過觸發(fā)系統(tǒng) PendSV 異常,在該服務(wù)程序中完成切換。
使用該異常切換上下文的原因是保證切換不會影響到其他中斷的及時響應(yīng)(切換上下文搶占了 ISR 的執(zhí)行,延時時間不可預(yù)知,對于實時系統(tǒng)是無法容忍的),在SysTick 中或其他需要進(jìn)行任務(wù)切換的地方懸起一個 PendSV 異常,系統(tǒng)會直到其他所有 ISR 都完成處理后才執(zhí)行該異常的服務(wù)程序,進(jìn)行上下文切換。

系統(tǒng)響應(yīng) PendSV 異常,在該中斷服務(wù)程序中,保存當(dāng)前任務(wù)現(xiàn)場, 選擇切換的下一個任務(wù),進(jìn)行任務(wù)切換,退出異常恢復(fù)線程模式運行新任務(wù),完成任務(wù)切換。

以下是 Cotex-M3 的服務(wù)程序,
首先先要明確的是,系統(tǒng)進(jìn)入異常處理程序的時候,使用的是主堆棧指針 MSP, 而一般情況下運行任務(wù)使用的線程模式使用的是進(jìn)程堆棧指針 PSP。后者使用是系統(tǒng)設(shè)置的,前者是硬件強(qiáng)制設(shè)置的。
對應(yīng)這兩個指針,系統(tǒng)有兩種堆棧,系統(tǒng)內(nèi)核和異常程序處理使用的是主堆棧,MSP 指向其棧頂。而對應(yīng)而不同任務(wù),我們在創(chuàng)建時為其分配了空間,作為該任務(wù)的堆棧,在該任務(wù)運行時,由系統(tǒng)設(shè)置進(jìn)程堆棧 PSP 指向該棧頂。
如下分析該服務(wù)函數(shù)的執(zhí)行:

void xPortPendSVHandler( void )
{
    /* This is a naked function. */
    __asm volatile
    (
    /*取出當(dāng)前任務(wù)的棧頂指針 也就是 psp -> R0*/
    "   mrs r0, psp                         \n"
    "   isb                                 \n"
    "                                       \n"
    /*取出當(dāng)前任務(wù)控制塊指針 -> R2*/
    "   ldr r3, pxCurrentTCBConst           \n"
    "   ldr r2, [r3]                        \n"
    "                                       \n"
    /*R4-R11 這些系統(tǒng)不會自動入棧,需要手動推到當(dāng)前任務(wù)的堆棧*/
    "   stmdb r0!, {r4-r11}                 \n"
    /*最后,保存當(dāng)前的棧頂指針 
    R0 保存當(dāng)前任務(wù)棧頂?shù)刂?    [R2] 是 TCB 首地址,也就是 pxTopOfStack
    下次,任務(wù)激活可以重新取出恢復(fù)棧頂,并取出其他數(shù)據(jù)
    */
    "   str r0, [r2]                        \n"
    "                                       \n"
    /*保護(hù)現(xiàn)場,調(diào)用函數(shù)更新下一個準(zhǔn)備運行的新任務(wù)*/
    "   stmdb sp!, {r3, r14}                \n"
    /*設(shè)置優(yōu)先級 第一個參數(shù),
    即:configMAX_SYSCALL_INTERRUPT_PRIORITY
    進(jìn)入臨界區(qū)*/
    "   mov r0, %0                          \n"
    "   msr basepri, r0                     \n"
    "   bl vTaskSwitchContext               \n"
    "   mov r0, #0                          \n"
    "   msr basepri, r0                     \n"
    "   ldmia sp!, {r3, r14}                \n"
    "                                       \n"
    /*函數(shù)返回 退出臨界區(qū)
    pxCurrentTCB 指向新任務(wù)
    取出新的 pxCurrentTCB 保存到 R1
    */
    "   ldr r1, [r3]                        \n"
    /*取出新任務(wù)的棧頂*/
    "   ldr r0, [r1]                        \n"
    /*恢復(fù)手動保存的寄存器*/
    "   ldmia r0!, {r4-r11}                 \n"
    /*設(shè)置線程指針 psp 指向新任務(wù)棧頂*/
    "   msr psp, r0                         \n"
    "   isb                                 \n"
    /*返回, 硬件執(zhí)行現(xiàn)場恢復(fù)
    開始執(zhí)行任務(wù)
    */
    "   bx r14                              \n"
    "                                       \n"
    "   .align 4                            \n"
    "pxCurrentTCBConst: .word pxCurrentTCB  \n"
    ::"i"(configMAX_SYSCALL_INTERRUPT_PRIORITY)
    );
}

在服務(wù)程序中,調(diào)用了函數(shù) vTaskSwitchContext 獲取新的運行任務(wù), 該函數(shù)會更新當(dāng)前任務(wù)運行時間,檢查任務(wù)堆棧使用是是否溢出,然后調(diào)用宏 taskSELECT_HIGHEST_PRIORITY_TASK()設(shè)置新的任務(wù)。該宏實現(xiàn)分兩種情況,普通情況下使用的定義如下

UBaseType_t uxTopPriority = uxTopReadyPriority;
while(listLIST_IS_EMPTY(&(pxReadyTasksLists[uxTopPriority])))
{
    --uxTopPriority;
}
    
listGET_OWNER_OF_NEXT_ENTRY(pxCurrentTCB, 
    &(pxReadyTasksLists[ uxTopPriority]));

uxTopReadyPriority = uxTopPriority;

通過 while 查找當(dāng)前存在就緒任務(wù)的最高優(yōu)先級鏈表,獲取鏈表項設(shè)置任務(wù)指針。(通一個鏈表內(nèi)多個項目通過指針循環(huán),實現(xiàn)同優(yōu)先級任務(wù)獲得相同時間片執(zhí)行)。

而另外一種方式,需要平臺支持,主要差別是查找最高任務(wù)優(yōu)先級,平臺支持利用平臺特性,效率會更高,但是移植性就不好說了。

發(fā)生異常跳轉(zhuǎn)到異常處理服務(wù)前,自動執(zhí)行的現(xiàn)場保護(hù)會保留返回模式(線程模式),使用堆棧指針等信息,所以,結(jié)束任務(wù)切換, 通過執(zhí)行 bx r14返回,系統(tǒng)會自動恢復(fù)現(xiàn)場(From stack),開始運行任務(wù)。

至此,任務(wù)切換完成。

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 又來到了一個老生常談的問題,應(yīng)用層軟件開發(fā)的程序員要不要了解和深入學(xué)習(xí)操作系統(tǒng)呢? 今天就這個問題開始,來談?wù)劜?..
    tangsl閱讀 4,311評論 0 23
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,506評論 19 139
  • 一、溫故而知新 1. 內(nèi)存不夠怎么辦 內(nèi)存簡單分配策略的問題地址空間不隔離內(nèi)存使用效率低程序運行的地址不確定 關(guān)于...
    SeanCST閱讀 8,107評論 0 27
  • 一、你的痛點 日常生活中,大家都有這樣的痛點:別人說什么,都略懂一點。但無法系統(tǒng)地整理出來,怎么辦?是因為沒...
    幸福布道者閱讀 285評論 1 2
  • 忘了有多久沒有這樣靜心思考的時光了。有時,不是為了寫好什么東西,只是覺得,記錄我的思考,哪怕只是想象。書寫我的心聲...
    鑒空閱讀 146評論 0 0

友情鏈接更多精彩內(nèi)容