【freertos】006-任務切換實現細節

李柱明發表於2022-03-31

前言

任務排程實現的兩個核心:

  • 排程器實現;(上一章節已描述排程基礎)

  • 任務切換實現。

    • 介面層實現。

原文:李柱明部落格:https://www.cnblogs.com/lizhuming/p/16080202.html

6.1 任務切換基礎

任務切換就是在就緒列表中尋找優先順序最高的就緒任務,然後去執行該任務。

任務切換有兩種方法:

  1. 手動:taskYIELD(),呼叫該API,強制觸發任務切換。在中斷中強制任務切換呼叫portYIELD_FROM_ISR()
  2. 系統:系統節拍時鐘中斷,在該中斷回撥裡會檢查是否觸發任務切換。

任務切換的大概內容:

  1. 儲存上文。
  2. 恢復下文。

重點:上述中不管是系統還是手動觸發切換任務,都只是觸發而已,最終還是根據就緒表中最高優先順序任務更新到pxCurrentTCB變數,然後切換到pxCurrentTCB指向的任務。


任務切換設計介面層,會分兩條主線分析:posix和cortex m


6.2 posix任務切換

任務切換原理都一樣,都是暫停當前在跑的任務(儲存上文),去跑下一個需要跑的任務(恢復下文)。

只是介面層不一樣,實現的方式也不一樣。

posix模擬器實現任務切換比較簡單,任務切換介面層相關的都是基於posix執行緒實現,利用訊號實現任務啟停。

posix標準下,任務切換實現如下:

  1. 進出臨界,通過pthread_sigmask()這個API實現遮蔽和解除遮蔽執行緒部分訊號。
  2. 找出當前任務,即當前執行態的任務的執行緒控制程式碼。
  3. 通過vTaskSwitchContext()找出下一個需要跑的任務。該API內部實現最主要的目的是按照排程器邏輯找出下一個需要執行的任務更新到pxCurrentTCB值。
  4. 呼叫prvSwitchThread()切換執行緒,發訊號恢復需要跑的執行緒,讓其解除阻塞。如果需要掛起的執行緒還沒有標記結束,就進入阻塞,等待執行緒訊號來解除阻塞。如果需要掛起的訊號已經標記消亡,則直接呼叫pthread_exit()結束該執行緒。
void vPortYield( void )
{
    /* 進入臨界 */
    vPortEnterCritical();
    /* 切換任務 */
    vPortYieldFromISR();
    /* 退出臨界 */
    vPortExitCritical();
}
void vPortYieldFromISR( void )
{
    Thread_t *xThreadToSuspend;
    Thread_t *xThreadToResume;
    /* 獲取當前執行緒控制程式碼 */
    xThreadToSuspend = prvGetThreadFromTask( xTaskGetCurrentTaskHandle() );
    /* 任務切換處理,更新pxCurrentTCB值 */
    vTaskSwitchContext();
    /* 獲取下一個需要跑的執行緒控制程式碼 */
    xThreadToResume = prvGetThreadFromTask( xTaskGetCurrentTaskHandle() );
    /* 切換進去 */
    prvSwitchThread( xThreadToResume, xThreadToSuspend );
}

6.3 cortex m3任務切換

不管是手動還是系統觸發任務切換,其任務切換都是在PendSV異常回撥中實現。

切換任務過程:

  1. 觸發任務切換異常後,部分CPU暫存器硬體使用PSP壓棧:xPSR、PC、LR、R12、R3-R0。
  2. 進入異常後,CPU使用MSP。
  3. 把剩餘部分暫存器R11-R4,通過軟體使用PSP壓棧。
  4. 進入臨界區。
  5. 呼叫vTaskSwitchContext()函式找出下一個要執行的任務更新到pxCurrentTCB
  6. 退出臨界。
  7. 通過pxCurrentTCB獲取到新的任務棧頂。
  8. 使用新的任務棧頂指標出棧R11-R4。
  9. 更新當前任務棧頂指標到PSP。
  10. 退出異常,硬體使用PSP出棧xPSR、PC、LR、R12、R3-R0。
  11. 進入新的任務了。

程式碼實現參考:

