bug現象:
key任務:
默認任務:
此時兩個任務的優先級相同,搶占式調度,時間片輪轉,空閑任務讓步。
但是會出現一個問題,key任務在發送完隊列之后不會立即跳轉到默認任務的隊列接收函數后的printf,而是會在printf("Send Successfully")打印一半之后再跳轉到默認任務的printf里,但是此時默認任務的printf也無法打印了。具體跳轉步驟如下圖:
原因解析:
步驟“1”執行隊列發送函數的時候,會喚醒等待隊列數據接收的任務,如果任務優先級比當前任務高,那么會立刻搶占cpu,但是現在由于兩個任務的優先級是平級的關系,只能在時間片輪轉的時候進行任務調度,讓默認任務優先運行(等待隊列接收的任務)。
由于不是立即跳轉,導致步驟“2”的printf進行了一半,跳轉到了步驟“3”的printf,由于printf是不可重入函數,所以步驟“3”的printf無法打印。
運行順序為:
步驟“1” -> 步驟“2”一半 ->步驟“3” ->步驟“3”所在任務運行一個時間片(等待隊列接收)->任務調度回到步驟“2”將printf剩下的一半打印完
解決方法:
1 將key任務優先級抬高
當key任務發送隊列喚醒默認任務時,不會在下一個時間片切換任務,而是等key任務主動放棄CPU資源的時候再進行任務調度(先看優先級,再是隊列接收),從而保證printf()不會被多個任務同時調用,保證默認任務中步驟“3”printf的打印正常進行。
2 將默認任務優先級抬高
與方法1同理,將默認任務優先級抬高后可以在發送隊列結束后立即跳轉(默認任務被喚醒,優先級比key任務高,搶占CPU)到默認任務的打印函數(步驟“3”),在默認任務運行到阻塞(等待隊列接收)主動出讓CPU資源后再跳轉到步驟“2”運行printf,這種方法同樣保證了printf同時只被一個任務調用。
3 不更改任務優先級,更改調度方式(不常用,了解即可)
關于調度方式:
這個設置的更改即從“時間片輪轉”變為“非時間片輪轉”,讓任務調度只能通過優先級搶占和主動出讓CPU資源來進行任務切換。
由于key任務和默認任務優先級平級,當取消時間片輪轉后,自然不會出現步驟“2”printf打印一半就被時間片調度跳轉到另一個任務的情況,此時沒有更高優先級的任務,只有key任務主動出讓CPU資源才能切換任務。也保證了printf()函數同時只被一個任務調用。
知識點補充:
立即觸發調度的操作/函數:(deepseek)
關于xQueueSend()或xSemaphoreGive()的調度:(deepseek)
1. 默認行為:有條件觸發調度
這兩個函數在以下場景中可能觸發立即調度:
-
存在等待的高優先級任務:若調用
xQueueSend()
或xSemaphoreGive()
時,有更高優先級的任務正在等待隊列數據或信號量,則調度器會立即切換任務(搶占式調度生效)。例如:// 任務 A(低優先級)發送數據到隊列
-
xQueueSend(xQueue, &data, 0);
-
// 任務 B(高優先級)正在阻塞等待該隊列
-
// 此時任務 A 會立即被搶占,任務 B 開始運行
-
配置為搶占式調度:若 FreeRTOS 的調度策略設置為搶占式(默認),則高優先級任務就緒時會立即觸發切換。
2. 不觸發調度的場景
以下情況不會立即觸發調度:
-
無等待任務或優先級不滿足:若沒有任務在等待隊列/信號量,或等待的任務優先級不高于當前任務,則當前任務繼續運行,調度器不會切換。
-
在中斷服務程序(ISR)中未顯式請求調度:在 ISR 中調用
xQueueSendFromISR()
或xSemaphoreGiveFromISR()
時,需手動調用portYIELD_FROM_ISR()
才會觸發調度。
3. 與時間片輪轉調度的關系
若系統啟用了時間片輪轉(configUSE_TIME_SLICING = 1
),即使沒有更高優先級任務,同優先級任務也會在時間片用完時切換。但 xQueueSend()
或 xSemaphoreGive()
本身不會強制切換,需依賴時間片中斷。
總結
場景 | 是否觸發調度 | 說明 |
發送數據/信號量喚醒更高優先級任務 | 立即觸發 | 搶占式調度生效,任務切換 |
發送數據/信號量喚醒同或低優先級任務 | 不觸發 | 當前任務繼續運行 |
在 ISR 中未手動調用 | 不觸發 | 需顯式請求調度 |
無任務等待隊列/信號量 | 不觸發 | 無任務需切換 |
補充:顯式調度與隱式調度(deepseek)
1. 顯式觸發調度(Explicit Scheduling)
含義:
開發者主動調用特定函數來強制觸發任務調度,明確要求系統立即切換到其他就緒任務。
特點:
-
直接控制調度時機,代碼中可見明確的調度指令。
-
適用于需要手動讓出CPU的場景,例如協作式多任務或優化關鍵代碼段的執行順序。
FreeRTOS 中的顯式觸發函數:
-
taskYIELD()
:立即觸發調度器重新選擇任務,即使當前任務的時間片未用完。void vTaskFunction(void *pvParameters) { while (1) { // 執行某些操作后主動讓出CPU taskYIELD(); // 顯式觸發調度 } }
2. 隱式觸發調度(Implicit Scheduling)
含義:
調度由系統事件或API函數的副作用自動觸發,開發者無需顯式調用調度函數。
特點:
-
調度時機由系統管理,與任務狀態變化(如阻塞、資源釋放)或中斷事件綁定。
-
更符合搶占式調度機制,提升系統自動化程度。
FreeRTOS 中的隱式觸發場景:
-
任務阻塞操作:調用
vTaskDelay()
、xQueueReceive()
等函數時,任務進入阻塞狀態,調度器自動切換任務。vTaskDelay(pdMS_TO_TICKS(100)); // 隱式觸發調度(任務阻塞)
-
資源釋放喚醒高優先級任務:例如,在
xSemaphoreGive()
或xQueueSend()
中,若釋放資源后存在更高優先級任務就緒,調度器自動搶占當前任務。xSemaphoreGive(xSemaphore); // 隱式觸發調度(若喚醒高優先級任務)
-
中斷服務程序(ISR)中的調度請求:在中斷中調用
xQueueSendFromISR()
或xSemaphoreGiveFromISR()
后,通過portYIELD_FROM_ISR()
隱式觸發調度(需結合中斷上下文)。
對比總結
特性 | 顯式觸發調度 | 隱式觸發調度 |
觸發方式 | 開發者主動調用函數(如 | 系統自動觸發(如任務阻塞、資源釋放、中斷) |
控制權 | 完全由開發者控制 | 由系統事件或API邏輯控制 |
典型場景 | 協作式任務切換、優化執行順序 | 搶占式調度、事件驅動任務切換 |
代碼可見性 | 顯式代碼指令 | 隱含在API或系統行為中 |
不可重入函數
顧名思義:不可重入函數就是不可以在它還沒有返回就再次被調用。
不可重入原因:
由于函數內部使用了全局變量、靜態變量、或調用了不可重入的函數等,導致函數在執行過程中可能被中斷,并在中斷后繼續執行時出現數據錯誤或不可預料的后果。以下是不可重入函數的幾個特點:
-
使用全局變量或靜態變量:如果函數內部使用了全局變量或靜態變量,那么在多任務環境下,多個任務同時調用該函數時,可能會對這些共享變量進行并發修改,導致數據不一致。
-
調用不可重入函數:如果函數內部調用了其他不可重入函數,那么這些被調用的函數也可能因為上述原因導致整個調用鏈不可重入。
-
使用動態內存分配:函數體內調用了
malloc()
或者free()
函數,由于這些函數維護內部的鏈表,且這個過程不是原子的,因此在多任務環境下可能導致內存管理出現問題。 -
使用標準I/O函數:標準I/O函數通常使用全局數據結構,因此在多任務環境下也可能導致不可重入問題。