消息队列

消息队列

消息队列的基本概念

​ 队列又称消息队列,是一种常用于任务间通信的数据结构,是一种异步的通信方式。

​ 队列可以在任务与任务间、中断和任务间传递信息,实现了任务接收来自其他任务或中断的不固定长度的消息。任务能够从队列里面读取消息,当队列中的消息是空时,读取消息的任务将被阻塞,用户还可以指定阻塞的任务时间 timeout,在这段时间中,如果队列为空,该任务将保持阻塞状态以等待队列数据有效。当队列中有新消息时,被阻塞的任务会被唤醒并处理新消息;当等待的时间超过了指定的阻塞时间,即使队列中尚无有效数据,任务也会自动从阻塞态转为就绪态。

​ 当有多个消息发送到消息队列时,通常是将先进入消息队列的消息先传给任务,也就是说,任务先得到的是最先进入消息队列的消息,即先进先出原则(FIFO),但是 uC/OS也支持后进先出原则(LIFO)。

uCOS中使用队列数据结构实现任务异步通信工作,具有如下特性:

  • 消息支持先进先出方式排队,支持异步读写工作方式。
  • 消息支持后进先出方式排队,往队首发送消息(LIFO)。
  • 读消息队列支持超时机制。
  • 可以允许不同长度的任意类型消息(因为是引用方式传递,无论多大的数据都只是
    个指针)。
  • 一个任务能够从任意一个消息队列接收和发送消息。
  • 多个任务能够从同一个消息队列接收和发送消息。
  • 当队列使用结束后,可以通过删除队列函数进行删除。

消息队列工作过程

​ 在 μCOS-III 中定义了一个数组 OSCfg_MsgPool[OS_CFG_MSG_POOL_SIZE],在系统初始化的时候就将这个大数组的各个元素串成单向链表,组成消息池,而这些元素我们称之为消息。

为什么 uCOS 的消息队列要搞一个消息池呢?

​ 因为这样子的处理很快,并且共用了资源,系统中所有被创建的队列都可以从消息池中取出消息,挂载到自身的队列上,以表示消息队列拥有消息,当消息使用完毕,则又会被释放回到消息池中,其他队列也可以从中取出消息,这样子的消息资源是能被系统所有的消息队列反复使用。

消息池初始化

在系统初始化(OSInit())的时候,系统就会将消息池进行初始化,其中,OS_MsgPoolInit()函数就是用来初始化消息池的。

/* OS_MsgPoolInit()消息池初始化 */

 1 void OS_MsgPoolInit (OS_ERR *p_err) //返回错误类型
 2 {
 3 		OS_MSG *p_msg1;
 4 		OS_MSG *p_msg2;
 5 		OS_MSG_QTY i;
 6 		OS_MSG_QTY loops;
 7
 8
 9
10 #ifdef OS_SAFETY_CRITICAL 		(1) //如果使能(默认禁用)了安全检测
11 		if (p_err == (OS_ERR *)0) { 	//如果错误类型实参为空
12 		OS_SAFETY_CRITICAL_EXCEPTION(); //执行安全检测异常函数
13 		return; //返回,停止执行
14 		}
15 #endif
16
17 #if OS_CFG_ARG_CHK_EN > 0u 		(2) //如果使能了参数检测
18 		if (OSCfg_MsgPoolBasePtr == (OS_MSG *)0) {//如果消息池不存在
19 		*p_err = OS_ERR_MSG_POOL_NULL_PTR; //错误类型为“消息池指针为空”
20 		return; //返回,停止执行
21 		}
22 		if (OSCfg_MsgPoolSize == (OS_MSG_QTY)0) { //如果消息池不能存放消息
23 		*p_err = OS_ERR_MSG_POOL_EMPTY; //错误类型为“消息池为空”
24 		return; //返回,停止执行
25 		}
26 #endif
27 		/* 将消息池里的消息逐条串成单向链表,方便管理 */
28 		p_msg1 = OSCfg_MsgPoolBasePtr;
29 		p_msg2 = OSCfg_MsgPoolBasePtr;
30 		p_msg2++;
31 		loops = OSCfg_MsgPoolSize - 1u;
32 		for (i = 0u; i < loops; i++) { (3) //初始化每一条消息
33 			p_msg1->NextPtr = p_msg2;
34 			p_msg1->MsgPtr = (void *)0;
35 			p_msg1->MsgSize = (OS_MSG_SIZE)0u;
36 			p_msg1->MsgTS = (CPU_TS )0u;
37 			p_msg1++;
38 			p_msg2++;
39 		}
40 		p_msg1->NextPtr = (OS_MSG *)0; (4) //初始化最后一个消息
41 		p_msg1->MsgPtr = (void *)0;
42 		p_msg1->MsgSize = (OS_MSG_SIZE)0u;
43 		p_msg1->MsgTS = (CPU_TS )0u;
44 		/* 初始化消息池数据 */
45 		OSMsgPool.NextPtr = OSCfg_MsgPoolBasePtr; (5)
46 		OSMsgPool.NbrFree = OSCfg_MsgPoolSize;
47 		OSMsgPool.NbrUsed = (OS_MSG_QTY)0;
48 		OSMsgPool.NbrUsedMax = (OS_MSG_QTY)0;
49 		*p_err = OS_ERR_NONE; //错误类型为“无错误”
50 }

初始化最后一个消息,每个消息有四个元素
  • NextPtr :指向下一个可用的消息。
  • MsgPtr:指向实际的消息。
  • MsgSize:记录消息的大小(以字节为单位)。
  • MsgTS:记录发送消息时的时间戳。