__asm void xPortPendSVHandler(void)
{
    extern uxCriticalNesting;
    extern pxCurrentTCB; /* 指向當前啟用的任務 */
    extern vTaskSwitchContext;

    PRESERVE8

    mrs r0, psp     /* PSP內容存入R0 */
    isb /* 指令同步隔離,清流水線 */

    ldr r3, = pxCurrentTCB /* 當前啟用的任務TCB指標存入R2 */
    ldr r2,[r3]

    stmdb r0 !,{r4 - r11} /* 儲存剩餘的暫存器,異常處理程式執行前,硬體自動將xPSR、PC、LR、R12、R0-R3入棧 */
    str r0,[r2] /* 將新的棧頂儲存到任務TCB的第一個成員中 */

    stmdb sp !,{r3, r14} /* 將R3和R14臨時壓入堆疊,因為即將呼叫函式vTaskSwitchContext,呼叫函式時,返回地址自動儲存到R14中,所以一旦呼叫發生,R14的值會被覆蓋,因此需要入棧保護; R3儲存的當前啟用的任務TCB指標(pxCurrentTCB)地址,函式呼叫後會用到,因此也要入棧保護*/
    mov r0,#configMAX_SYSCALL_INTERRUPT_PRIORITY /* 進入臨界區 */
    msr basepri,r0
    dsb /* 資料和指令同步隔離 */
    isb
    bl vTaskSwitchContext /* 呼叫函式,尋找新的任務執行,通過使變數pxCurrentTCB指向新的任務來實現任務切換 */
    mov r0,#0 /* 退出臨界區*/
    msr basepri,r0
    ldmia sp !,
    {r3, r14} /* 恢復R3和R14*/

    ldr r1,[r3] 
    ldr r0, [r1] /* 當前啟用的任務TCB第一項儲存了任務堆疊的棧頂,現在棧頂值存入R0*/
    ldmia r0 !,{r4 - r11} /* 出棧*/
    msr psp,r0
    isb
    bx r14 /* 異常發生時,R14中儲存異常返回標誌,包括返回後進入執行緒模式還是處理器模式、使用PSP堆疊指標還是MSP堆疊指標,當呼叫 bx r14指令後,硬體會知道要從異常返回,然後出棧,這個時候堆疊指標PSP已經指向了新任務堆疊的正確位置,當新任務的執行地址被出棧到PC暫存器後,新的任務也會被執行。*/
    nop
}

6.4 任務切換:vTaskSwitchContext()

不同的介面層實現任務切換,都需要呼叫核心層vTaskSwitchContext()檢索出新的的pxCurrentTCB值,並在介面層切進去。

6.4.1 檢查排程器狀態

切換任務時,需要檢查排程器是否正常,正常才會檢索出新的任務到pxCurrentTCB

如果排程器被掛起,標記下xYieldPendingpdTRUE

xYieldPending這個標記表示,在恢復排程器或下次系統節拍時(排程器已恢復正常)情況下,觸發一次上下文切換。

if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE ) /* 掛起排程器就不允許任務切換. */
{
    /* 帶中斷保護的API函式的都會有一個引數"xHigherPriorityTaskWoken",若是使用者沒有使用這個引數,這裡設定任務切換標誌。在下個系統中斷服務例程中,會檢查xYieldPending的值,若是為pdTRUE則會觸發一次上下文切換。*/
    xYieldPending = pdTRUE;
}

如果排程器正常,便需要標記xYieldPendingpdFALSE,表示下次觸發任務切換不需要檢查該值進行強制切換。

6.4.2 任務執行時間統計處理

如果開啟了configGENERATE_RUN_TIME_STATS巨集,表示開啟了任務執行時間統計。

任務執行的時間統計在任務切換時處理,其簡要原理是在任務切入時開始計時,任務切出時結束本次任務執行計時,把執行時長累加到pxCurrentTCB->ulRunTimeCounter記錄下來。

注意,這裡的時間值不要和系統節拍混淆,這兩個時間值在兩個獨立的時間域裡各自維護的。

獲取當前時間值的函式由使用者實現(因為這個時間域提供的時間系統是由使用者指定實現的),通過下面兩個巨集函式之一實現獲取當前時間值:

  1. portALT_GET_RUN_TIME_COUNTER_VALUE()
  2. portGET_RUN_TIME_COUNTER_VALUE()

切出舊任務時,把舊任務本次跑的時間累加到pxCurrentTCB->ulRunTimeCounter

同時,切入新的任務時,儲存下切入任務時的時間點到ulTaskSwitchedInTime,用於切出統計時間。

綜上可得:

