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

前言

任務調度實現的兩個核心:

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

  • 任務切換實現。

    • 接口層實現。

原文:李柱明博客://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 */
    }
}