1、任務管理
任務或者說進程是一個操作系統的基本概念,該書並沒有去說明什么是任務,而是從應用的角度去介紹怎么在FreeRTOS中去創建一個任務並管理它。
1.1 任務函數
FreeRTOS中的任務是以一個函數的形式存在的,具有統一的函數原型,如下:
void TaskFunction(void *pvParameters);
其必須返回void且帶有一個void指針參數,任務函數體內通常有一個死循環,決不能有一條return語句,也不能執行到函數尾部,如果某個任務不再需要,可以顯式的將其刪除。
1.2任務狀態
當MCU只有一個核,應用程序又包含多個任務,那么只有一個任務正在執行,其他任務都處於等待狀態,如下:
這是最簡單的模型,那么CPU選擇哪一個任務運行呢?在早期CPU非常昂貴和稀有,許多用戶排隊等待CPU資源,那個時候的任務調度算法側重“公平共享”處理器時間,所有的任務地位平等,調度器給每個任務一個固定的時間獲得CPU資源,時間一到運行的任務就必須將CPU讓出來給其他任務,這種調度算法成為時間片輪轉調度。但是在實際的應用場景中,不同的任務緊急程度不同,好比你正在洗衣服手機響了,那么你應該放下手中的衣服並去接聽電話,當電話說完了再去接着洗衣服。於是時間片輪轉調度發展出一些變種,比如將優先級相同的任務放在同一個隊列,並且優先級越高的隊列其時間片也越短。
隨着處理器功能越來越強大,價格越來越便宜,單個用戶可以獨占一個處理器,可以使用戶同時運行多個應用程序,比如用戶可以一邊聽音樂一邊看網頁。隨着系統中的任務越來越多,應用場景也越來越復雜,原來只有運行態和等待態的模型已經不能滿足要求。例如:一個已獲得除CPU之外的所有資源(如內存空間等)的任務A,和一個已經在CPU中運行,但是因為等待某些事件(比如等待用戶輸入)而被中斷運行的任務B,這兩個任務此時都在內存中且沒有在CPU上運行,那么這兩個任務是否都可以歸為等待態呢?顯然不行,試想一下,A和B都在等待態,且B的優先級更高,那么調度器就會將CPU交給B,運行B時發現它在等待事件,將B移出CPU如此往復,直到B等待的事件發生,在這個過程中CPU實際沒有做有效工作,浪費了CPU資源。如此只能將A和B置為不同的狀態,FreeRTOS就將A的狀態定義為就緒態,而B的狀態成為阻塞態。就緒態就是已經准備好,只要CPU一空閑立馬就可以運行的狀態,而阻塞態是等待某一事件,只有該事件發生才能繼續運行的狀態,一些操作系統是將已等到事件發生的阻塞態任務轉為就緒態,如下:
就緒態和運行態之間之所以是雙向箭頭,因為一些支持搶占式的操作系統中,當就緒態中有了一個優先級更高的任務時,會搶占正在運行的低優先級的任務,並將低優先級的任務置為就緒態或者因為運行的任務已運行超過允許的時間而被移出運行態進入就緒態。
隨着任務進一步增多,而內存因為成本等原因,空間增長跟不上任務的增長,另外CPU的速度遠高於IO等設備的速度,內存中可能會出現許多處於阻塞態的任務。這個時候就有必要將一些阻塞態任務從內存中移到磁盤,將內存讓給處於運行態和就緒態的任務,如下圖:
掛起態之所以能夠轉化為就緒態,是因為在任務掛起時,如果它等待的事件發生了,那當調度器將其移入內存時,它的狀態就變為就緒態了。下圖是FreeRTOS中的完整任務狀態機模型。
為了避免反復下載到板子等繁瑣操作帶來的麻煩,我使用了FreeRTOS在Windows下的模擬器,在PC機上做實驗,模擬器下載地址是
https://sourceforge.net/projects/freertos/
壓縮包里面有Visual Studio環境的工程文件。
1.3 實驗一:創建任務
//每隔1秒,打印一次字符串
static void MyFirstTask( void *pvParameters )
{
while(1)
{
printf("\r\nThis is MyFirstTask!\r\n");
vTaskDelay(1000);//延時1000ms
}
}
void Test1_CreateTask( void )
{
//創建一個任務
xTaskCreate( MyFirstTask, //任務函數
"MyFirstTask", //任務名
configMINIMAL_STACK_SIZE, //任務棧深度
NULL, //傳入任務函數的參數
5, //任務優先級
NULL ); //任務句柄
vTaskStartScheduler();
for( ;; ); //任務調度器啟動失敗會進入這里
}
FreeRTOS中任務實例是一個永不返回的函數,函數原型固定為:
void ATaskFunction( void *pvParameters );
創建任務使用FreeRTOS 的API 函數xTaskCreate():
portBASE_TYPE xTaskCreate( pdTASK_CODE pvTaskCode,
const signed portCHAR * const pcName,
unsigned portSHORT usStackDepth,
void *pvParameters,
unsigned portBASE_TYPE uxPriority,
xTaskHandle *pxCreatedTask );
pvTaskCode :任務只是永不退出的C 函數,實現常通常是一個死循環。參數
pvTaskCode 只一個指向任務的實現函數的指針(效果上僅僅是函數名)。
pcName: 具有描述性的任務名。這個參數不會被FreeRTOS 使用。其只是單純地用於輔助調試。識別一個具有可讀性的名字總是比通過句柄來識別容易得多。
應用程序可以通過定義常量 config_MAX_TASK_NAME_LEN 來定義任務名的最大長度——包括’\0’結束符。如果傳入的字符串長度超過了這個最大值,字符串將會自動被截斷。
usStackDepth: 當任務創建時,內核會分為每個任務分配屬於任務自己的唯一狀態。usStackDepth 值用於告訴內核為它分配多大的棧空間。這個值指定的是棧空間可以保存多少個字(word),而不是多少個字節(byte)。比如說,如果是32 位寬的棧空間,傳入的usStackDepth值為100,則將會分配400 字節的棧空間(100 * 4bytes)。棧深度乘以棧寬度的結果千萬不能超過一個size_t 類型變量所能表達的最大值。應用程序通過定義常量 configMINIMAL_STACK_SIZE 來決定空閑
任務任用的棧空間大小。在FreeRTOS 為微控制器架構提供的Demo 應用程序中,賦予此常量的值是對所有任務的最小建議值。如果你的任務會使用大量棧空間,那么你應當賦予一個更大的值。沒有任何簡單的方法可以決定一個任務到底需要多大的棧空間。計算出來雖然是可能的,但大多數用戶會先簡單地賦予一個自認為合理的值,然后利用FreeRTOS 提供的特性來確證分配的空間既不欠缺也不浪費。
pvParameters: 任務函數接受一個指向void 的指針(void*)。pvParameters 的值即是傳遞到任務中的值。這篇文檔中的一些范例程序將會示范這個參數可以如何使用。
uxPriority: 指定任務執行的優先級。優先級的取值范圍可以從最低優先級0 到
最高優先級(configMAX_PRIORITIES – 1)。configMAX_PRIORITIES 是一個由用戶定義的常量。優先級號並沒有上限(除了受限於采用的數據類型和系統的有效內存空間),但最好使用實際需要的最小數值以避免內存浪費。如果uxPriority 的值超過了(configMAX_PRIORITIES – 1),將會導致實際賦給任務的優先級被自動封頂到最大合法值。
pxCreatedTask pxCreatedTask: 用於傳出任務的句柄。這個句柄將在API 調用中對該創建出來的任務進行引用,比如改變任務優先級,或者刪除任務。如果應用程序中不會用到這個任務的句柄,則pxCreatedTask 可以被設為NULL。
返回值: 有兩個可能的返回值:
1. pdTRUE
表明任務創建成功。
2. errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY
由於內存堆空間不足,FreeRTOS 無法分配足夠的空間來保存任務結構數據和任務棧,因此無法創建任務。
該實驗的運行效果如下:
實驗一:創建任務
在任務中調用的vTaskDelay需要在FreeRTOSConfig.h文件中將“INCLUDE_vTaskDelay”宏開關打開。調用vTaskDelay的任務會被阻塞,並進行一次任務切換。
那么在任務MyFirstTask被阻塞的這1000ms時間里,CPU在干啥?CPU是一直處於運行狀態的,FreeRTOS在啟動任務調度器vTaskStartScheduler()時,會自動創建一個空閑任務:
//如果需要空閑任務的句柄,就會進入該分支,並將空閑任務句柄存到xIdleTaskHandle中
#if ( INCLUDE_xTaskGetIdleTaskHandle == 1 )
{
xReturn = xTaskCreate( prvIdleTask, ( signed char * ) "IDLE", tskIDLE_STACK_SIZE, ( void * ) NULL, ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), &xIdleTaskHandle );
}
#else
{
xReturn = xTaskCreate( prvIdleTask, ( signed char * ) "IDLE", tskIDLE_STACK_SIZE, ( void * ) NULL, ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), NULL );
}
#endif
空閑任務的優先級為默認為0,是最低優先級,執行的是循環指令(如果有任務被刪除,空閑任務要負責回收資源),為的就是當所有任務都被阻塞時,CPU“有事可做”。
當然也可以在FreeRTOSConfig.h文件中將“configUSE_IDLE_HOOK”宏開關打開,然后在空閑任務鈎子函數vApplicationIdleHook()中搞一些“小事情”:
l 執行低優先級,后台或需要不停處理的功能代碼。
l 測試系統處理裕量(空閑任務只會在所有其它任務都不運行時才有機會執行,所以測量出空閑任務占用的處理時間就可以清楚的知道系統有多少富余的處理時間)。
l 將處理器配置到低功耗模式——提供一種自動省電方法,使得在沒有任何應用功能需要處理的時候,系統自動進入省電模式。
因為空閑任務隨時都可能被搶斷,所以空閑任務鈎子函數不能完成較復雜的功能而且永遠不能被阻塞或掛起,以防沒有任務處於就緒態使得CPU“無事可做”。
1.4實驗二:創建兩個任務
static char strTask1[] = "\r\nThis is Task1!\r\n";
static char strTask2[] = "\r\nThis is Task2!\r\n";
//打印字符串
static void PrintString( void *pvParameters )
{
//將傳入的任務參數強制轉為char *型
char *strPrintf = (char *)pvParameters;
int i ,j;
while(1)
{
printf("\r\n%s\r\n",strPrintf);
for(i=0;i<10000;i++)
for(j=0;j<10000;j++);
}
}
void Test2_CreateTwoTask( void )
{
//創建一個任務
xTaskCreate( PrintString, //任務函數
"PrintString1", //任務名
configMINIMAL_STACK_SIZE, //任務棧深度
strTask1, //傳入任務函數的參數
5, //任務優先級
NULL ); //任務句柄
//創建一個任務
xTaskCreate( PrintString, //任務函數
"PrintString2", //任務名
configMINIMAL_STACK_SIZE, //任務棧深度
strTask2, //傳入任務函數的參數
5, //任務優先級
NULL ); //任務句柄
vTaskStartScheduler();
for( ;; ); //任務調度器啟動失敗會進入這里
}
這里創建的兩個任務幾乎完全一樣,只是打印的字符串不一樣,因此可以共用一個任務函數來創建兩個任務實例,只是傳入任務函數的參數不同,兩個任務實例在調度器的控制下獨立運行。
運行效果如下:
實驗二:創建兩個任務
可以看出同優先級的任務,即使自身沒有阻塞或掛起,依然會被交替執行,這叫“時間片輪轉”,每個任務執行一個“時間片”后退出運行態,然后調度器選擇另一個同優先級的任務。時間片的長度可以通過在FreeRTOSConfig.h文件中配置宏“configTICK_RATE_HZ”,比如將其配置為100(Hz),那么時間片的長度就是10ms,時間片的長度要合適,不能太長也不能太短。
如果將上述源碼中任務1的優先級從5提高到6,可以看到運行效果如下:
調度器每次選擇就緒列表中優先級最高的任務,所以任務2得不到被執行的機會,稱之為“餓死”。為了避免多任務系統中低優先級的任務被餓死,高優先級的任務要主動將自己阻塞或者掛起,比如調用前文提到的vTaskDelay()。同時需要注意的是,FreeRTOS是“可搶占式調度”,意思是如果有優先級更高的任務被創建出來,調度器會進行一次任務切換,運行更高優先級的任務,即使當前任務的“時間片”沒有用完。
1.5實驗三:修改任務的優先級
FreeRTOS常用的管理任務優先級的有這樣幾個API:
UBaseType_t uxTaskPriorityGet( TaskHandle_t xTask )
功能:獲得某個任務當前的優先級;
參數:
xTask:需要獲取優先級的任務的句柄,當值為NULL時表示獲取當前任務的優先級;
UBaseType_t uxTaskPriorityGetFromISR( TaskHandle_t xTask )
功能:與uxTaskPriorityGet完成的功能是一樣的,唯一的區別是uxTaskPriorityGetFromISR是中斷安全的API。必須說明的是,只有以”FromISR”或”FROM_ISR”結束的API 函數或宏才可以在中斷服務例程中。
參數:
xTask:需要獲取優先級的任務的句柄,當值為NULL時表示獲取當前任務的優先級;
void vTaskPrioritySet( TaskHandle_t xTask, UBaseType_t uxNewPriority )
功能:設置任務的優先級
參數:
xTask:需要設置優先級的任務的句柄,當值為NULL時表示獲取當前任務的優先級;
uxNewPriority:新優先級
實驗源碼如下:
TaskHandle_t taskHandle_Task2;//任務2的句柄
//任務1函數
static void Task1( void *pvParameters )
{
int i,j;
while(1)
{ //將任務2的優先級設置成與本任務優先級一致
vTaskPrioritySet(taskHandle_Task2,uxTaskPriorityGet(NULL));
printf("\r\nTask2 is Running!\r\n");
for(i=0;i<1000;i++)
for(j=0;j<1000;j++);
}
}
//任務2 函數
static void Task2( void *pvParameters )
{
int i,j;
while(1)
{
//降低本任務優先級
vTaskPrioritySet(NULL,uxTaskPriorityGet(NULL)-1);
printf("\r\nTask2 Cannot Run!\r\n");
for(i=0;i<1000;i++)
for(j=0;j<1000;j++);
}
}
void Test3_CreateTwoTask( void )
{
//創建任務1
xTaskCreate( Task1, //任務函數
"Task1", //任務名
configMINIMAL_STACK_SIZE, //任務棧深度
NULL, //傳入任務函數的參數
5, //任務優先級
NULL ); //任務句柄
//創建任務2
xTaskCreate( Task2, //任務函數
"Task2", //任務名
configMINIMAL_STACK_SIZE, //任務棧深度
NULL, //傳入任務函數的參數
5, //任務優先級
&taskHandle_Task2 ); //任務句柄
vTaskStartScheduler();
for( ;; ); //任務調度器啟動失敗會進入這里
}
運行效果:
實驗三:修改任務的優先級
1.6實驗四:刪除任務
用到的API是void vTaskDelete( TaskHandle_t xTaskToDelete ),
功能:刪除指定的任務
參數:
xTaskToDelete:要刪除的任務句柄
實驗源碼如下:
TaskHandle_t taskHandle_Task2;//任務2的句柄
//任務1函數
static void Task1( void *pvParameters )
{
vTaskDelete(taskHandle_Task2);//刪除任務2
while(1)
{
printf("\r\nTHis is Task1!\r\n");
vTaskDelay(1000);
}
}
//任務2函數
static void Task2( void *pvParameters )
{
while(1)
{
printf("\r\nTHis is Task2!\r\n");
vTaskDelay(1000);
}
}
void Test4_CreateTwoTask( void )
{
//創建任務1
xTaskCreate( Task1, //任務函數
"Task1", //任務名
configMINIMAL_STACK_SIZE, //任務棧深度
NULL, //傳入任務函數的參數
4, //任務優先級
NULL ); //任務句柄
//創建任務2
xTaskCreate( Task2, //任務函數
"Task2", //任務名
configMINIMAL_STACK_SIZE, //任務棧深度
NULL, //傳入任務函數的參數
5, //任務優先級
&taskHandle_Task2 ); //任務句柄
vTaskStartScheduler();
for( ;; ); //任務調度器啟動失敗會進入這里
}
運行效果如下:
任務2的優先級高於任務1,但是在任務2自我阻塞時,任務1開始運行並刪除了任務2。需要主要的是,任務1刪除任務2並自我阻塞后,空閑任務開始運行,空閑任務會回收任務2占用的資源。