/* 任務執行時間統計功能 */
#if ( configGENERATE_RUN_TIME_STATS == 1 )
{
    /* 獲取當前時間值。注意,這裡的時間值不要和系統節拍混淆,這兩個時間值在兩個獨立的時間域裡各自維護的。 */
    #ifdef portALT_GET_RUN_TIME_COUNTER_VALUE
        portALT_GET_RUN_TIME_COUNTER_VALUE( ulTotalRunTime );
    #else
        ulTotalRunTime = portGET_RUN_TIME_COUNTER_VALUE();
    #endif

    /* 將任務執行的時間新增到到目前為止的累計時間中。
    	任務開始執行的時間儲存在ulTaskSwitchedInTime中。
    	注意,這裡沒有溢位保護,所以計數值只有在計時器溢位之前才有效。
    	對負值的防範是為了防止可疑的執行時統計計數器實現——這些實現是由應用程式而不是核心提供的。*/ */
    if( ulTotalRunTime > ulTaskSwitchedInTime )
    {
        pxCurrentTCB->ulRunTimeCounter += ( ulTotalRunTime - ulTaskSwitchedInTime );
    }
    else
    {
        mtCOVERAGE_TEST_MARKER();
    }
				/* 儲存當前時間 */
    ulTaskSwitchedInTime = ulTotalRunTime;
}
#endif /* configGENERATE_RUN_TIME_STATS */

6.4.3 棧溢位檢查

任務切換時會對任務棧進行檢查,是否溢位或者是否被踩。

/* 棧溢位檢查 */
taskCHECK_FOR_STACK_OVERFLOW();

有兩種方案可檢查棧溢位,可同時使用:(以堆疊向下生長為例)

  1. 方案1:檢查任務棧頂指標。如果任務上文壓棧後,任務棧頂pxCurrentTCB->pxTopOfStack比棧起始pxCurrentTCB->pxStack還小,說明已經棧溢位了。

  2. 方案2:棧起始內容檢查。初始化時,把任務棧其實pxCurrentTCB->pxStack一部分棧記憶體初始化為特定的值。在每次任務切換時,檢查下這幾個值是否為原有值,如果不是,說明被踩棧了;如果不是,可初步判斷任務戰安全(不能絕對判斷當前任務棧安全)。

    • 這部分內容需要使用者在vApplicationStackOverflowHook()內實現。

參考程式碼:(例子方案的條件可以結合使用)

  • portSTACK_LIMIT_PADDING值用於偏移,縮少任務棧安全範圍。
  • 方案1:檢查任務棧頂指標。
#if ( ( configCHECK_FOR_STACK_OVERFLOW == 1 ) && ( portSTACK_GROWTH < 0 )  /* 向下生長 */
#define taskCHECK_FOR_STACK_OVERFLOW()                                                            \
{                                                                                                 \
    /* 當前儲存的堆疊指標是否在堆疊限制內 */                            \
    if( pxCurrentTCB->pxTopOfStack <= pxCurrentTCB->pxStack + portSTACK_LIMIT_PADDING )           \
    {                                                                                             \
        vApplicationStackOverflowHook( ( TaskHandle_t ) pxCurrentTCB, pxCurrentTCB->pcTaskName ); \
    }                                                                                             \
}
#ednif
  • 方案2:棧起始內容檢查。
#if ( ( configCHECK_FOR_STACK_OVERFLOW == 1 ) && ( portSTACK_GROWTH < 0 )  /* 向下生長 */
#define taskCHECK_FOR_STACK_OVERFLOW()  \
{   \
    /* 檢查棧尾值是否異常 */    \
    const uint32_t * const pulStack = ( uint32_t * ) pxCurrentTCB->pxStack;                    \
    const uint32_t ulCheckValue = ( uint32_t ) 0xa5a5a5a5;                                    \
                                                                                            \
    if( ( pulStack[ 0 ] != ulCheckValue ) ||                                                \
        ( pulStack[ 1 ] != ulCheckValue ) ||                                                \
        ( pulStack[ 2 ] != ulCheckValue ) ||                                                \
        ( pulStack[ 3 ] != ulCheckValue ) )                                                    \
    {                                                                                        \
        vApplicationStackOverflowHook( ( TaskHandle_t ) pxCurrentTCB, pxCurrentTCB->pcTaskName ); \
    }   \
}
#ednif

6.4.4 檢索就緒表發掘新任務

freertos就緒表是一個二級線性表,由陣列+連結串列組成。
各級就緒連結串列都寄存在pxReadyTasksLists陣列中,排程器檢索就緒任務就是從pxReadyTasksLists陣列中,從最高優先順序就緒連結串列開始檢索就緒任務。

從最高優先順序的就緒連結串列開始檢索,找到所有就緒任務中最高優先順序的就緒連結串列。

