第十八章 消息队列(第二部分)
六、常用消息队列的函数
1、创建消息队列
在创建消息队列的时候,是需要用户自己定义消息队列的句柄的,但是定义了队列的句柄并不等于创建了队列,创建队列必须是调用rt_mq_create()函数进行创建
如果创建成功则返回消息队列句柄,如果是返回
实例
RT_NULL,则表示失败
1 /* 创建一个消息队列 */
2 test_mq = rt_mq_create("test_mq", /* 消息队列名字 */
3 40, /* 消息的最大长度 */
4 20, /* 消息队列的最大容量 */
5 RT_IPC_FLAG_FIFO);/* 队列模式 FIFO(0x00)*/
6 if (test_mq != RT_NULL)
7 rt_kprintf("消息队列创建成功!\n\n");
rt_mq_create()源码
1 rt_mq_t rt_mq_create(const char *name,
2 rt_size_t msg_size,
3 rt_size_t max_msgs,
4 rt_uint8_t flag)
5 {
6 struct rt_messagequeue *mq;
7 struct rt_mq_message *head;
8 register rt_base_t temp;
9
10 RT_DEBUG_NOT_IN_INTERRUPT;
11
12 /* 分配消息队列对象 */ (1)**分配消息队列对象**
13 mq = (rt_mq_t)rt_object_allocate(RT_Object_Class_MessageQueue, name);
14 if (mq == RT_NULL)
15 return mq;
16
17 /* 设置 parent */
18 mq->parent.parent.flag = flag; (2)**设置消息队列的阻塞唤醒模式**
19
20 /* 初始化消息队列内核对象 */
21 rt_ipc_object_init(&(mq->parent)); (3)**初始化消息队列内核对象**
22
23 /* 初始化消息队列 */
24
25 /* 获得正确的消息队列大小 */
26 mq->msg_size = RT_ALIGN(msg_size, RT_ALIGN_SIZE); (4)**设置消息队列的节点大小与消息队列的最大容量**
27 mq->max_msgs = max_msgs;
28
29 /* 分配消息内存池 */
30 mq->msg_pool = RT_KERNEL_MALLOC((mq->msg_size +
31 sizeof(struct rt_mq_message)) * mq->max_msgs);
32 if (mq->msg_pool == RT_NULL) { (5)
33 rt_mq_delete(mq);
34
35 return RT_NULL;
36 }
37
38 /* 初始化消息队列头尾链表 */
39 mq->msg_queue_head = RT_NULL; (6) **初始化消息队列头尾链表。**
40 mq->msg_queue_tail = RT_NULL;
41
42 /* 初始化消息队列空闲链表 */
43 mq->msg_queue_free = RT_NULL;
44 for (temp = 0; temp < mq->max_msgs; temp ++) { (7) :**将所有的消息队列的节点连接起来,形成空闲链表。**
45 head = (struct rt_mq_message *)((rt_uint8_t *)mq->msg_pool +
46 temp * (mq->msg_size + sizeof(struct rt_mq_message)));
47 head->next = mq->msg_queue_free;
48 mq->msg_queue_free = head;
49 }
50
51 /* 消息队列的个数为 0(清零)*/
52 mq->entry = 0; (8):**消息队列的个数为 0(清零)。**
53
54 return mq;
55 }
56 RTM_EXPORT(rt_mq_create);
(1):分配消息队列对象,调用 rt_object_allocate 将从对象系统分配对象,为创建的消息队列分配一个消息队列的对象,并且命名对象名称。
(2):设置消息队列的阻塞唤醒模式,
(3):初始化消息队列内核对象。此处会初始化一个链表,用于记录访问此队列而阻塞的线程,通过这个链表,可以找到对应的阻塞线程的控制块,从而能恢复线程。
(4):设置消息队列的节点大小与消息队列的最大容量,节点大小要按RT_ALIGN_SIZE 字节对齐,
消息队列的容量由用户自己定义。
(5):给此消息队列分配内存。每个消息节点中都有一个消息头,用于链表链接,指向下一个消息节点,作为消息的排序。
2、消息队列删除
- 队列删除函数是根据消息队列句柄直接删除的,删除之后这个消息队列的所有信息都会被系统回收清空,而且不能再次使用这个消息队列了。若消息队列没有被创建,无法删除。
- 删除消息队列的时候会把所有由于访问此消息队列而进入阻塞态的线程都从阻塞链表中删除
实例
1 /* 定义消息队列控制块 */
2 static rt_mq_t test_mq = RT_NULL;
3
4 rt_err_t uwRet = RT_EOK;
5
6 uwRet = rt_mq_delete(test_mq);
7 if (RT_EOK == uwRet)
8 rt_kprintf("消息队列删除成功!\n\n");
rt_mq_delete() 源码
1 rt_err_t rt_mq_delete(**rt_mq_t mq**) ,**mq 是 rt_mq_delete 传入的参数,
是消息队列句柄,表示的是要删除哪个想队列**
2 {
3 RT_DEBUG_NOT_IN_INTERRUPT;
4
5 /* 检查消息队列 */
6 RT_ASSERT(mq != RT_NULL); (1) :**检测消息队列是否被创建了,
如果是则可以进行删除操作。**
7
8 /* 恢复所有因为访问此队列而阻塞的线程 */
9 rt_ipc_list_resume_all(&(mq->parent.suspend_thread)); (2)
10
11 #if defined(RT_USING_MODULE) && defined(RT_USING_SLAB)
12 /* 消息队列对象属于应用程序模块 ,此处不使用 */
13 if (mq->parent.parent.flag & RT_OBJECT_FLAG_MODULE)
14 rt_module_free(mq->parent.parent.module_id, mq->msg_pool);
15 else
16 #endif
17
18 /* 释放消息队列内存 */
19 RT_KERNEL_FREE(mq->msg_pool); (3):**删除了消息队列,释放该消息队列的内存**
20
21 /* 删除消息队列对象 */
22 rt_object_delete(&(mq->parent.parent)); (4)
23
24 return RT_EOK;
25 }
(2):调用 rt_ipc_list_resume_all()函数将所有因为访问此队列的而阻塞的线程从阻塞态中恢复过来,线程得到队列返回的错误代码。
在实际情况一般不这样子使用,在删除的时候:
- 先确认所有的线程都无需再次访问此队列
- &此时没有线程被此队列阻塞才进行删除操作。
——将因为等待消息而被阻塞的线程恢复。
(4):删除消息队列对象并且释放消息队列内核对象的内存,释放内核对象内存在 rt_object_delete()函数中实现。
3、消息队列发送消息函数
实例
发送消息时,发送者需指定发送到的消息队列的对象句柄(即指向消息队列控制块的指针),并且指定发送的消息内容以及消息大小,在发送一个普通消息之后,空闲消息链表上的消息被转移到了消息队列尾链表上
1 static void send_thread_entry(void* parameter)
2 {
3 rt_err_t uwRet = RT_EOK;
4 uint32_t send_data1 = 1;
5 uint32_t send_data2 = 2;
6 while (1) {/* K1 被按下 */
7 if ( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON ) {
8 /* 将数据写入(发送)到队列中,等待时间为 0 */
9 uwRet = rt_mq_send(test_mq, /* 写入(发送)队列的 ID(句柄) */
10 &send_data1, /* 写入(发送)的数据 */
11 sizeof(send_data1)); /* 数据的长度 */
12 if (RT_EOK != uwRet) {
13 rt_kprintf("数据不能发送到消息队列!错误代码: %lx\n",uwRet);
14 }
15 }/* K1 被按下 */
16 if ( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON ) {
17 /* 将数据写入(发送)到队列中,等待时间为 0 */
18 uwRet = rt_mq_send(test_mq, /* 写入(发送)队列的 ID(句柄) */
19 &send_data2, /* 写入(发送)的数据 */
20 sizeof(send_data2)); /* 数据的长度 */
21 if (RT_EOK != uwRet) {
22 rt_kprintf("数据不能发送到消息队列!错误代码: %lx\n",uwRet);
23 }
24 }
25 rt_thread_delay(20);
26 }
27 }
rt_mq_send() 源码
1 rt_err_t rt_mq_send(rt_mq_t mq, void *buffer, rt_size_t size) (1)**在发送消息的时候需要传递一些参数:
rt_mq_t——mq 是已经创建的消息队列句柄;
void *buffer 是即将发送消息的存储地址;
rt_size_t size 是即将发送消息的大小。**
2 {
3 register rt_ubase_t temp;
4 struct rt_mq_message *msg;
5
6 RT_ASSERT(mq != RT_NULL); (2):**检测传递进来的参数,
如果传送的参数有一个无效,都无法发送**
7 RT_ASSERT(buffer != RT_NULL);
8 RT_ASSERT(size != 0);
9
10 /* 判断消息的大小*/
11 if (size > mq->msg_size) (3)**:判断消息的大小,
大小不能超过创建设置的消息队列的大小**
12 return -RT_ERROR;
13
14 RT_OBJECT_HOOK_CALL(rt_object_put_hook, (&(mq->parent.parent)));
15
16 /* 关中断 */
17 temp = rt_hw_interrupt_disable();
18
19 /* 获取一个空闲链表,必须有一个空闲链表项*/
20 msg = (struct rt_mq_message *)mq->msg_queue_free; (4)**获取一个空闲链表指针,必须
有一个空闲链表节点用于存放要发送的消息。
如果消息队列已经满了,则无法发送消息。**
21 /* 消息队列满 */
22 if (msg == RT_NULL) {
23 /* 开中断 */
24 rt_hw_interrupt_enable(temp);
25
26 return -RT_EFULL;
27 }
28 /* 移动空闲链表指针 */
29 mq->msg_queue_free = msg->next; (5):**移动空闲链表指针。**
30
31 /* 开中断 */
32 rt_hw_interrupt_enable(temp);
33
34 /* 这个消息是新的链表尾部,其下一个指针为 RT_NULL /
35 msg->next = RT_NULL;
36 /* 拷贝数据 */
37 rt_memcpy(msg + 1, buffer, size); (6)**拷贝数据,
将即将发送的数据拷贝到空闲链表的节点中,因为空
闲节点有消息头,所以其真正存放消息的地址是 msg + 1**。
38
39 /* 关中断 */
40 temp = rt_hw_interrupt_disable();
41 /* 将消息挂载到消息队列尾部 */
42 if (mq->msg_queue_tail != RT_NULL) { (7) **将空闲队列的消息
挂载到消息队列尾部,**
43 /* 如果已经存在消息队列尾部链表 */
44 ((struct rt_mq_message *)mq->msg_queue_tail)->next = msg;
45 }
46
47 /* 设置新的消息队列尾部链表指针 */
48 mq->msg_queue_tail = msg; (8)**重置消息队列尾链表指针**
49 /* 如果头部链表是空的,设置头部链表指针 */
50 if (mq->msg_queue_head == RT_NULL) (9) **如果连头链表是空的**
51 mq->msg_queue_head = msg;
52
53 /* 增加消息数量记录 */
54 mq->entry ++; (10) :**记录当前消息队列的消息个数,自加 1。**
55
56 /* 恢复挂起线程 */
57 if (!rt_list_isempty(&mq->parent.suspend_thread)) { (11) **恢复挂起线程**
58 rt_ipc_list_resume(&(mq->parent.suspend_thread));
59
60 /* 开中断 */
61 rt_hw_interrupt_enable(temp);
62
63 rt_schedule(); (12) :**发起一次线程调度。**
64
65 return RT_EOK;
66 }
67
68 /* 开中断 */
69 rt_hw_interrupt_enable(temp);
70
71 return RT_EOK;
72 }
73 RTM_EXPORT(rt_mq_send);
(2):检测传递进来的参数,如果这些参数之中有一个是无效的,都无法发送消息。
(7):**将空闲队列的消息挂载到消息队列尾部,**如果此时消息队列已经有消息,也就是尾部链表不为空,那么就直接将发送的消息挂载到尾部链表后面。
(8):重置消息队列尾链表指针,指向当前发送的消息,无论当前消息队列中尾链表是否有消息,都需要重置尾链表指针的指向。
(9):如果连头链表是空的,就需要设置头部链表指针指向当前要发送的消息,也就是指向消息自身。
(11):恢复挂起线程。如果当前有线程因为访问队列而进入阻塞,现在有消息了则可以将该线程从阻塞中恢复。
4、消息队列接收消息函数
- 当消息队列中有消息时,接收线程才能接收到消息,接收消息是有阻塞机制的,用户可以自定义等待时间,
- RT-Thread 的接收消息过程是:接收一个消息后消息队列的头链表消息被转移到了空闲消息链表中
实例
1 /* 队列读取(接收),等待时间为一直等待 */
2 uwRet = rt_mq_recv(test_mq, /* 读取(接收)队列的 ID(句柄) */
3 &r_queue, /* 读取(接收)的数据保存位置 */
4 sizeof(r_queue), /* 读取(接收)的数据的长度 */
5 RT_WAITING_FOREVER); /* 等待时间:一直等 */
6 if (RT_EOK == uwRet)
7 {
8 rt_kprintf("本次接收到的数据是:%d\n",r_queue);
9 } else
10 {
11 rt_kprintf("数据接收出错,错误代码: 0x%lx\n",uwRet);
12 }
rt_mq_recv() 源码
1 rt_err_t rt_mq_recv(rt_mq_t mq, (1):**消息队列对象的句柄。**
2 void *buffer, (2)**buffer 是用于接收消息的数据存储地址,必须在接收之前就定义,才有效**
3 rt_size_t size, (3) **消息大小。**
4 rt_int32_t timeout) (4) **指定超时时间。**
5 {
6 struct rt_thread *thread;
7 register rt_ubase_t temp;
8 struct rt_mq_message *msg;
9 rt_uint32_t tick_delta;
10
11 RT_ASSERT(mq != RT_NULL);
12 RT_ASSERT(buffer != RT_NULL);
13 RT_ASSERT(size != 0); (5) **检测传递进来的参数是否有效,
有效才进行消息队列的数据读取。**
14
15
16 tick_delta = 0;
17 /* 获取当前的线程 */
18 thread = rt_thread_self(); (6) **获取当前运行的线程。**
19 RT_OBJECT_HOOK_CALL(rt_object_trytake_hook, (&(mq->parent.parent)));
20
21 /* 关中断 */
22 temp = rt_hw_interrupt_disable();
23
24 /* 非阻塞情况 */
25 if (mq->entry == 0 && timeout == 0) { (7) **如果当前消息队列中
没有消息并且设置了不等待,
则立即返回错误代码。***(情况1)*
26 rt_hw_interrupt_enable(temp);
27
28 return -RT_ETIMEOUT;
29 }
30
31 /* 消息队列为空 */
32 while (mq->entry == 0) { (8) **如果消息队列为空,但是用户设置了
等待时间,则进入循环中。***(情况二)*
33 RT_DEBUG_IN_THREAD_CONTEXT;
34
35 /* 重置线程中的错误号 */
36 thread->error = RT_EOK; (9)**重置线程中的错误码。**
37
38 /* 不等待 */
39 if (timeout == 0) {
40 /* 开中断 */
41 rt_hw_interrupt_enable(temp);
42
43 thread->error = -RT_ETIMEOUT;
44
45 return -RT_ETIMEOUT;
46 }
47
48 /* 挂起当前线程 */
49 rt_ipc_list_suspend(&(mq->parent.suspend_thread), (10)
50 thread,
51 mq->parent.parent.flag);
52
53 /* 有等待时间,启动线程计时器 */
54 if (timeout > 0) { (11) **获取当前系统 systick 时间**
55 /* 获取 systick 定时器时间 */
56 tick_delta = rt_tick_get();
57
58 RT_DEBUG_LOG(RT_DEBUG_IPC, ("set thread:%s to timer list\n",
59 thread->name));
60
61 /* 重置线程计时器的超时并启动它 */
62 rt_timer_control(&(thread->thread_timer), (12)
63 RT_TIMER_CTRL_SET_TIME,
64 &timeout);
65 rt_timer_start(&(thread->thread_timer));
66 }
67
68 /* 开中断 */
69 rt_hw_interrupt_enable(temp);
70
71 /* 发起线程调度 */
72 rt_schedule(); (13) **发起一次线程调度。
当前线程都已经挂起了,需要进行线程切换。**
73
74
75 if (thread->error != RT_EOK) {
76 /* 返回错误 */
77 return thread->error;
78 }
79
80 /* 关中断 */
81 temp = rt_hw_interrupt_disable();
82
83 /* 如果它不是永远等待,然后重新计算超时滴答 */
84 if (timeout > 0) {
85 tick_delta = rt_tick_get() - tick_delta;
86 timeout -= tick_delta;
87 if (timeout < 0)
88 timeout = 0;
89 }
90 }
91
92 /* 获取消息 */
93 msg = (struct rt_mq_message *)mq->msg_queue_head; (14)
94
95 /* 移动消息队列头链表指针 */
96 mq->msg_queue_head = msg->next; (15)
97 /* 到达队列尾部,设置为 NULL */
98 if (mq->msg_queue_tail == msg) (16)
99 mq->msg_queue_tail = RT_NULL;
100
101 /* 记录消息个数,自减一 */
102 mq->entry --; (17) **记录当前消息队列中消息的个数,
entry 减一。拿走一个少一个**
103
104 /* 开中断 */
105 rt_hw_interrupt_enable(temp);
106
107 /* 拷贝消息到指定存储地址 */
108 rt_memcpy(buffer, msg + 1, size > mq->msg_size ? mq->msg_size : size); (18)
109
110 /* 关中断 */
111 temp = rt_hw_interrupt_disable();
112 /*移到空闲链表 */
113 msg->next = (struct rt_mq_message *)mq->msg_queue_free; (19)
114 mq->msg_queue_free = msg;
115 /* 开中断 */
116 rt_hw_interrupt_enable(temp);
117
118 RT_OBJECT_HOOK_CALL(rt_object_take_hook, (&(mq->parent.parent)));
119
120 return RT_EOK;
121 }
122 RTM_EXPORT(rt_mq_recv);
(10):挂起当前线程,因为当前线程是由于消息队列为空,并且用户设置了超时时间,直接将当前线程挂起,进入阻塞状态。 (情况二)
(11) :用户有设置等待时间,需要启动线程计时器,并且调用rt_tick_get()函数获取当前系统 systick 时间。
(12):重置线程计时器的超时并启动它,调用 rt_timer_control()函数改变当前线程阻塞时间,阻塞的时间根据用户自定义的 timeout 设置,并且调用rt_timer_start()函数开始定时。
(14):如果当前消息队列中有消息,那么获取消息队列的线程可以直接从消息队列的 msg_queue_head 链表获取到消息,并不会进入阻塞态中。
(15):移动消息队列头链表指针。重置消息队列的 msg_queue_head 指向当前消息的下一个消息。因为当前的消息被取走了,下一个消息才是可获取的有效消息。
(18):拷贝消息到指定存储地址 buffer,拷贝消息的大小为 size,其大小最大不能超过创建消息队列时候已经定义的消息大小 msg_size。
(19):获取一个消息后,**消息队列上的头链表消息被转移到空闲消息链表中,**相当消息的删除操作,这样可以保证消息队列的循环利用,而不会导致头链表指针移动到队列尾部时没有可用的消息节点。
在使用 RT-Thread 提供的消息队列函数的时候,需要了解以下几点:
在使用 RT-Thread 提供的消息队列函数的时候,需要了解以下几点:
- 使用 rt_mq_recv()、rt_mq_send()、rt_mq_delete()等这些函数之前应先创建需消息
队列,并根据队列句柄进行操作。 - 队列读取采用的是先进先出(FIFO)模式,会首先读取出首先存储在队列中的数
据。当然也有例外,RT-Thread 给我们提供了另一个函数,可以发送紧急消息的,
那么读取的时候就会读取到紧急消息的数据。 - 必须要我们定义一个存储读取出来的数据的地方,并且把存储数据的起始地址传
递给 rt_mq_recv()函数,否则,将发生地址非法的错误。 - 接收消息队列中的消息是拷贝的方式,读取消息时候定义的地址必须保证能存放
下即将读取消息的大小。