OSMsgPool 是个全局变量,用来管理内存池的存取操作,它包含以下四个元素
  • NextPtr :指向下一个可用的消息。
  • NbrFree :记录消息池中可用的消息个数。
  • NbrUsed: 记录已用的消息个数。
  • NbrUsedMax:记录使用的消息峰值数量。

初始化完成的消息池示意图

消息队列的运作机制

uCOS 的消息队列控制块由多个元素组成,当消息队列被创建时,编译器会静态为消息队列分配对应的内存空间,用于保存消息队列的一些信息(队列的名字,队列可用的最大消息个数,入队指针,出队指针等)

注意:在创建成功的时候,这些内存就被占用了,创建队列的时候用户指定队列的最大消息个数,无法再次更改,每个消息空间可以存放任意类型的数据。

使用机制:
先进先出:

​ 任务或者中断服务程序都可以给消息队列发送消息,当发送消息时,如果队列未满,uCOS 会将从消息池中取出一个消息,将消息挂载到队列的尾部,消息中的成员变量MsgPtr 指向要发送的消息。如果队列已满,则返回错误代码,入队失败。

后进先出:

​ uCOS 还支持发送紧急消息,其过程与发送消息几乎一样,唯一的不同是,当发送紧急消息时,发送的消息会挂载到队列的队头而非队尾,这样,接收者就能够优先接收到紧急消息,从而及时进行消息处理。

读队列:

​ 当某个任务试图读一个队列时,可以指定一个阻塞超时时间。在这段时间中,如果队列为空,该任务将保持阻塞状态以等待队列数据有效。当其它任务或中断服务程序往其等待的队列中写入了数据,该任务将自动由阻塞态转移为就绪态。当等待的时间超过了指定的阻塞时间,即使队列中尚无有效数据,任务也会自动从阻塞态转移为就绪态。

无用队列:

​ 当消息队列不再被使用时,可以对它进行删除操作,一旦删除操作完成,消息队列将被永久性的删除,所有关于队列的信息会被清空,直到再次创建才可使用。

读消息队列时的阻塞机制

​ 我们使用的消息队列一般不是属于某个任务的队列,在很多时候,我们创建的队列,是每个任务都可以去对他进行读写操作的,但是为了保护每个任务对它进行读操作的过程,我们必须要有阻塞机制,在某个任务对它读操作的时候,必须保证该任务能正常完成读操作,而不受后来的任务干扰。

如何实现这个先来后到的机制呢??

​ 很简单,因为 uCOS 已经为我们做好了,我们直接使用就好了。

eg:有一个任务 A 对某个队列进行读操作的时候(也就是我们所说的出队),发现它没有消息,那么此时任务 A 有 3 个选择:第一个选择,任务 A 扭头就走,既然队列没有消息,那我也不等了,干其它事情去,这样子任务 A 不会进入阻塞态;第二个选择,任务 A 还是在这里等等吧,可能过一会队列就有消息,此时任务 A 会进入阻塞状态,在等待着消息的道来,而任务 A 的等待时间就由我们自己定义,比如设置 1000 个系统时钟节拍 tick 的等待,在这 1000 个 tick 到来之前任务 A 都是处于阻塞态,当阻塞的这段时间任务 A 等到了队列的消息,那么任务 A 就会从阻塞态变成就绪态,如果此时任务 A 比当前运行的任务优先级还高,那么,任务 A 就会得到消息并且运行;假如 1000 个 tick 都过去了,队列还没消息,那任务 A 就不等了,从阻塞态中唤醒,返回一个没等到消息的错误代码,然后继续执行任务 A的其他代码;第三个选择,任务 A死等,不等到消息就不走了,这样子任务 A就会进入阻塞态,直到完成读取队列的消息。

​ 假如有多个任务阻塞在一个消息队列中,那么这些阻塞的任务将按照任务优先级进行排序,优先级高的任务将优先获得队列的访问权。
​ 如果发送消息的时候用户选择广播消息,那么在等待中的任务都会收到一样的消息。

消息队列的应用场景

发送到队列的消息是通过引用方式实现的,这意味着队列存储的是数据的地址,我们可以通过这个地址将这个数据读取出来,这样子,无论数据量是多大,其操作时间都是一定的,只是一个指向数据地址指针。

消息队列的结构

/* 消息队列的结构 */
 1 struct os_q {
 2 		OS_OBJ_TYPE   Type;   //(1)消息队列的类型,用户无需理会。
 3 		CPU_CHAR   *NamePtr;   //(2)消息队列的名字。
 4 		OS_PEND_LIST   PendList; //(3)等待消息队列的任务列表。
 5 #if OS_CFG_DBG_EN > 0u
 6 		OS_Q   *DbgPrevPtr;
 7 		OS_Q   *DbgNextPtr;
 8 		CPU_CHAR   *DbgNamePtr;
 9 #endif
10 		OS_MSG_Q MsgQ; (4)消息列表
11 }
/* os_msg_q 结构 */
1 struct os_msg_q {
2 		OS_MSG    *InPtr; (1)/*指向要插入队列的下一个 OS_MSG 的指针*/
3 		OS_MSG    *OutPtr; (2)/*指向要从队列中提取的下一个 OS_MSG 的指针*/
4 		OS_MSG_QTY   NbrEntriesSize; (3) /*队列中允许的最大消息个数 */
5 		OS_MSG_QTY   NbrEntries; (4)/* 队列中当前的消息个数 */
6 		OS_MSG_QTY   NbrEntriesMax; (5) /*队列中的消息个数峰值*/
7 };

消息队列常用函数讲解

创建消息队列函数 OSQCreate()

消息队列删除函数 OSQDel()

消息队列发送函数 OSQPost()

消息队列获取函数 OSQPend()

posted @ 2020-03-31 10:43  RSheng16  阅读(552)  评论(0)    收藏  举报