然後檢索這個優先順序的就緒連結串列:

  • 如果這個優先順序只有一個就緒任務,就把這個就緒任務更新到pxCurrentTCB

  • 如果這個優先順序不止一個就緒任務,就把這個連結串列索引指向的任務的下一個任務更新到pxCurrentTCB

    • 這點就是freertos時間片的機制,偽時間片,因為這樣的實現導致freertos預設每個同級任務只有一人時間片。
#define taskSELECT_HIGHEST_PRIORITY_TASK()        \
{                 \
  /* 從就緒列表陣列中找出最高優先順序列表*/    \
  while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopReadyPriority ] ) ) )  \
  {                \
    configASSERT( uxTopReadyPriority );        \
    --uxTopReadyPriority;           \
  }                \
                                  \
  /* 相同優先順序的任務使用時間片共享處理器就是通過這個巨集實現*/   \
  listGET_OWNER_OF_NEXT_ENTRY(pxCurrentTCB, &( pxReadyTasksLists[ uxTopReadyPriority ] ) );   \
} /* taskSELECT_HIGHEST_PRIORITY_TASK *

#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList )                                           \
{                                                                                          \
    List_t * const pxConstList = ( pxList );                                               \
    /* 獲取所有指向的下一個任務到pxTCB,並更新當前連結串列索引。  */                         \
    ( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;                           \
    if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) ) \
    {                                                                                      \
        ( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;                       \
    }                                                                                      \
    ( pxTCB ) = ( pxConstList )->pxIndex->pvOwner;                                         \
}

這樣,就完成了更新pxCurrentTCB值,這個值就是需要切入的新任務的任務控制程式碼值。

附件

任務切換核心層:vTaskSwitchContext()

void vTaskSwitchContext( void )
{
    if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE ) /* 掛起排程器就不允許任務切換. */
    {
        /* 帶中斷保護的API函式的都會有一個引數"xHigherPriorityTaskWoken",若是使用者沒有使用這個引數,這裡設定任務切換標誌。在下個系統中斷服務例程中,會檢查xYieldPending的值,若是為pdTRUE則會觸發一次上下文切換。*/
        xYieldPending = pdTRUE;
    }
    else
    {
        xYieldPending = pdFALSE; /* 不需要在下次觸發切換。現在就可以切換。 */
        traceTASK_SWITCHED_OUT();

        /* 任務執行時間統計功能 */
        #if ( configGENERATE_RUN_TIME_STATS == 1 )
            {
                /* 獲取當前時間值。注意,這裡的時間值不要和系統節拍混淆,這兩個時間值在兩個獨立的時間域裡各自維護的。 */
                #ifdef portALT_GET_RUN_TIME_COUNTER_VALUE
                    portALT_GET_RUN_TIME_COUNTER_VALUE( ulTotalRunTime );
                #else
                    ulTotalRunTime = portGET_RUN_TIME_COUNTER_VALUE();
                #endif

                /* 將任務執行的時間新增到到目前為止的累計時間中。
                	任務開始執行的時間儲存在ulTaskSwitchedInTime中。
                	注意,這裡沒有溢位保護,所以計數值只有在計時器溢位之前才有效。
                	對負值的防範是為了防止可疑的執行時統計計數器實現——這些實現是由應用程式而不是核心提供的。*/ */
                if( ulTotalRunTime > ulTaskSwitchedInTime )
                {
                    pxCurrentTCB->ulRunTimeCounter += ( ulTotalRunTime - ulTaskSwitchedInTime );
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }
				/* 儲存當前時間 */
                ulTaskSwitchedInTime = ulTotalRunTime;
            }
        #endif /* configGENERATE_RUN_TIME_STATS */

        /* 棧溢位檢查 */
        taskCHECK_FOR_STACK_OVERFLOW();

        /* 在切換當前執行的任務之前,儲存其errno*/
        #if ( configUSE_POSIX_ERRNO == 1 )
            {
                pxCurrentTCB->iTaskErrno = FreeRTOS_errno;
            }
        #endif

        /* 選出下一個需要跑的任務. */
        taskSELECT_HIGHEST_PRIORITY_TASK();
        traceTASK_SWITCHED_IN();

        /* 切換到新任務後,更新全域性errno */
        #if ( configUSE_POSIX_ERRNO == 1 )
            {
                FreeRTOS_errno = pxCurrentTCB->iTaskErrno;
            }
        #endif

        #if ( configUSE_NEWLIB_REENTRANT == 1 )
            {
                /* 略 */
                _impure_ptr = &( pxCurrentTCB->xNewLib_reent );
            }
        #endif /* configUSE_NEWLIB_REENTRANT */
    }
}

相關文章