FreeRTOS --(11)任務管理之系統節拍

前面有了創建任務、啓動調度器、任務控制,接下來便開始分析一個 Tick 到來之後,FreeRTOS 即將有什麼行爲;

在啓動調度器的時候,就已經配置好了 SysTick,它作爲 OS 的心跳,每隔一個固定週期來一次 SysTick 中斷,來驅動 OS 做事(任務調度);

以 STM32 爲例,定義的 configTICK_RATE_HZ 爲 1000,由《FreeRTOS --(9)任務管理之啓動調度器》得知,系統節拍時鐘週期爲1ms;

不同的處理器結構可能有所區別,所以他是需要移植的部分,在 port.c 中 xPortSysTickHandler

void xPortSysTickHandler( void )
{
    /* The SysTick runs at the lowest interrupt priority, so when this interrupt
    executes all interrupts must be unmasked.  There is therefore no need to
    save and then restore the interrupt mask value as its value is already
    known - therefore the slightly faster vPortRaiseBASEPRI() function is used
    in place of portSET_INTERRUPT_MASK_FROM_ISR(). */
    vPortRaiseBASEPRI();
    {
        /* Increment the RTOS tick. */
        /* 如果返回值標記了任務切換,即有優先級高的任務 */
        if( xTaskIncrementTick() != pdFALSE )
        {
            /* A context switch is required.  Context switching is performed in
            the PendSV interrupt.  Pend the PendSV interrupt. */
            /* 設置PendSV中斷位 */
            portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
        }
    }
    vPortClearBASEPRIFromISR();
}

開頭調用 vPortRaiseBASEPRI();結尾調用 vPortClearBASEPRIFromISR(); 是爲了創造臨界區;

調用了 xTaskIncrementTick;如果返回 pdTRUE 則代表要進行任務切換,那麼就手動拉起 PendSV;否則不進行上下文切換;

接下來看下 xTaskIncrementTick 做了什麼,大概猜測是選最高優先級的,並且在 Ready 鏈表的任務投入運行:

BaseType_t xTaskIncrementTick( void )
{
TCB_t * pxTCB;
TickType_t xItemValue;
BaseType_t xSwitchRequired = pdFALSE;

    /* Called by the portable layer each time a tick interrupt occurs.
    Increments the tick then checks to see if the new tick value will cause any
    tasks to be unblocked. */
    traceTASK_INCREMENT_TICK( xTickCount );
    if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
    {
        /* Minor optimisation.  The tick count cannot change in this
        block. */
        const TickType_t xConstTickCount = xTickCount + ( TickType_t ) 1;

        /* Increment the RTOS tick, switching the delayed and overflowed
        delayed lists if it wraps to 0. */
        xTickCount = xConstTickCount;

        if( xConstTickCount == ( TickType_t ) 0U ) /*lint !e774 'if' does not always evaluate to false as it is looking for an overflow. */
        {
            taskSWITCH_DELAYED_LISTS();
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }

        /* See if this tick has made a timeout expire.  Tasks are stored in
        the queue in the order of their wake time - meaning once one task
        has been found whose block time has not expired there is no need to
        look any further down the list. */
        if( xConstTickCount >= xNextTaskUnblockTime )
        {
            for( ;; )
            {
                if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
                {
                    /* The delayed list is empty.  Set xNextTaskUnblockTime
                    to the maximum possible value so it is extremely
                    unlikely that the
                    if( xTickCount >= xNextTaskUnblockTime ) test will pass
                    next time through. */
                    xNextTaskUnblockTime = portMAX_DELAY; /*lint !e961 MISRA exception as the casts are only redundant for some ports. */
                    break;
                }
                else
                {
                    /* The delayed list is not empty, get the value of the
                    item at the head of the delayed list.  This is the time
                    at which the task at the head of the delayed list must
                    be removed from the Blocked state. */
                    pxTCB = listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList ); /*lint !e9079 void * is used as this macro is used with timers and co-routines too.  Alignment is known to be fine as the type of the pointer stored and retrieved is the same. */
                    xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );

                    if( xConstTickCount < xItemValue )
                    {
                        /* It is not time to unblock this item yet, but the
                        item value is the time at which the task at the head
                        of the blocked list must be removed from the Blocked
                        state - so record the item value in
                        xNextTaskUnblockTime. */
                        xNextTaskUnblockTime = xItemValue;
                        break; /*lint !e9011 Code structure here is deedmed easier to understand with multiple breaks. */
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }

                    /* It is time to remove the item from the Blocked state. */
                    ( void ) uxListRemove( &( pxTCB->xStateListItem ) );

                    /* Is the task waiting on an event also?  If so remove
                    it from the event list. */
                    if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
                    {
                        ( void ) uxListRemove( &( pxTCB->xEventListItem ) );
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }

                    /* Place the unblocked task into the appropriate ready
                    list. */
                    prvAddTaskToReadyList( pxTCB );

                    /* A task being unblocked cannot cause an immediate
                    context switch if preemption is turned off. */
                    #if (  configUSE_PREEMPTION == 1 )
                    {
                        /* Preemption is on, but a context switch should
                        only be performed if the unblocked task has a
                        priority that is equal to or higher than the
                        currently executing task. */
                        if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
                        {
                            xSwitchRequired = pdTRUE;
                        }
                        else
                        {
                            mtCOVERAGE_TEST_MARKER();
                        }
                    }
                    #endif /* configUSE_PREEMPTION */
                }
            }
        }

        /* Tasks of equal priority to the currently running task will share
        processing time (time slice) if preemption is on, and the application
        writer has not explicitly turned time slicing off. */
        #if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) )
        {
            if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 )
            {
                xSwitchRequired = pdTRUE;
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
        #endif /* ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) ) */

        #if ( configUSE_TICK_HOOK == 1 )
        {
            /* Guard against the tick hook being called when the pended tick
            count is being unwound (when the scheduler is being unlocked). */
            if( xPendedTicks == ( TickType_t ) 0 )
            {
                vApplicationTickHook();
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
        #endif /* configUSE_TICK_HOOK */

        #if ( configUSE_PREEMPTION == 1 )
        {
            if( xYieldPending != pdFALSE )
            {
                xSwitchRequired = pdTRUE;
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
        #endif /* configUSE_PREEMPTION */
    }
    else
    {
        ++xPendedTicks;

        /* The tick hook gets called at regular intervals, even if the
        scheduler is locked. */
        #if ( configUSE_TICK_HOOK == 1 )
        {
            vApplicationTickHook();
        }
        #endif
    }

    return xSwitchRequired;
}

這個函數有點長,但是其實思路是非常清晰易懂的:

1、因爲 FreeRTOS 支持掛起調度器,也就是調用 vTaskSuspendAll 後,RTOS 在每個 Tick 來臨的時候,不在調度任務進行上下文切換;所以,每次進入 xTaskIncrementTick 的時候,要判斷調度器是否被掛起;

2、如果允許調度,首先增加當前的計數器的計數:xTickCount

3、增加完 xTickCount 後,判斷計數器是否溢出,如果溢出了,那麼調用 taskSWITCH_DELAYED_LISTS 來交換 pxDelayedTaskList 和 pxOverflowDelayedTaskList (爲了解決xTickCount溢出問題,FreeRTOS使用了兩個延時列表:xDelayedTaskList1 和 xDelayedTaskList2。並使用延時列表指針pxDelayedTaskList和溢出延時列表指針pxOverflowDelayedTaskList分別指向上面的延時列表1和延時列表2(在創建任務時將延時列表指針指向延時列表)。這兩個延時列表指針變量和兩個延時列表變量都是在tasks.c中定義的靜態局部變量)

/* pxDelayedTaskList and pxOverflowDelayedTaskList are switched when the tick
count overflows. */
#define taskSWITCH_DELAYED_LISTS()                                                                  \
{                                                                                                   \
    List_t *pxTemp;                                                                                 \
                                                                                                    \
    /* The delayed tasks list should be empty when the lists are switched. */                       \
    configASSERT( ( listLIST_IS_EMPTY( pxDelayedTaskList ) ) );                                     \
                                                                                                    \
    pxTemp = pxDelayedTaskList;                                                                     \
    pxDelayedTaskList = pxOverflowDelayedTaskList;                                                  \
    pxOverflowDelayedTaskList = pxTemp;                                                             \
    xNumOfOverflows++;                                                                              \
    prvResetNextTaskUnblockTime();                                                                  \
}

這兩個鏈表專門爲了處理計數器溢出而存在;一旦溢出,就交換,OS 始終取的是 pxDelayedTaskList 中的 Delay Task,在掛接任務的時候,判斷時鐘計數器,看是否需要往 pxOverflowDelayedTaskList 上面掛;

4、對比當前的時間 xConstTickCount 和下一個阻塞在時間上的任務的時間 xNextTaskUnblockTime 大小,查看阻塞時間是否到期,xNextTaskUnblockTime 是一個全局變量,記錄着下一個最近的任務阻塞時間;

5、如果阻塞時間到期,那麼首先判斷當前的 Delay 鏈表是否爲空,如果爲空,則說明沒有阻塞在時間上的任務,將 xNextTaskUnblockTime 賦值爲最大 portMAX_DELAY,直接退出;

6、如果阻塞時間到期,而且 pxDelayedTaskList 鏈表不爲空,那麼取出 pxDelayedTaskList 鏈表的第一個元素(注意,往 pxDelayedTaskList 鏈表中插入 Item 的時候,是用 vListInsert ,根據喚醒時間有序插入的,即前面放置的是 Delay 時間最小的,後面是 Delay 大的)的時間,和當前的時間 xConstTickCount 進行比對,看看是否超期,如果沒有超期,那麼將其更新到下一個喚醒時間 xNextTaskUnblockTime 中,退出;如果到期,那麼將其從 pxDelayedTaskList 鏈表中移除(如果在等 Event 也同時從 Event 中移除),將其添加到 ReadyList(prvAddTaskToReadyList),

/*
 * Place the task represented by pxTCB into the appropriate ready list for
 * the task.  It is inserted at the end of the list.
 */
#define prvAddTaskToReadyList( pxTCB )                                                              \
    traceMOVED_TASK_TO_READY_STATE( pxTCB );                                                        \
    taskRECORD_READY_PRIORITY( ( pxTCB )->uxPriority );                                             \
    vListInsertEnd( &( pxReadyTasksLists[ ( pxTCB )->uxPriority ] ), &( ( pxTCB )->xStateListItem ) ); \
    tracePOST_MOVED_TASK_TO_READY_STATE( pxTCB )

7、如果支持搶佔的話(不支持搶佔的 RTOS 是沒有靈魂的),就要判斷解除阻塞的任務和當前的任務的優先級,哪個更高,如果高於當前的任務優先級,那麼 xSwitchRequired 設置爲 pdTRUE,表示要進行一次上下文切換;

8、循環步驟 4 到 7,也就是對 pxDelayedTaskList  鏈表中的元素進行遍歷,直到 pxDelayedTaskList 爲空,或者有元素的運行時間還未到,還需要繼續阻塞;

9、此刻,該到期的任務,已經全部從 pxDelayedTaskList  鏈表移動到了 pxReadyTasksLists 中,對應優先級的地方;

10、如果定義了搶佔(configUSE_PREEMPTION),同時也定義了同一個優先級輪轉調度(configUSE_TIME_SLICING) 的話呢(普通情況下,這兩個都需要定義,不然沒有靈魂),只要當前的任務所在的 pxReadyTasksLists 鏈表中,包含不止一個待運行的任務,就要去輪轉調度另一個任務執行;所以 xSwitchRequired 設置爲  pdTRUE;

11、如果應用層定義了 configUSE_TICK_HOOK,那麼會調用 vApplicationTickHook 鉤子;

12、如果定義了搶佔(configUSE_PREEMPTION),而且 xYieldPending 也是 pdTRUE 的時候,也會設置 xSwitchRequired 設置爲  pdTRUE,強制去進行上下文切換,

xYieldPending 這個變量什麼時候會被設置稱爲 pdTRUE

對於隊列以及使用隊列機制的信號量、互斥量等,在中斷服務程序中調用了這些API函數,將任務從阻塞中解除,則需要調用函數xTaskRemoveFromEventList()將任務的事件列表項從事件列表中移除。在移除事件列表項的過程中,會判斷解除的任務優先級是否大於當前任務的優先級,如果解除的任務優先級更高,會將變量xYieldPending設置爲pdTRUE。在下一次系統節拍中斷服務函數中,觸發一次任務切換;

  if(pxUnblockedTCB->uxPriority > pxCurrentTCB->uxPriority)
 {
      /*任務具有更高的優先級,返回pdTRUE。告訴調用這個函數的任務,它需要強制切換上下文。*/
      xReturn= pdTRUE;
 
      /*帶中斷保護的API函數的都會有一個參數參數"xHigherPriorityTaskWoken",如果用戶沒有使用這個參數,這裏設置任務切換標誌。在下個系統中斷服務例程中,會檢查xYieldPending的值,如果爲pdTRUE則會觸發一次上下文切換。*/
      xYieldPending= pdTRUE;
 }        

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章