FreeRTOS任務調度


為了滿足處理器多任務並發進行的需求,需要通過系統調度來合理安排各個任務占有CPU的時間。任務管理和調度是RTOS的核心功能。

一般系統中,任務可以分為Running態和非Running態,而非Running態可以細分。很容易理解,Running態就是占用CPU的任務,而非Running態就是其他任務。

FreeRTOS中,任務狀態可以分為Running,Suspend,Ready,Blocked。任務在被創建之后,就會被默認設置為Ready態。調度器通過不同的任務優先級和時間將任務在各個狀態之間切換。各個狀態之間的轉換順序如下:

1.任務狀態

 

 FreeRTOS中的調度方式包括以下

搶占式調度:當有新的任務就緒(ready),且優先級大於等於當前任務的優先級時,當前任務就會被搶占;需要用戶自己通過configUSE_PREEMPTION配置。

時間片調度:同處於ready態的最高優先級的任務會輪流運行固定的時間片;通過configUSE_TIME_SLICING配置,默認開啟。

 2.任務創建

FreeRTOS任務創建有兩個接口:

 1 xTaskCreate           2 xTaskCreateStatic 

 任務創建的時候,會同時配置TCB(Task Control Block),用來存儲該任務的優先級,任務等。

任務被創建后,默認將任務設置為就緒態,等待系統調度

3.狀態鏈表

FreeRTOS中不同狀態的任務分別用不同的狀態鏈表來管理:

  1.運行態

 1 TCB_t * volatile pxCurrentTCB 

這個比較特殊,因為運行態的任務永遠只有一個,所以用pxCurrentTCB指向任務的TCB即可

  2.就緒態:

 1 List_t pxReadyTasksLists[ configMAX_PRIORITIES ] 

configMAX_PRIORITIES對應最大的任務優先級,從0開始,隨着數字的增大優先級提高。pxReadyTasksLists是一個鏈表數組,每個就緒的任務都會被添加到該數組對應優先級的鏈表中

 

  3.阻塞態:

1 List_t xDelayedTaskList1
2 List_t xDelayedTaskList2
3 List_t * volatile pxDelayedTaskList;
4 List_t * volatile pxOverflowDelayedTaskList;

當任務在等待某個事件或者主動delay的時候,就會被添加到延遲鏈表中,並根據即將溢出的時間(鏈表中的值為 xConstTickCount + xTicksToWait)先后順序排列。當該延遲時間溢出(xConstTickCount + xTicksToWait 小於 xConstTickCount)時就會被添加到延遲溢出鏈表,當系統時間溢出后,調用 taskSWITCH_DELAYED_LISTS 將鏈表pxDelayedTaskList 和 pxOverflowDelayedTaskList 交換。

  4.掛起態:

 1 List_t xSuspendedTaskList; 

掛起的任務將會被添加到這個鏈表中,並不再參與系統調度直到有任務調用恢復接口將該任務恢復。

  5.

還有個特殊的待定鏈表

 1 List_t xPendingReadyList 

當調度器被掛起時,被喚醒的任務就會先被添加到該鏈表中,直到調度器恢復時,再從該鏈表添加到就緒鏈表中,並且判斷是否需要進行任務搶占。

該鏈表的作用就在於暫存調度器被掛起時就緒的任務

 

4.調度器啟動過程

 1 void vTaskStartScheduler( void ) 

  1. 創建IDLE任務,並且把任務優先級設置為0和特權位
  2. 創建Timer任務,任務優先級一般高於用戶的最高優先級
  3. 禁止中斷,保持調度器啟動(xPortStartScheduler)過程中不被中斷
  4. 調用xPortStartScheduler:

配置tick中斷以及使能tick timer,設置SVC異常,上下文切換和運行第一個任務

 

5.任務切換時機

FreeRTOS通過觸發SWI中斷,在SWI中斷處理函數中進行任務切換

有以下方式觸發:

  1. 同等級任務時間片用完,在SysTick 中斷中觸發觸發PendSV
  2. 用戶主動調用taskYIELD(),觸發PendSV
  3. 高優先任務恢復就緒或任務調用事件阻塞接口時搶占觸發PendSV

最終都是通過調用移植層提供的 portYIELD() 宏觸發 PendSV 異常,手工往NVIC的 PendSV 寄存器中寫 1。如果還有其他高優先級的中斷,則等待其他高優先級中斷處理完才執行 PendSV 異常處理進行任務切換。

 
  1. 同等級任務時間片用完,在SysTick 中斷中觸發觸發PendSV

 1 //FreeRTOS/Source/portable/ARMv8M/non_secure/port.c
 2 void SysTick_Handler( void ) /* PRIVILEGED_FUNCTION */
 3 {
 4   uint32_t ulPreviousMask;
 5 
 6    ulPreviousMask = portSET_INTERRUPT_MASK_FROM_ISR();//屏蔽中斷
 7    {
 8         /* 增加系統tick. */
 9        if( xTaskIncrementTick() != pdFALSE )
10        {
11            /* 觸發PendSV,等中斷處理函數結束后再判斷判斷是否進行任務切換. */
12            *( portNVIC_INT_CTRL ) = portNVIC_PENDSVSET;
13        }
14    }
15    portCLEAR_INTERRUPT_MASK_FROM_ISR( ulPreviousMask );//解除中斷屏蔽,恢復屏蔽前狀態
16 }

