講完了線程同步的機制,我們要開始線程通訊的學習,
線程通訊中的郵箱消息隊列也屬于 RT-Thread 的IPC機制。
目錄
前言
一、郵箱
1.1 郵箱控制塊
1.2 郵箱操作
1.2.1 創建和刪除
1.2.2 初始化和脫離
1.2.3 發送郵件
1.2.4 接收郵件
1.3 示例(指針傳遞)
1.3.1 郵箱消息傳遞
1.3.2 郵箱個數示例
二、消息隊列
2.1 消息隊列控制塊
2.2 消息隊列操作
2.2.1 創建和刪除
2.2.2 初始化和脫離
2.2.3 發送消息
2.2.4 接收消息
2.3 消息隊列原理簡析
2.4 示例(消息隊列原理理解)
2.4.1 理解消息隊列原理
2.4.2 消息傳遞
結語
前言
與上篇文章的介紹的信號量、互斥量和事件集,郵箱、消息隊列同樣為 RT-Thread IPC機制。但是信號量它們屬于線程同步機制,并不能在線程之間傳遞消息,我們本文介紹的 郵箱、消息隊列就是實現線程間消息傳遞的機制。
相對于上一篇文章的內容,線程通訊的學習會相對復雜些,因為涉及到消息的傳遞,消息在實際項目中的可能存在多種不同的情況,所以 郵箱和消息隊列的使用場景和方式是關鍵,尤其是消息隊列。基本上實際項目中的所有消息類型都可以使用消息隊列的方式。消息隊列應用于串口通信我會單獨用一篇博文來說明,本文先做基礎介紹和基本示例的講解。
本 RT-Thread 專欄記錄的開發環境:
RT-Thread記錄(一、RT-Thread 版本、RT-Thread Studio開發環境 及 配合CubeMX開發快速上手)
RT-Thread記錄(二、RT-Thread內核啟動流程 — 啟動文件和源碼分析
RT-Thread 內核篇系列博文鏈接:
RT-Thread記錄(三、RT-Thread 線程操作函數及線程管理與FreeRTOS的比較)
RT-Thread記錄(四、RT-Thread 時鐘節拍和軟件定時器)
RT-Thread記錄(五、RT-Thread 臨界區保護)
RT-Thread記錄(六、IPC機制之信號量、互斥量和事件集)
一、郵箱
RT-Thread 中的郵件是線程、中斷服務、定時器向線程發送消息的有效手段(中斷和定時器需要非阻塞方式,不能等待發送,也不能接收)。
郵箱中的每一封郵件只能容納固定的 4 字節內容(32位內核正好可以傳遞一個指針)。
郵箱特點 RAM空間占用少,效率較高。
RT-Thread 有點類似 FreeRTOS 的任務通知,同樣的只能傳遞4個字節內容。
但是 FreeRTOS 的任務通知是屬于任務自己的,每個任務有且只有一個通知,
而 RT-Thread 的郵箱由郵箱控制塊統一管理,新建一個郵箱,可以包含多封郵件(每封4個字節)。
1.1 郵箱控制塊
老規矩用源碼,解釋看注釋(使用起來也方便復制 ~ ~!)
#ifdef RT_USING_MAILBOX
/**
* mailbox structure
*/
struct rt_mailbox
{
struct rt_ipc_object parent; /**< inherit from ipc_object */
rt_ubase_t *msg_pool; /**< 郵箱緩沖區的開始地址 */
rt_uint16_t size; /**< 郵箱緩沖區的大小 */
rt_uint16_t entry; /**< 郵箱中郵件的數目 */
rt_uint16_t in_offset; /**< 郵箱緩沖的入口指針 */
rt_uint16_t out_offset; /**< 郵箱緩沖的出口指針 */
rt_list_t suspend_sender_thread; /**< 發送線程的掛起等待隊列 */
};
typedef struct rt_mailbox *rt_mailbox_t;
#endif
1.2 郵箱操作
1.2.1 創建和刪除
同以前的線程那些一樣,動態的方式,先定義一個郵箱結構體的指針變量,接收創建好的句柄。
創建郵箱:
/**
參數的含義:
1、name 郵箱名稱
2、size 郵箱容量(就是多少封郵件,4的倍數)
3、flag 郵箱標志,它可以取如下數值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO
返回值:
RT_NULL 創建失敗
郵箱對象的句柄 創建成功
*/
rt_mailbox_t rt_mb_create(const char *name, rt_size_t size, rt_uint8_t flag)
最后的 flag 和信號量一樣建議 RT_IPC_FLAG_PRIO
:
刪除郵箱:
/**
參數的含義:
mb 郵箱對象的句柄
返回
RT_EOK 成功
*/
rt_err_t rt_mb_delete(rt_mailbox_t mb)
1.2.2 初始化和脫離
靜態的方式,先定義一個郵箱結構體,然后對他進行初始化。
這里要注意,還要定義一個數組,用來做郵箱的內存空間,和靜態初始化線程一樣。
初始化郵箱:
/**
參數含義:
1、mb 郵箱對象的句柄,需要取自定義的結構體地址
2、name 郵箱名稱
3、msgpool 緩沖區指針(用戶自定義的數組的地址,第一個數組元素的地址)
4、size 郵箱容量(就是數組的大小/4)
5、flag 郵箱標志,它可以取如下數值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO
返回
RT_EOK 成功
*/
rt_err_t rt_mb_init(rt_mailbox_t mb,
const char *name,
void *msgpool,
rt_size_t size,
rt_uint8_t flag)
脫離郵箱:
/**
參數的含義:
mb 郵箱對象的句柄
返回
RT_EOK 成功
*/
rt_err_t rt_mb_detach(rt_mailbox_t mb)
1.2.3 發送郵件
在 RT-Thread 中發送郵件分為 有無等待方式發送郵件,以及發送緊急郵件。
在我建的工程版本中,并沒有發送緊急郵件函數了,這里按照工程源碼來說明,就不介紹發送緊急郵件的函數了,在一般的 STM32 應用中,個人認為緊急郵件有沒有都沒有影響!
無等待方式適用于所有的線程和中斷,等待方式不能用于中斷中!
無等待發送郵件:
/**
參數:
1、mb 郵箱對象的句柄
2、value 郵件內容
返回
RT_EOK 發送成功
-RT_EFULL 郵箱已經滿了
看函數原型,其實就是把等待方式發送的時間改成了0
*/
rt_err_t rt_mb_send(rt_mailbox_t mb, rt_ubase_t value)
{
return rt_mb_send_wait(mb, value, 0);
}
無等待發送其實就是使用等待方式發送郵件,等待時間為0。
等待方式發送郵件:
/**
參數:
1、mb 郵箱對象的句柄
2、value 郵件內容
3、timeout 超時時間
返回:
RT_EOK 發送成功
-RT_ETIMEOUT 超時
-RT_ERROR 失敗,返回錯誤
*/
rt_err_t rt_mb_send_wait(rt_mailbox_t mb,
rt_ubase_t value,
rt_int32_t timeout)
1.2.4 接收郵件
接收郵件時,除了指定接收郵件的郵箱句柄,并指定接收到的郵件存放位置(需要有一個變量來保存接收到的數據)。
/**
參數含義:
1、mb 郵箱對象的句柄,從哪個郵件控制塊取郵件
2、value 郵件內容,需要用一個變量保存
3、timeout 超時時間
返回值:
RT_EOK 接收成功
-RT_ETIMEOUT 超時
-RT_ERROR 失敗,返回錯誤
*/
rt_err_t rt_mb_recv(rt_mailbox_t mb, rt_ubase_t *value, rt_int32_t timeout)
1.3 示例(指針傳遞)
2個示例,第一個是正常的消息傳遞,第二個是與郵箱創建個數有關的引導示例。
1.3.1 郵箱消息傳遞
前面說到過,郵箱中的每一封郵件只能容納固定的 4 字節內容,但是4字節可以傳遞指針,我們分別做個簡單的演示。
示例中,我們使用兩個不同的按鍵來發送郵件,通過一個事件來接收郵件,并打印收到的郵件內容。
按鍵key3,發送4字節的內容,按鍵Key2,發送一個字符串指針:
郵件創建:
在接收線程中,我們打印出接收到的數值:
測試結果,兩個按鍵按下,線程不僅能收到直接傳過來的4字節數據,還能通過傳遞的指針發送一個字符串:
1.3.2 郵箱個數示例
在上面的例子中,我們開始創建的郵箱大小就一個,我們測試下,如果沒有線程接收,是不是就會打印郵箱滿的消息,我們把線程接收郵箱代碼注釋掉,其他還是和前面測試一樣:
我們再來改一下,使用一個按鍵測試一下這個 size 是字節呢,還是直接是郵件個數,直接看圖說明:
在靜態初始化郵件時候,我們需要注意我們開辟的空間大小,需要是4的倍數,我們一般都是用數組除以4直接表示郵箱的size
大小,如下:
RT-Thread 是通過控制塊來管理這些IPC機制,在實際測試中,為了加深對某個對象的理解,比如這里的郵箱,可以直接打印出郵箱的參數來查看當前郵箱的狀態。學會測試!!!
二、消息隊列
消息隊列能夠接收來自線程或中斷服務例程中不固定長度的消息,并把消息緩存在自己的內存空間中。
消息隊列和郵箱的區別是長度并不限定在 4 個字節以內,但是如果如果把消息隊列的每條消息的最大字節規定在4個字節以內,那么消息隊列就和郵箱一樣了。
典型應用,使用串口接收不定長數據(后期會單獨有博文介紹消息隊列在串口接收上的應用)。
2.1 消息隊列控制塊
消息隊列控制塊的這些屬性,我們等會用示例來打印出來看,加深一下對這些屬性的認識。
#ifdef RT_USING_MESSAGEQUEUE
/**
* message queue structure
*/
struct rt_messagequeue
{
struct rt_ipc_object parent; /**< inherit from ipc_object */
void *msg_pool; /**< 消息隊列的開始地址 */
rt_uint16_t msg_size; /**< 每個消息長度 */
rt_uint16_t max_msgs; /**< 最大的消息數量 */
rt_uint16_t entry; /**< 已經有的消息數 */
void *msg_queue_head; /**< list head 鏈表頭 */
void *msg_queue_tail; /**< list tail 鏈表尾*/
void *msg_queue_free; /**< 空閑消息鏈表 */
rt_list_t suspend_sender_thread; /**< 掛起的發送線程 */
};
typedef struct rt_messagequeue *rt_mq_t;
#endif
2.2 消息隊列操作
2.2.1 創建和刪除
先定義一個郵箱結構體的指針變量,接收創建好的句柄。
創建消息隊列:
/**
參數:
1、name 消息隊列的名稱
2、msg_size 消息隊列中一條消息的最大長度,單位字節
3、max_msgs 消息隊列的最大個數
4、flag 消息隊列采用的等待方式,它可以取如下數值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO
返回:
RT_EOK 發送成功
消息隊列對象的句柄 成功
RT_NULL 失敗
*/
rt_mq_t rt_mq_create(const char *name,
rt_size_t msg_size,
rt_size_t max_msgs,
rt_uint8_t flag)
注意!msg_size
單位是字節,在32位系統中 RT-Thread 默認#define RT_ALIGN_SIZE 4
,所以如果 msg_size
不是4字節對齊,系統會自動補全。
比如用戶定義為9,那么系統會自動把消息隊列大小設置為 12,定義為1,設置為4。
還有flag
的使用,依然得注意一下,和郵箱信號量等一樣,注意實時性問題。
刪除消息隊列:
/**
參數
mq 消息隊列對象的句柄
返回
RT_EOK 成功
*/
rt_err_t rt_mq_delete(rt_mq_t mq)
2.2.2 初始化和脫離
靜態的方式,先定義一個消息隊列結構體,然后對他進行初始化。
初始化消息隊列:
/**
參數:
1、mq 消息隊列對象的句柄,需要取自定義的結構體地址
2、name 名稱
3、msgpool 存放消息的地址
4、msg_size 消息隊列中一條消息的最大長度,單位字節
5、pool_size 存放消息的緩沖區大小
6、flag 消息隊列采用的等待方式,
返回:
RT_EOK 成功
*/
rt_err_t rt_mq_init(rt_mq_t mq,
const char *name,
void *msgpool,
rt_size_t msg_size,
rt_size_t pool_size,
rt_uint8_t flag)
脫離消息隊列:
/**
參數:
mq 消息隊列對象的句柄
返回:
RT_EOK 成功
*/
rt_err_t rt_mq_detach(rt_mq_t mq)
2.2.3 發送消息
和郵件一樣,在 RT-Thread 中發送郵件分為 有無等待方式發送,以及緊急消息發送。
無等待方式適用于所有的線程和中斷,等待方式不能用于中斷中!
無等待發送消息:
/**
看函數原型,其實就是把等待方式發送的時間改成了0
參數:
1、mq 消息隊列對象的句柄
2、buffer 消息內容
3、size 消息大小
返回:
RT_EOK 成功
-RT_EFULL 消息隊列已滿
-RT_ERROR 失敗,表示發送的消息長度大于消息隊列中消息的最大長度
*/
rt_err_t rt_mq_send(rt_mq_t mq, const void *buffer, rt_size_t size)
{
return rt_mq_send_wait(mq, buffer, size, 0);
}
等待方式發送郵件:
/**
除了最后多一個時間,其他參數,和上面無等待方式一樣
timeout 超時時間(時鐘節拍)
*/
rt_err_t rt_mq_send_wait(rt_mq_t mq,
const void *buffer,
rt_size_t size,
rt_int32_t timeout)
發送緊急消息:
/**
參數:
1、mq 消息隊列對象的句柄
2、buffer 消息內容
3、size 消息大小
返回:
RT_EOK 成功
-RT_EFULL 消息隊列已滿
-RT_ERROR 失敗
*/
rt_err_t rt_mq_urgent(rt_mq_t mq, const void *buffer, rt_size_t size)
2.2.4 接收消息
接收消息時,接收者需指定存儲消息的消息隊列對象句柄,并且指定一個內存緩沖區,接收到的消息內容將被復制到該緩沖區里。
/**
參數:
mq 消息隊列對象的句柄
buffer 消息內容
size 消息大小
timeout 指定的超時時間
返回:
RT_EOK 成功收到
-RT_ETIMEOUT 超時
-RT_ERROR 失敗,返回錯誤
*/
rt_err_t rt_mq_recv(rt_mq_t mq,
void *buffer,
rt_size_t size,
rt_int32_t timeout)
2.3 消息隊列原理簡析
消息隊列控制塊:
要理解 消息隊列 的原理,就得從他初始化的狀態開始說起:
發送消息,其實所有的步驟都是在rt_mq_send_wait
函數中的,再次強調,學會看源碼!
關鍵的幾個地方說明一下:
當然這里沒有特意的說明等待時間問題,因為發送和接收都可以阻塞等待,這里不是要理解的重點。
發送完完成以后如果發現有線程在等待消息隊列,會發生一次調度:
接收消息,其實類似,可以自己查看源碼,試著分析。
對于上述過程的理解,我單獨寫了個例子,結合例子去理解上面的步驟,更加直觀!請看下面 理解消息隊列原理示例。
2.4 示例(消息隊列原理理解)
2個示例,第一個為了更加直觀的理解消息隊列原理,第二個是簡單的消息傳遞。
對于典型的串口接收不定長度數據的示例,我會單獨使用一篇文章來介紹。
2.4.1 理解消息隊列原理
我們在上面 《2.3 消息隊列原理簡析》 分析了一下消息隊列的原理,我們再來通過一個例子直觀的加深一下理解。
新建一個消息隊列(注意新建時候的參數):
我們2個按鍵,通過Key2按鍵發送消息:
通過 Key3 打印 消息隊列 對應的狀態值:
我們測試的時候,通過觀察消息隊列初始化以后的狀態,然后每次發送以后觀察 head,tail,free的變化情況,加深我們對消息隊列的理解:
通過上面的示例再去理解消息隊列的原理,就很直觀了,如果有消息接收,觀察地址的變化,同樣的可以分析出接收消息時候的原理。
2.4.2 消息傳遞
消息傳遞相對來說,就簡單多了,直接在上面的基礎上,新建一個任務接收消息(因為沒有做長度識別,這里沒有做解析):
還是通過上面的Key2按鍵發送消息:
結語
本文雖然只介紹了2個IPC機制,但是在項目中,它們的使用無處不在。
消息隊列的應用在我們實際使用中,是很重要的,串口通信接收數據就是使用消息隊列來實現。對于消息隊列的串口應用,我會單獨開一片博文來總結。
本文針對消息隊列的實現原理給出了很好的示例,還是那句話,學會多看源碼,多動手測試!
我會用心寫好每一篇博文,希望大家支持!謝謝!
審核編輯:湯梓紅
-
IPC
+關注
關注
3文章
345瀏覽量
51830 -
線程
+關注
關注
0文章
504瀏覽量
19651 -
RT-Thread
+關注
關注
31文章
1273瀏覽量
39925 -
消息隊列
+關注
關注
0文章
33瀏覽量
2969
發布評論請先 登錄
相關推薦
評論