引言
RTOS 系統的核心是任務管理,而在實時作業系統中,任務和線程在概念上其實是一樣的。是以任務管理也可以叫做線程管理。初步上手 RTOS 系統首先必須掌握的也是任務的建立、删除、挂起和恢複等操作,由此可見任務管理的重要性。在日常生活中,我們要完成一個大任務,一般會将它分解成多個簡單、容易解決的小問題,小問題逐個被解決,大問題也就随之解決了。在多線程作業系統中,也同樣需要開發人員把一個複雜的應用分解成多個小的、可排程的、序列化的程式單元,當合理地劃分任務并正确地執行時,這種設計能夠讓系統滿足實時系統的性能及時間的要求。本文中使用的例子,多是參考與FreeRTOS和RT-Thread。
介紹
多任務系統
多任務系統會把一個大問題(應用)“分而治之”,把大問題劃分成很多個小問題,逐漸的把小問題解決掉,大問題也就随之解決了,這些小問題可以單獨的作為一個小任務來處理。這些小任務是并發處理的,注意,并不是說同一時刻一起執行很多個任務,而是由于每個任務執行的時間很短,導緻看起來像是同一時刻執行了很多個任務一樣。多個任務帶來了一個新的問題,
究竟哪個任務先運作,哪個任務後運作呢?完成這個功能的東西在 RTOS 系統中叫做任務排程器。不同的系統其任務排程器的實作方法也不同。線程管理的主要功能是對線程進行管理和排程,系統中總共存在兩類線程,分别是系統線程和使用者線程,系統線程是由 RTOS核心建立的線程,使用者線程是由應用程式建立的線程,這兩類線程都會從核心對象容器中配置設定線程對象,當線程被删除時,也會被從對象容器中删除。
在多任務系統中,根據程式的功能,我們把這個程式主體分割成一個個獨立的,無限循環且不能傳回的小程式,這個小程式我們稱之為任務。每個任務都是獨立的,互不幹擾的,且具備自身的優先級,它由作業系統排程管理。
高優先級的任務可以打斷低優先級任務的運作而取得 CPU 的使用權,這樣
就保證了那些緊急任務的運作。這樣我們就可以為那些對實時性要求高的任務設定一個很高的優先級,比如自動駕駛中的障礙物檢測任務等。高優先級的任務執行完成以後重新把 CPU 的使用權歸還給低優先級的任務,這個就是搶占式多任務系統的基本原理。
任務
什麼是任務?
在裸機系統中,系統的主體就是 main 函數裡面順序執行的無限循環,這個無限循環裡面 CPU 按照順序完成各種事情。在多任務系統中,我們根據功能的不同,把整個系統分割成一個個獨立的且無法傳回的函數,這個函數我們稱為任務。在使用 RTOS 的時候一個實時應用可以作為一個獨立的任務。每個任務都有自己的運作環境,不依賴于系統中其他的任務或者 RTOS 排程器。任何一個時間點隻能有一個任務運作,具體運作哪個任務是由 RTOS 排程器來決定的,RTOS 排程器是以就會重複的開啟、關閉每個任務。任務不需要了解 RTOS 排程器的具體行為,RTOS 排程器的職責是確定當一個任務開始執行的時候其上下文環境(寄存器值,堆棧内容等)和任務上一次退出的時候相同。為了做到這一點,每個任務都必須有個堆棧,當任務切換的時候将上下文環境儲存在堆棧中,這樣當任務再次執行的時候就可以從堆棧中取出上下文環境,任務恢複運作。
任務的大概形式具體見如下代碼:
void task_entry(void *pvParameters)
{
/*任務主體,無限循環且不能傳回*/
while()
{
//任務主體代碼
}
}
複制
任務狀态
RTOS的任務狀态大緻可分為:建立态、運作态、就緒态、阻塞态(有的作業系統也稱為挂起态,有的作業系統同時有阻塞态和挂起态)和終止态。
-
建立态
當線程剛開始建立還沒開始運作時就處于初始狀态;在初始狀态下,線程不參與排程。
-
運作态
當一個任務正在運作時,那麼就說這個任務處于運作态,處于運作态的任務就是目前正在使用處理器的任務。如果使用的是單核處理器的話那麼不管在任何時刻永遠都隻有一個任務處于運作态。
-
就緒态
處于就緒态的任務是那些已經準備就緒(這些任務沒有被阻塞或者挂起),可以運作的任務,但是處于就緒态的任務還沒有運作,因為有一個同優先級或者更高優先級的任務正在運作!
-
阻塞态(挂起态)
阻塞态也稱挂起态,它可能因為資源不可用而挂起等待,或線程主動延時一段時間而挂起。在挂起狀态下,線程不參與排程。
如果一個任務目前正在等待某個外部事件的話就說它處于阻塞态,比如說如果某個任務調用了函數 vTaskDelay()的話就會進入阻塞态,直到延時周期完成。任務在等待隊列、信号量、事件組、通知或互斥信号量的時候也會進入阻塞态。任務進入阻塞态會有一個逾時時間,當超過這個逾時時間任務就會退出阻塞态,即使所等待的事件還沒有來臨!
-
終止态
線程運作結束時将處于關閉狀态。關閉狀态的線程不參與線程的排程。

