FreeRTOS --(11)任務管理之系統節拍
轉載自https://blog.csdn.net/zhoutaopower/article/details/107146764
前面有了建立任務、啟動排程器、任務控制,接下來便開始分析一個 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; }