一、前言
從這一講開始,我們進(jìn)入RT-thread內(nèi)核的學(xué)習(xí),這是操作系統(tǒng)和裸機(jī)的區(qū)別,也是操作系統(tǒng)的核心所在,關(guān)于內(nèi)核的基礎(chǔ)我就不介紹了,大家可以先去官網(wǎng)上了解一下什么是內(nèi)核。我這一講重點講解內(nèi)核的線程管理,關(guān)于內(nèi)核的其他內(nèi)容我后續(xù)會接著講。
事先說明,本人也是剛接觸RT-thread不久,有些理解可能還比較顯淺,如果大家發(fā)現(xiàn)有什么錯誤,請一定要指正,謝謝!??!
然后就是文中有一些概念的定義和描述我是直接在官網(wǎng)上面抄過來的,因為我覺得官網(wǎng)上有些有些概念寫的非常清晰明了,我自己去描述的話可能還會帶大家繞彎。
內(nèi)核基礎(chǔ)知識:https://www.rt-thread.org/document/site/programming-manual/basic/basic/#
源碼鏈接
我發(fā)布的所有關(guān)于RT-thread的教程源代碼都在下面這個鏈接里面,隨著我教程的更新,新的代碼也會加入進(jìn)去。
教程源碼下載鏈接:https://pan.baidu.com/s/1N2D8dM31deKIqNqaIQfPiA
提取碼:7nsx
二、線程介紹
什么是線程
玩過單片機(jī)的同學(xué)應(yīng)該都知道,裸機(jī)程序在運(yùn)行完啟動程序之后總是會從main函數(shù)開始執(zhí)行用戶的程序。
我們這里假設(shè)有這么一份代碼,main函數(shù)里面有一個while循環(huán),里面調(diào)用了兩個函數(shù),函數(shù)A和函數(shù)B,main函數(shù)以外有一個中斷服務(wù)函數(shù)C。函數(shù)A是一個溫濕度采集函數(shù),函數(shù)B是LCD顯示函數(shù),顯示溫濕度的值,函數(shù)C是按鍵的中斷服務(wù)函數(shù),按鍵按下的時候停止溫濕度的采集。其實這三個函數(shù)我們就可以把它看作是三個線程,函數(shù)A和函數(shù)B是優(yōu)先級等同的兩個線程,正常運(yùn)行的時候,先運(yùn)行函數(shù)A,再運(yùn)行函數(shù)B,如此循環(huán),如果中斷觸發(fā)了,優(yōu)先級更高的函數(shù)C就會被優(yōu)先處理,整個程序運(yùn)行的流程其實就體現(xiàn)了單線程的線程管理和調(diào)度。
RT-thread的線程管理其實也是一樣的,線程是實現(xiàn)任務(wù)的載體,它是 RT-Thread 中最基本的調(diào)度單位,它描述了一個任務(wù)執(zhí)行的運(yùn)行環(huán)境,也描述了這個任務(wù)所處的優(yōu)先等級,重要的任務(wù)可設(shè)置相對較高的優(yōu)先級,非重要的任務(wù)可以設(shè)置較低的優(yōu)先級,不同的任務(wù)還可以設(shè)置相同的優(yōu)先級,輪流運(yùn)行。
線程的優(yōu)點
到這里,不知道大家有沒有一個疑惑,既然裸機(jī)程序函數(shù)的調(diào)度相當(dāng)于線程的調(diào)度,那為什么裸機(jī)的程序沒有線程這個概念呢?它們運(yùn)行的時候本質(zhì)都是一樣的呀,為什么很多人都說操作系統(tǒng)的運(yùn)行效率更高呢?
我的理解是這樣的,裸機(jī)的函數(shù)調(diào)度的方式比較單一,就是不斷循環(huán)的調(diào)用這些函數(shù),函數(shù)的優(yōu)先級也不多。而操作系統(tǒng)的每一個線程都可以設(shè)置優(yōu)先級,我們可以根據(jù)需要把重要的任務(wù)分配更高的優(yōu)先級,從而使得線程的調(diào)度不是單一的死循環(huán)調(diào)度,在多線程調(diào)度的時候,操作系統(tǒng)的存在使得代碼的運(yùn)行效率大大的提高。
舉個例子,有一個函數(shù)A,里面有多個延時函數(shù),有幾ms的,也有幾十ms的,那么這個函數(shù)A如果在裸機(jī)的程序里面跑的時候必須規(guī)規(guī)矩矩等待延時結(jié)束再往下跑,而如果是在RT-thread操作系統(tǒng)里面跑的話,當(dāng)執(zhí)行延時函數(shù)的時候,函數(shù)A的線程會被掛起,然后執(zhí)行其他線程里面優(yōu)先級最高的,等延時時間到了再返回,繼續(xù)在函數(shù)A往下跑。這樣一來,當(dāng)代碼量比較龐大,函數(shù)關(guān)系復(fù)雜的時候,操作系統(tǒng)的優(yōu)勢就體現(xiàn)出來了。
線程棧
RT-Thread 線程具有獨立的棧,當(dāng)進(jìn)行線程切換時,會將當(dāng)前線程的上下文存在棧中,當(dāng)線程要恢復(fù)運(yùn)行時,再從棧中讀取上下文信息,進(jìn)行恢復(fù)。
線程棧還用來存放函數(shù)中的局部變量:函數(shù)中的局部變量從線程??臻g中申請;函數(shù)中局部變量初始時從寄存器中分配(ARM 架構(gòu)),當(dāng)這個函數(shù)再調(diào)用另一個函數(shù)時,這些局部變量將放入棧中。
線程的類型
RT-thread的線程有兩種類型,分別是系統(tǒng)線程和用戶線程.
系統(tǒng)線程是由 RT-Thread 內(nèi)核創(chuàng)建的線程
用戶線程是由我們自己創(chuàng)建的線程,當(dāng)創(chuàng)建的線程被啟動之后就會加入任務(wù)的調(diào)度。
線程的狀態(tài)
線程在運(yùn)行的時候有多種狀態(tài),具體如下表所示:
線程的優(yōu)先級
因為RT-Thread 的線程調(diào)度器是搶占式的,所以線程的優(yōu)先級非常重要。RT-Thread在運(yùn)行的時候會先從就緒線程列表中查找最高優(yōu)先級的線程運(yùn)行,運(yùn)行完了或者需要延時的時候會讓出cpu的使用權(quán),讓就緒線程列表中該線程以外的優(yōu)先級最高的線程運(yùn)行。
RT-Thread 最大支持 256 個線程優(yōu)先級 (0~255),數(shù)值越小的優(yōu)先級越高,0 為最高優(yōu)先級。在一些資源比較緊張的系統(tǒng)中,可以根據(jù)實際情況選擇只支持 8 個或 32 個優(yōu)先級的系統(tǒng)配置;對于 ARM Cortex-M 系列,普遍采用 32 個優(yōu)先級。最低優(yōu)先級默認(rèn)分配給空閑線程使用,用戶一般不使用。在系統(tǒng)中,當(dāng)有比當(dāng)前線程優(yōu)先級更高的線程就緒時,當(dāng)前線程將立刻被換出,高優(yōu)先級線程搶占處理器運(yùn)行。
線程時間片
每個線程都有時間片這個參數(shù),但時間片僅對優(yōu)先級相同的就緒態(tài)線程有效。系統(tǒng)對優(yōu)先級相同的就緒態(tài)線程采用時間片輪轉(zhuǎn)的調(diào)度方式進(jìn)行調(diào)度時,時間片起到約束線程單次運(yùn)行時長的作用。
假設(shè)有 2 個優(yōu)先級相同的就緒態(tài)線程 A 與 B,A 線程的時間片設(shè)置為 10,B 線程的時間片設(shè)置為 5,那么當(dāng)系統(tǒng)中不存在比 A 優(yōu)先級高的就緒態(tài)線程時,系統(tǒng)會在 A、B 線程間來回切換執(zhí)行,并且每次對 A 線程執(zhí)行 10 個節(jié)拍的時長,對 B 線程執(zhí)行 5 個節(jié)拍的時長。
線程入口函數(shù)
我前面也說過了,一個函數(shù)可以當(dāng)做是一個線程,在RT-Thread實際的操作中也是一樣,我們每創(chuàng)建一個線程,就必須創(chuàng)建一個線程的入口函數(shù)。
線程的入口函數(shù)一般有以下兩種代碼形式:
無限循環(huán)模式:
使用這種模式的時候需要注意的是,該線程一旦被運(yùn)行,就會導(dǎo)致優(yōu)先級比它低的線程一直不能夠被調(diào)度,因為這個線程本身是一個死循環(huán),它能夠一直在低優(yōu)先級線程前面被調(diào)度。
如果我們需要死循環(huán)但是又不需要一直死循環(huán)的時候,可以把這個線程配置較低的優(yōu)先級,或者在函數(shù)里面調(diào)用延時函數(shù),從而使得該線程能夠讓出cpu使用權(quán)。
void thread_entry(void* paramenter)
{
while (1)
{
/* 等待事件的發(fā)生 */
/* 對事件進(jìn)行服務(wù)、進(jìn)行處理 */
}
}
順序執(zhí)行或有限次循環(huán)模式:
此類線程不會循環(huán)或不會永久循環(huán),可謂是 “一次性” 線程,一定會被執(zhí)行完畢。而且在執(zhí)行完畢后,線程將被系統(tǒng)自動刪除。比如下面這種簡單的順序語句或者do whlie()、for()循環(huán)等。
static void thread_entry(void* parameter)
{
/* 處理事務(wù) #1 */
…
/* 處理事務(wù) #2 */
…
/* 處理事務(wù) #3 */
}
線程錯誤碼
一個線程就是一個執(zhí)行場景,錯誤碼是與執(zhí)行環(huán)境密切相關(guān)的,所以每個線程配備了一個變量用于保存錯誤碼,線程的錯誤碼有以下幾種。
#define RT_EOK 0 /* 無錯誤 */
#define RT_ERROR 1 /* 普通錯誤 */
#define RT_ETIMEOUT 2 /* 超時錯誤 */
#define RT_EFULL 3 /* 資源已滿 */
#define RT_EEMPTY 4 /* 無資源 */
#define RT_ENOMEM 5 /* 無內(nèi)存 */
#define RT_ENOSYS 6 /* 系統(tǒng)不支持 */
#define RT_EBUSY 7 /* 系統(tǒng)忙 */
#define RT_EIO 8 /* IO 錯誤 */
#define RT_EINTR 9 /* 中斷系統(tǒng)調(diào)用 */
#define RT_EINVAL 10 /* 非法參數(shù) */
線程狀態(tài)切換
RT-Thread 提供一系列的操作系統(tǒng)調(diào)用接口,使得線程的狀態(tài)在這五個狀態(tài)之間來回切換。幾種狀態(tài)間的轉(zhuǎn)換關(guān)系如下圖所示:
1:線程通過調(diào)用函數(shù) rt_thread_create/init() 進(jìn)入到初始狀態(tài)(RT_THREAD_INIT)。
2:初始狀態(tài)的線程通過調(diào)用函數(shù) rt_thread_startup() 進(jìn)入到就緒狀態(tài)(RT_THREAD_READY);
3:就緒狀態(tài)的線程被調(diào)度器調(diào)度后進(jìn)入運(yùn)行狀態(tài)(RT_THREAD_RUNNING)。
4:當(dāng)處于運(yùn)行狀態(tài)的線程調(diào)用 rt_thread_delay(),rt_sem_take(),rt_mutex_take(),rt_mb_recv() 等函數(shù)或者獲取不到資源時,將進(jìn)入到掛起狀態(tài)(RT_THREAD_SUSPEND)。
5:掛起狀態(tài)的線程,如果等待超時依然未能獲得資源或由于其他線程釋放了資源,那么它將返回到就緒狀態(tài)。
6:掛起狀態(tài)的線程,如果調(diào)用 rt_thread_delete/detach() 函數(shù),將更改為關(guān)閉狀態(tài)(RT_THREAD_CLOSE)。
7:運(yùn)行狀態(tài)的線程,如果運(yùn)行結(jié)束,就會在線程的最后部分執(zhí)行 rt_thread_exit() 函數(shù),將狀態(tài)更改為關(guān)閉狀態(tài)。
#1:線程通過調(diào)用函數(shù) rt_thread_create/init() 進(jìn)入到初始狀態(tài)(RT_THREAD_INIT)。
2:初始狀態(tài)的線程通過調(diào)用函數(shù) rt_thread_startup() 進(jìn)入到就緒狀態(tài)(RT_THREAD_READY);
3:就緒狀態(tài)的線程被調(diào)度器調(diào)度后進(jìn)入運(yùn)行狀態(tài)(RT_THREAD_RUNNING)。
4:當(dāng)處于運(yùn)行狀態(tài)的線程調(diào)用 rt_thread_delay(),rt_sem_take(),rt_mutex_take(),rt_mb_recv() 等函數(shù)或者獲取不到資源時,將進(jìn)入到掛起狀態(tài)(RT_THREAD_SUSPEND)。
5:掛起狀態(tài)的線程,如果等待超時依然未能獲得資源或由于其他線程釋放了資源,那么它將返回到就緒狀態(tài)。
6:掛起狀態(tài)的線程,如果調(diào)用 rt_thread_delete/detach() 函數(shù),將更改為關(guān)閉狀態(tài)(RT_THREAD_CLOSE)。
7:運(yùn)行狀態(tài)的線程,如果運(yùn)行結(jié)束,就會在線程的最后部分執(zhí)行 rt_thread_exit() 函數(shù),將狀態(tài)更改為關(guān)閉狀態(tài)。
系統(tǒng)線程
系統(tǒng)線程是指由系統(tǒng)創(chuàng)建的線程,用戶線程是由用戶程序調(diào)用線程管理接口創(chuàng)建的線程,在 RT-Thread 內(nèi)核中的系統(tǒng)線程有空閑線程和主線程。
空閑線程
空閑線程是系統(tǒng)創(chuàng)建的最低優(yōu)先級的線程,線程狀態(tài)永遠(yuǎn)為就緒態(tài)。當(dāng)系統(tǒng)中無其他就緒線程存在時,調(diào)度器將調(diào)度到空閑線程,它通常是一個死循環(huán),且永遠(yuǎn)不能被掛起。關(guān)于空閑線程在 RT-Thread的特殊用途,我在以后的教程里面再說。
主線程
在系統(tǒng)啟動時,系統(tǒng)會創(chuàng)建 main 線程,用戶可以在 main() 函數(shù)里添加自己的應(yīng)用程序初始化代碼。但是在實際的項目運(yùn)用中,為了方便管理每一個功能模塊,我建議大家最好不要在main() 函數(shù)里面寫應(yīng)用的代碼,原因我這里不多說了,在后續(xù)的教程中我會一一講解。
三、編程講解
我這里只介紹線程最常用的最基本的用法,如果你們還想學(xué)習(xí)進(jìn)階的用法,可以到官網(wǎng)上查閱相關(guān)的資料。
第一步: 創(chuàng)建/初始化線程
創(chuàng)建線程創(chuàng)建的是一個動態(tài)線程,即線程??臻g的內(nèi)存是由系統(tǒng)自由分配的。初始化線程創(chuàng)建的是一個靜態(tài)線程,線程??臻g的內(nèi)存由用戶自己去指定。
創(chuàng)建線程函數(shù)如下圖所示:
系統(tǒng)線程
系統(tǒng)線程是指由系統(tǒng)創(chuàng)建的線程,用戶線程是由用戶程序調(diào)用線程管理接口創(chuàng)建的線程,在 RT-Thread 內(nèi)核中的系統(tǒng)線程有空閑線程和主線程。
空閑線程:
空閑線程是系統(tǒng)創(chuàng)建的最低優(yōu)先級的線程,線程狀態(tài)永遠(yuǎn)為就緒態(tài)。當(dāng)系統(tǒng)中無其他就緒線程存在時,調(diào)度器將調(diào)度到空閑線程,它通常是一個死循環(huán),且永遠(yuǎn)不能被掛起。關(guān)于空閑線程在 RT-Thread的特殊用途,我在以后的教程里面再說。
主線程:
在系統(tǒng)啟動時,系統(tǒng)會創(chuàng)建 main 線程,用戶可以在 main() 函數(shù)里添加自己的應(yīng)用程序初始化代碼。但是在實際的項目運(yùn)用中,為了方便管理每一個功能模塊,我建議大家最好不要在main() 函數(shù)里面寫應(yīng)用的代碼,原因我這里不多說了,在后續(xù)的教程中我會一一講解。
三、編程講解
我這里只介紹線程最常用的最基本的用法,如果你們還想學(xué)習(xí)進(jìn)階的用法,可以到官網(wǎng)上查閱相關(guān)的資料。
第一步: 創(chuàng)建/初始化線程
創(chuàng)建線程創(chuàng)建的是一個動態(tài)線程,即線程??臻g的內(nèi)存是由系統(tǒng)自由分配的。初始化線程創(chuàng)建的是一個靜態(tài)線程,線程棧空間的內(nèi)存由用戶自己去指定。
創(chuàng)建線程函數(shù)如下圖所示:
編程示例:
/* 創(chuàng)建線程:led0 */
rt_thread_t thread1 = rt_thread_create("led0", //線程名稱
led0_entry, //線程入口函數(shù)
RT_NULL, //線程入口函數(shù)參數(shù)
1024, //線程棧大小
25, //優(yōu)先級
10); //時間片
初始化線程函數(shù)的定義如下圖所示:
編程示例:
/* 靜態(tài)線程參數(shù)定義 */
ALIGN(RT_ALIGN_SIZE)
static char led1_stack[1024]; //線程棧內(nèi)存空間
static struct rt_thread led1; //線程句柄
/* 創(chuàng)建線程:led1 */
rt_thread_init(&led1, //線程句柄
"led1", //線程名稱
led1_entry, //線程入口函數(shù)
RT_NULL, //線程入口函數(shù)參數(shù)
&led1_stack[0], //線程棧起始地址
sizeof(led1_stack), //線程棧大小
THREAD_PRIORITY - 1, //優(yōu)先級
THREAD_TIMESLICE); //時間片
第二步:啟動線程
編程示例:
/* 創(chuàng)建線程:led0 */
rt_thread_t thread1 = rt_thread_create("led0", //線程名稱
led0_entry, //線程入口函數(shù)
RT_NULL, //線程入口函數(shù)參數(shù)
1024, //線程棧大小
25, //優(yōu)先級
10); //時間片
/* 如果線程創(chuàng)建成功,啟動這個線程 */
if (thread1 != RT_NULL)
{
rt_thread_startup(thread1);
}
第三步:編寫線程入口函數(shù)
線程的入口函數(shù)是由用戶自己定義的,我們只要保證入口函數(shù)的函數(shù)名和創(chuàng)建線程時所用的函數(shù)名一致即可。
編程示例:
```c
void led0_entry(void *parameter)
{
while(1)
{
rt_pin_write(LED0_PIN, PIN_LOW);
rt_thread_mdelay(1000);
rt_pin_write(LED0_PIN, PIN_HIGH);
rt_thread_mdelay(1000);
}
}
四、項目實戰(zhàn)
我這里在main函數(shù)里面創(chuàng)建了一個動態(tài)和一個靜態(tài)線程,分別讓led0和led1閃爍。其實在實際的項目應(yīng)用中,一般不會在main函數(shù)加入很多應(yīng)用的代碼,而是用命令的方式把函數(shù)加入應(yīng)用列表,這個我在下一講才會介紹,所以這里就先這樣寫吧。
代碼如下:
#include <rtthread.h>
#include <rtdevice.h>
#include <board.h>
#define LED0_PIN GET_PIN(F, 9)
#define LED1_PIN GET_PIN(F, 10)
#define THREAD_PRIORITY 25 //線程優(yōu)先級
#define THREAD_TIMESLICE 5 //線程時間片
/* 靜態(tài)線程參數(shù)定義 */
ALIGN(RT_ALIGN_SIZE)
static char led1_stack[1024]; //線程棧內(nèi)存空間
static struct rt_thread led1; //線程句柄
/* led0線程入口函數(shù) */
void led0_entry(void *parameter)
{
rt_pin_write(LED0_PIN, PIN_LOW);
rt_thread_mdelay(1000);
rt_pin_write(LED0_PIN, PIN_HIGH);
rt_thread_mdelay(1000);
}
/* led1線程入口函數(shù) */
void led1_entry(void *parameter)
{
rt_pin_write(LED1_PIN, PIN_LOW);
rt_thread_mdelay(300);
rt_pin_write(LED1_PIN, PIN_HIGH);
rt_thread_mdelay(300);
}
int main(void)
{
int i = 0;
/* 把LED引腳設(shè)置為輸出 */
rt_pin_mode(LED0_PIN, PIN_MODE_OUTPUT);
rt_pin_mode(LED1_PIN, PIN_MODE_OUTPUT);
/* 創(chuàng)建線程:led0 */
rt_thread_t thread1 = rt_thread_create("led0", //線程名稱
led0_entry, //線程入口函數(shù)
RT_NULL, //線程入口函數(shù)參數(shù)
1024, //線程棧大小
25, //優(yōu)先級
10); //時間片
/* 如果線程創(chuàng)建成功,啟動這個線程 */
if (thread1 != RT_NULL)
{
rt_thread_startup(thread1);
}
/* 創(chuàng)建線程:led1 */
rt_thread_init(&led1, //線程句柄
"led1", //線程名稱
led1_entry, //線程入口函數(shù)
RT_NULL, //線程入口函數(shù)參數(shù)
&led1_stack[0], //線程棧起始地址
sizeof(led1_stack), //線程棧大小
THREAD_PRIORITY - 1, //優(yōu)先級
THREAD_TIMESLICE); //時間片
rt_thread_startup(&led1);
while (1)
{
rt_thread_mdelay(100);
}
}
五、結(jié)束語
線程在RT-thread項目的實戰(zhàn)中運(yùn)用非常廣泛,因此,希望大家好好看一下線程相關(guān)的內(nèi)容,了解更多線程的用法。
好了,關(guān)于線程管理的編程講解就到這里,如果還有什么問題可以私信給我。如果需要本文對應(yīng)的源碼的話可以在博文前言部分的鏈接下載。
如果覺得這篇文章對你有用,點贊+關(guān)注支持一下博主唄。
后續(xù)我會繼續(xù)更新RT-thread入門教程系列,如果感興趣的同學(xué)可以關(guān)注一下博主,謝謝!