任務優先級
線程的優先級是表示線程被排程的優先程度。每個線程都具有優先級,線程越重要,賦予的優先級就應越高,線程被排程的可能才會越大。對于 ARM Cortex-M 系列,普遍采用 32 個優先級。最低優先級預設配置設定給空閑線程使用,使用者一般不使用。在系統中,當有比目前線程優先級更高的線程就緒時,目前線程将立刻被換出,高優先級線程搶占處理器運作。優先級數字越低表示任務的優先級越低,0 的優先級最低。RTOS 排程器確定處于就緒态或運作态的高優先級的任務擷取處理器使用權,換句話說就是處于就緒态的最高優先級的任務才會運作。處于就緒态的優先級相同的任務就會使用時間片輪轉排程器擷取運作時間。
任務控制塊
的每個任務都有一些屬性需要存儲,RTOS 把這些屬性集合到一起用一個結構體來表示,這個結構體叫做任務控制塊(TCB)。任務控制塊就相當于任務的身份證,裡面存有任務的所有資訊,比如任務的棧指針,任務名稱,任務的形參等。有了這個任務控制塊之後,以後系統對任務的全部操作都可以通過這個任務控制塊來實作。
任務堆棧
RTOS 之是以能正确的恢複一個任務的運作就是因為有任務堆棧在保駕護航,(如果是在有程序的作業系統中,儲存和恢複現場是通過PCB完成)任務排程器在進行任務切換的時候會将目前任務的現場(CPU 寄存器值等)儲存在此任務的任務堆棧中,等到此任務下次運作的時候就會先用堆棧中儲存的值來恢複現場,恢複現場以後任務就會接着從上次中斷的地方開始運作。建立任務的時候需要給任務指定堆棧。線程棧還用來存放函數中的局部變量:函數中的局部變量從線程棧空間中申請;函數中局部變量初始時從寄存器中配置設定(ARM 架構),當這個函數再調用另一個函數時,這些局部變量将放入棧中。
使用方法
建立和删除任務
建立線程
一個線程要成為可執行的對象,就必須由作業系統的核心來為它建立一個線程。
一般情況,建立線程都會分為兩種方式,分别是動态建立和靜态建立。
比如FreeRTOS的線程建立就是分為xTaskCreate( 使用動态的方法建立一個任務)和xTaskCreateStatic( 使用靜态的方法建立一個任務)。
動态建立任務的堆棧由系統配置設定,而靜态建立任務的堆棧由使用者自己傳遞。
新建立的任務預設就是就緒态的,如果目前沒有比它更高優先級的任務運作那麼此任務就會立即進入運作态開始運作,不管在任務排程器啟動前還是啟動後,都可以建立任務。
我們均以FreeRTOS為例。
動态建立
xTaskCreate()此函數用來動态建立一個任務,任務需要 RAM 來儲存與任務有關的狀态資訊(任務控制塊),任務也需要一定的 RAM 來作為任務堆棧。如果使用函數 xTaskCreate()來建立任務的話那麼這些所需的 RAM 就會自動的從 FreeRTOS 的堆中配置設定,是以必須提供記憶體管理檔案,預設我們使用heap_4.c 這個記憶體管理檔案
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,
const char * const pcName,
const uint16_t usStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t * const pxCreatedTask )
複制
參數:
名稱 | 作用 |
---|---|
pxTaskCode | 任務函數。 |
pcName | 任務名字,一般用于追蹤和調試,任務名字長度不能超過configMAX_TASK_NAME_LEN。 |
usStackDepth | 任務堆棧大小,注意實際申請到的堆棧是 usStackDepth 的 4 倍。其中空閑任務的任務堆棧大小為 configMINIMAL_STACK_SIZE。 |
pvParameters | 傳遞給任務函數的參數。 |
uxPriotiry: | 任務優先級,範圍 0~ configMAX_PRIORITIES-1。 |
pxCreatedTask | 任務句柄,任務建立成功以後會傳回此任務的任務句柄,這個句柄其實就是任務的任務堆棧。此參數就用來儲存這個任務句柄。其他 API 函數可能會使用到這個句柄。 |
傳回值:
名稱 | 含義 |
---|---|
pdPASS | 任務建立成功。 |
errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY | 任務建立失敗,因為堆記憶體不足! |
靜态建立
靜态建立任務使用xTaskCreateStatic(),但是使用此函數建立的任務所需 的 RAM 需 要 用 用 戶 來 提 供 。
TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode,
const char * const pcName,
const uint32_t ulStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
StackType_t * const puxStackBuffer,
StaticTask_t * const pxTaskBuffer )
複制
參數:
名稱 | 作用 |
---|---|
pxTaskCode | 任務函數。 |
pcName | 任務名字,一般用于追蹤和調試,任務名字長度不能超過。 |
configMAX_TASK_NAME_LEN。 | |
usStackDepth | 任務堆棧大小,由于本函數是靜态方法建立任務,是以任務堆棧由使用者給出,一般是個數組,此參數就是這個數組的大小。 |
pvParameters | 傳遞給任務函數的參數。 |
uxPriotiry | 任務優先級,範圍 0~ configMAX_PRIORITIES-1。 |
puxStackBuffer | 任務堆棧,一般為數組,數組類型要為 StackType_t 類型。 |
pxTaskBuffe | 任務控制塊。 |
傳回值:
名稱 | 含義 |
---|---|
NULL | 任務建立失敗,puxStackBuffer 或 pxTaskBuffer 為 NULL 的時候會導緻這個錯誤的發生。 |
其他值 | 任務建立成功,傳回任務的任務句柄。 |
删除線程
被删除了的任務不再存在,也就是說再也不會進入運作态。任務被删除以後就不能再使用此任務的句柄!如果此任務是使用動态方法建立的,也就是使用函數 xTaskCreate()建立的,那麼在此任務被删除以後此任務之前申請的堆棧和控制塊記憶體會在空閑任務中被釋放掉,是以當調用函數 vTaskDelete()删除任務以後必須給空閑任務一定的運作時間。隻有那些由核心配置設定給任務的記憶體才會在任務被删除以後自動的釋放掉,使用者配置設定給任務的記憶體需要使用者自行釋放掉。
vTaskDelete( TaskHandle_t xTaskToDelete )
複制
參數:
xTaskToDelete: 要删除的任務的任務句柄。
挂起和恢複線程
有時候我們需要暫停某個任務的運作,過一段時間以後在重新運作。這個時候要是使用任務删除和重建的方法的話那麼任務中變量儲存的值肯定丢失了!RTOS 給我們提供了解決這種問題的方法,那就是任務挂起和恢複,當某個任務要停止運作一段時間的話就将這個任務挂起,當要重新運作這個任務的話就恢複這個任務的運作。
下面還以FreeRTOS為例:
挂起線程
在FreeRTOS中,vTaskSuspend()此函數用于将某個任務設定為挂起态,進入挂起态的任務永遠都不會進入運作态。退出挂起态的唯一方法就是調用任務恢複函數 vTaskResume()或 xTaskResumeFromISR()。
void vTaskSuspend( TaskHandle_t xTaskToSuspend );
複制
參數:
xTaskToSuspend: 要挂起的任務的任務句柄,建立任務的時候會為每個任務配置設定一個任務句柄。如果使用函數 xTaskCreate()建立任務的話那麼函數的參數pxCreatedTask 就是此任務的任務句柄,如果使用函數 xTaskCreateStatic()建立任務的話那麼函數的傳回值就是此任務的任務句柄。也可以通過函數 xTaskGetHandle()來根據任務名字來擷取某個任務的任務句柄。注意!如果參數為 NULL 的話表示挂起任務自己。
恢複線程
在FreeRTOS中,vTaskResume()此函數用于将某一個任務從挂起态恢複到就緒态。
void vTaskResume( TaskHandle_t xTaskToResume)
複制
參數:
xTaskToResume: 要恢複的任務的任務句柄。
任務排程器
我們想要任務能夠進行排程,就必須依賴于任務排程器,在FreeROTS中排程器開始使用的是vTaskStartScheduler();,這個函數的功能就是開啟任務排程器。
開啟後我們才可以進行任務排程。
空閑任務
空閑任務就是空閑的時候運作的任務,也就是系統中其他的任務由于各種原因不能運作的時候空閑任務就在運作。空閑任務是 RTOS 系統自動建立的,不需要使用者手動建立。任務排程器啟動以後就必須有一個任務運作!但是空閑任務不僅僅是為了滿足任務排程器啟動以後至少有一個任務運作而建立的,空閑任務中還會去做一些其他的事情,如下:
- 判斷系統是否有任務删除,如果有的話就在空閑任務中釋放被删除任務的任務堆棧和任務控制塊的記憶體。
- 運作使用者設定的空閑任務鈎子函數。
-
判斷是否開啟低功耗 tickless 模式,如果開啟的話還需要做相應的處理
空閑任務的任務優先級是最低的,為 0.
空閑線程是一個線程狀态永遠為就緒态的線程.
應用執行個體
光看枯燥的知識可能不太容易了解,下面我們來舉個例子。
下面我們的項目,設計 4 個任務:start_task、key_task、task1_task 和 task2_task,這四個任務的任務功能如下:
start_task:用來建立其他 3 個任務。
key_task: 按鍵服務任務,檢測按鍵的按下結果,根據不同的按鍵結果執行不同的操作。
task1_task:應用任務 1。
task2_task: 應用任務 2。
實驗需要四個按鍵,KEY0、KEY1、KEY2 和 KEY_UP,這四個按鍵的功能如下:
KEY0: 此按鍵為中斷模式,在中斷服務函數中恢複任務 2 的運作。
KEY1: 此按鍵為輸入模式,用于恢複任務 1 的運作。
KEY2: 此按鍵為輸入模式,用于挂起任務 2 的運作。
KEY_UP: 此按鍵為輸入模式,用于挂起任務 1 的運作。
(1)、start_task 任務,用于建立其他 3 個任務。
(2)、在 key_tssk 任務裡面,KEY_UP 被按下,調用函數 vTaskSuspend()挂起任務 1。
(3)、KEY1 被按下,調用函數 vTaskResume()恢複任務 1 的運作。
(4)、KEY2 被按下,調用函數 vTaskSuspend()挂起任務 2。
(5)、任務 1 的任務函數,用于觀察任務挂起和恢複的過程。
(6)、任務 2 的任務函數,用于觀察任務挂起和恢複的過程(中斷方式)。
//任務優先級
#define START_TASK_PRIO 1
//任務堆棧大小
#define START_STK_SIZE 128
//任務句柄
TaskHandle_t StartTask_Handler;
//任務函數
void start_task(void *pvParameters);
//任務優先級
#define KEY_TASK_PRIO 2
//任務堆棧大小
#define KEY_STK_SIZE 128
//任務句柄
TaskHandle_t KeyTask_Handler;
//任務函數
void key_task(void *pvParameters);
//任務優先級
#define TASK1_TASK_PRIO 3
//任務堆棧大小
#define TASK1_STK_SIZE 128
//任務句柄
TaskHandle_t Task1Task_Handler;
//任務函數
void task1_task(void *pvParameters);
//任務優先級
#define TASK2_TASK_PRIO 4
//任務堆棧大小
#define TASK2_STK_SIZE 128
//任務句柄
TaskHandle_t Task2Task_Handler;
//任務函數
void task2_task(void *pvParameters);
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//設定系統中斷優先級分組4
delay_init(); //延時函數初始化
uart_init(115200); //初始化序列槽
LED_Init(); //初始化LED
KEY_Init(); //初始化按鍵
EXTIX_Init(); //初始化外部中斷
//建立開始任務
xTaskCreate((TaskFunction_t )start_task, //任務函數
(const char* )"start_task", //任務名稱
(uint16_t )START_STK_SIZE, //任務堆棧大小
(void* )NULL, //傳遞給任務函數的參數
(UBaseType_t )START_TASK_PRIO, //任務優先級
(TaskHandle_t* )&StartTask_Handler); //任務句柄
vTaskStartScheduler(); //開啟任務排程
}
//開始任務任務函數
void start_task(void *pvParameters)
{
taskENTER_CRITICAL(); //進入臨界區
//建立KEY任務
xTaskCreate((TaskFunction_t )key_task,
(const char* )"key_task",
(uint16_t )KEY_STK_SIZE,
(void* )NULL,
(UBaseType_t )KEY_TASK_PRIO,
(TaskHandle_t* )&KeyTask_Handler);
//建立TASK1任務
xTaskCreate((TaskFunction_t )task1_task,
(const char* )"task1_task",
(uint16_t )TASK1_STK_SIZE,
(void* )NULL,
(UBaseType_t )TASK1_TASK_PRIO,
(TaskHandle_t* )&Task1Task_Handler);
//建立TASK2任務
xTaskCreate((TaskFunction_t )task2_task,
(const char* )"task2_task",
(uint16_t )TASK2_STK_SIZE,
(void* )NULL,
(UBaseType_t )TASK2_TASK_PRIO,
(TaskHandle_t* )&Task2Task_Handler);
vTaskDelete(StartTask_Handler); //删除開始任務
taskEXIT_CRITICAL(); //退出臨界區
}
//key任務函數
void key_task(void *pvParameters)
{
u8 key,statflag=0;
while(1)
{
key=KEY_Scan(0);
switch(key)
{
case WKUP_PRES:
statflag=!statflag;
if(statflag==1)
{
vTaskSuspend(Task1Task_Handler);//挂起任務
printf("挂起任務1的運作!\r\n");
}
else if(statflag==0)
{
vTaskResume(Task1Task_Handler); //恢複任務1
printf("恢複任務1的運作!\r\n");
}
break;
case KEY1_PRES:
vTaskSuspend(Task2Task_Handler);//挂起任務2
printf("挂起任務2的運作!\r\n");
break;
}
vTaskDelay(10); //延時10ms
}
}
//task1任務函數
void task1_task(void *pvParameters)
{
u8 task1_num=0;
printf("Task1 Run:000");
while(1)
{
task1_num++; //任務執1行次數加1 注意task1_num1加到255的時候會清零!!
LED0=!LED0;
printf("任務1已經執行:%d次\r\n",task1_num);
printf("%d",task1_num); //顯示任務執行次數
vTaskDelay(1000); //延時1s,也就是1000個時鐘節拍
}
}
//task2任務函數
void task2_task(void *pvParameters)
{
u8 task2_num=0;
printf("Task2 Run:000");
while(1)
{
task2_num++; //任務2執行次數加1 注意task1_num2加到255的時候會清零!!
LED1=!LED1;
printf("任務2已經執行:%d次\r\n",task2_num);
vTaskDelay(1000); //延時1s,也就是1000個時鐘節拍
}
}
複制
一開始任務 1 和任務 2 都正常運作,當挂起任務 1 或者任務 2 以後,任務 1 或者任務 2 就會停止運作,直到下一次重新恢複任務 1 或者任務 2 的運作。重點是,儲存任務運作次數的變量都沒有發生資料丢失,如果用任務删除和重建的方法這些資料必然會丢失掉的