tick的中斷處理函數先屏蔽中斷,有些實現中會屏蔽全局中斷,而有些只是屏蔽低優先級的中斷,為了保證tick簡單可靠,屏蔽tick優先級以上所有中斷比較合適。函數退出前會解除中斷屏蔽,恢復屏蔽前狀態,這里不一定就是完全解除中斷屏蔽了,因為這個tick也可能是搶占了低優先級的中斷來執行的。 等待tick中斷處理函數退出后,當沒有比PendSV更高優先級的中斷,就會進入PendSV中斷進行任務切換。 

也有

  2.用戶主動調用taskYIELD(),觸發PendSV 

//FreeRTOS/Source/include/task.h 
#define taskYIELD() portYIELD()

//FreeRTOS/Source/portable/ARMv8M/non_secure/portable/GCC/ARM_CM23/portmacro.h
#define portYIELD()    vPortYield()

taskYIELD()在中是一個宏,vPortYield()也是觸發PendSV

   3.高優先任務恢復就緒或任務調用事件阻塞接口時搶占觸發PendSV

高優先任務恢復就緒(如信號量,隊列等阻塞、掛起狀態下退出),任務調用事件阻塞接口(如等待信號量,sleep等)

上述接口調用過程中都會調用到 xTaskResumeAll() 

 1 void vTaskDelay( const TickType_t xTicksToDelay )
 2 {
 3   BaseType_t xAlreadyYielded = pdFALSE;
 4 
 5     /* A delay time of zero just forces a reschedule. */
 6     if( xTicksToDelay > ( TickType_t ) 0U )
 7     {
 8         configASSERT( uxSchedulerSuspended == 0 );
 9         vTaskSuspendAll();
10         {
11             traceTASK_DELAY();
12 
13             /* A task that is removed from the event list while the
14             scheduler is suspended will not get placed in the ready
15             list or removed from the blocked list until the scheduler
16             is resumed.
17             This task cannot be in an event list as it is the currently
18             executing task. */
19             prvAddCurrentTaskToDelayedList( xTicksToDelay, pdFALSE );
20         }
21         xAlreadyYielded = xTaskResumeAll();
22     }
23     else
24     {
25         mtCOVERAGE_TEST_MARKER();
26     }
27     /* Force a reschedule if xTaskResumeAll has not already done so, we may
28     have put ourselves to sleep. */
29     if( xAlreadyYielded == pdFALSE )
30     {
31         portYIELD_WITHIN_API();
32     }
33     else
34     {
35         mtCOVERAGE_TEST_MARKER();
36     }
37 }

   以 vTaskDelay 為例,當sleep的時間大於0的時候,就會把當前task從 pxReadyTasksLists 移除並添加到 pxDelayedTaskList 鏈表中,然后調用 xTaskResumeAll()。xTaskResumeAll() 中會判斷在調度器掛起期間是否有有新的任務 ready,如果有,就會將這些 task 從 xPendingReadyList 中移除並添加到 pxReadyTasksLists, 並判斷是否需要進行任務搶占。

 

6. 任務切換過程

 一般來說都是在 PendSV_Handler() 中進行保存上一個任務的現場,任務切換,恢復新的任務的現場的操作。這個一般都是匯編來實現,並且每一款處理器都不一樣。這里不贅述了,值得一提的就是所有的 xPortPendSVHandler() 都是通過 tasks.c 中的 vTaskSwitchContext() 來進行任務切換的。 

 1 //FreeRTOS/Source/tasks.c
 2 //已經僅為該函數部分代碼
 3 void vTaskSwitchContext( void )
 4 {
 5     //...
 6     traceTASK_SWITCHED_OUT();//FreeRTOS中 rtos trace 點,記錄原本正在執行的任務的切出
 7     //...
 8     /* Check for stack overflow, if configured. */
 9     taskCHECK_FOR_STACK_OVERFLOW();
10 
11     /* Before the currently running task is switched out, save its errno. */
12     #if( configUSE_POSIX_ERRNO == 1 )
13     {
14         pxCurrentTCB->iTaskErrno = FreeRTOS_errno;
15     }
16     #endif
17 
18     /* Select a new task to run using either the generic C or port
19     optimised asm code. */
20     taskSELECT_HIGHEST_PRIORITY_TASK(); //在 pxReadyTasksLists 中選擇優先級最高的 task,軟件實現O(1)
21     traceTASK_SWITCHED_IN();//FreeRTOS中的rtos trace點,記錄新任務切入
22 
23     //...
24 } 

有趣的是,可以在該函數的兩個 trace 點中添加自定義的 debug 工具,比如檢測運行task時間過長,記錄任務的切換流程等

 

參考文檔:

https://blog.csdn.net/qq_18150497/article/details/52824082

https://juejin.im/post/5da5bdd05188254f4d2b7f67

https://blog.csdn.net/pfysw/article/details/80964603

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM