写在前面:
由于时间的不足与学习的碎片化,写博客变得有些奢侈。
但是对于记录学习(忘了以后能快速复习)的渴望一天天变得强烈。
既然如此
不如以天为单位,以时间为顺序,仅仅将博客当做一个知识学习的目录,记录笔者认为最通俗、最有帮助的资料,并尽量总结几句话指明本质,以便于日后搜索起来更加容易。
标题的结构如下:“类型”:“知识点”——“简短的解释”
部分内容由于保密协议无法上传。
点击此处进入学习日记的总目录
2024.03.30:UCOSIII第二十七节:消息队列
四十一、UCOSIII:消息队列
1、消息队列的基本概念
队列又称消息队列,是一种常用于任务间通信的数据结构,队列可以在任务与任务间、中断和任务间传递信息,实现了任务接收来自其他任务或中断的不固定长度的消息。
任务能够从队列里面读取消息,当队列中的消息是空时,读取消息的任务将被阻塞,用户还可以指定阻塞的任务时间timeout。
在这段时间中,如果队列为空,该任务将保持阻塞状态以等待队列数据有效。
当队列中有新消息时,被阻塞的任务会被唤醒并处理新消息;
当等待的时间超过了指定的阻塞时间,即使队列中尚无有效数据,任务也会自动从阻塞态转为就绪态。
消息队列是一种异步的通信方式。
通过消息队列服务,任务或中断服务程序可以将消息放入消息队列中。
同样,一个或多个任务可以从消息队列中获得消息。当有多个消息发送到消息队列时,通常是将先进入消息队列的消息先传给任务,也就是说,任务先得到的是最先进入消息队列的消息,即先进先出原则(FIFO),但是μC/OS也支持后进先出原则(LIFO)。
2、消息队列工作过程
我们在μC/OS-III中定义了一个数组OSCfg_MsgPool[OS_CFG_MSG_POOL_SIZE]。
因为在使用消息队列的时候存取消息比较频繁,在系统初始化的时候就将这个大数组的各个元素串成单向链表,组成我们说的消息池,而这些元素我们称之为消息。
为什么这里是单向链表而不是我们之前在各种列表中看到的双向链表?
因为消息的存取并不需要从链表中间,只需在链表的首尾存取即可,单向链表即够用,使用双向链表反而更复杂。
消息池的大小OS_CFG_MSG_POOL_SIZE由用户自己定义,该宏定义在os_cfg_app.h头文件中。
为什么μC/OS的消息队列要搞一个消息池呢?
因为这样子的处理很快,并且共用了资源,系统中所有被创建的队列都可以从消息池中取出消息,挂载到自身的队列上,以表示消息队列拥有消息,当消息使用完毕,则又会被释放回到消息池中,其他队列也可以从中取出消息,这样子的消息资源是能被系统所有的消息队列反复使用。
1. 消息池初始化
在系统初始化(OSInit())的时候,系统就会将消息池进行初始化。
其中,OS_MsgPoolInit()函数就是用来初始化消息池的,OS_MsgPoolInit()函数的定义位于os_msg.c文件中,其源码如下:
void OS_MsgPoolInit (OS_ERR *p_err) //返回错误类型
{
OS_MSG *p_msg1;
OS_MSG *p_msg2;
OS_MSG_QTY i;
OS_MSG_QTY loops;
#ifdef OS_SAFETY_CRITICAL//(1)//如果启用(默认禁用)了安全检测
if (p_err == (OS_ERR *)0) { //如果错误类型实参为空
OS_SAFETY_CRITICAL_EXCEPTION(); //执行安全检测异常函数
return; //返回,停止执行
}
#endif
#if OS_CFG_ARG_CHK_EN > 0u//(2)//如果启用了参数检测
if (OSCfg_MsgPoolBasePtr == (OS_MSG *)0) {//如果消息池不存在
*p_err = OS_ERR_MSG_POOL_NULL_PTR; //错误类型为“消息池指针为空”
return; //返回,停止执行
}
if (OSCfg_MsgPoolSize == (OS_MSG_QTY)0) { //如果消息池不能存放消息
*p_err = OS_ERR_MSG_POOL_EMPTY; //错误类型为“消息池为空”
return; //返回,停止执行
}
#endif
/* 将消息池里的消息逐条串成单向链表,方便管理 */
p_msg1 = OSCfg_MsgPoolBasePtr;
p_msg2 = OSCfg_MsgPoolBasePtr;
p_msg2++;
loops = OSCfg_MsgPoolSize - 1u;
for (i = 0u; i < loops; i++) { //(3)//初始化每一条消息
p_msg1->NextPtr = p_msg2;
p_msg1->MsgPtr = (void *)0;
p_msg1->MsgSize = (OS_MSG_SIZE)0u;
p_msg1->MsgTS = (CPU_TS )0u;
p_msg1++;
p_msg2++;
}
p_msg1->NextPtr = (OS_MSG *)0; //(4)//最后一条消息
p_msg1->MsgPtr = (void *)0;
p_msg1->MsgSize = (OS_MSG_SIZE)0u;
p_msg1->MsgTS = (CPU_TS )0u;
/* 初始化消息池数据 */
OSMsgPool.NextPtr = OSCfg_MsgPoolBasePtr;//(5)
OSMsgPool.NbrFree = OSCfg_MsgPoolSize;
OSMsgPool.NbrUsed = (OS_MSG_QTY)0;
OSMsgPool.NbrUsedMax = (OS_MSG_QTY)0;
*p_err = OS_ERR_NONE; //错误类型为“无错误”
}
- (1):如果启用了安全检测(OS_SAFETY_CRITICAL)这个宏定义,那么在编译代码的时候会包含安全检测,如果p_err指针为空,系统会执行安全检测异常函数OS_SAFETY_CRITICAL_EXCEPTION(),然后退出。
- (2):如果启用了参数检测(OS_CFG_ARG_CHK_EN)这个宏定义,那么在编译代码的时候会包含参数检测,如果消息池不存在,系统会返回错误类型为“消息池指针为空”的错误代码,然后退出,不执行初始化操作;如果消息池不能存放消息,系统会返回错误类型为“消息池为空”的错误代码,然后退出,也不执行初始化操作。
- (3):系统会将消息池里的消息逐条串成单向链表,方便管理,通过for循环将消息池中的每个消息元素(消息)进行初始化,并且通过单链表连接起来。
- (4):初始化最后一个消息,每个消息有四个元素,具体见图OS_MSG
NextPtr:指向下一个可用的消息。
MsgPtr:指向实际的消息。
MsgSize:记录消息的大小(以字节为单位)。
MsgTS:记录发送消息时的时间戳。
- (5):OSMsgPool是个全局变量,用来管理内存池的存取操作,它包含以下四个元素,具体见图。
NextPtr:指向下一个可用的消息。
NbrFree:记录消息池中可用的消息个数。
NbrUsed:记录已用的消息个数。
NbrUsedMax:记录使用的消息峰值数量。
初始化完成的消息池示意图具体见图
2. 消息队列的运作机制
μC/OS的消息队列控制块由多个元素组成,当消息队列被创建时,编译器会静态为消息队列分配对应的内存空间(因为我们需要自己定义一个消息队列控制块),用于保存消息队列的一些信息如队列的名字,队列可用的最大消息个数,入队指针、出队指针等。
在创建成功的时候,这些内存就被占用了,创建队列的时候用户指定队列的最大消息个数,无法再次更改,每个消息空间可以存放任意类型的数据。
任务或者中断服务程序都可以给消息队列发送消息。
当发送消息时,如果队列未满,μC/OS会将从消息池中取出一个消息,将消息挂载到队列的尾部,消息中的成员变量MsgPtr指向要发送的消息。
如果队列已满,则返回错误代码,入队失败。
μC/OS还支持发送紧急消息,也就是我们所说的后进先出(LIFO)排队,其过程与发送消息几乎一样,唯一的不同是,当发送紧急消息时,发送的消息会挂载到队列的队头而非队尾,这样,接收者就能够优先接收到紧急消息,从而及时进行消息处理。
当某个任务试图读一个队列时,可以指定一个阻塞超时时间。
在这段时间中,如果队列为空,该任务将保持阻塞状态以等待队列数据有效。
当其他任务或中断服务程序往其等待的队列中写入了数据,该任务将自动由阻塞态转移为就绪态。
当等待的时间超过了指定的阻塞时间,即使队列中尚无有效数据,任务也会自动从阻塞态转移为就绪态。
当消息队列不再被使用时,可以对它进行删除操作,一旦删除操作完成,消息队列将被永久性的删除,所有关于队列的信息会被清空,知道再次创建才可使用。
消息队列的运作过程具体见图
3、消息队列的阻塞机制
我们使用的消息队列一般不是属于某个任务的队列,在很多时候,我们创建的队列,是每个任务都可以去对他进行读写操作的。
但是为了保护每个任务对它进行读操作的过程(μC/OS队列的写操作是没有阻塞的),我们必须要有阻塞机制,在某个任务对它读操作的时候,必须保证该任务能正常完成读操作,而不受后来的任务干扰,凡事都有先来后到嘛!
如何实现这个先来后到的机制呢?
μC/OS已经为我们做好了阻塞机制,我们直接使用就好了。
假设有一个任务A对某个队列进行读操作的时候(也就是我们所说的出队),发现它没有消息,那么此时任务A有3个选择:
第一个选择,任务A扭头就走,既然队列没有消息,那我也不等了,干其他事情去,这样子任务A不会进入阻塞态;
第二个选择,任务A还是在这里等等吧,可能过一会队列就有消息,此时任务A会进入阻塞状态,在等待着消息的道来。而任务A的等待时间就由我们自己定义,比如设置1000个系统时钟节拍tick的等待,在这1000个tick到来之前任务A都是处于阻塞态。当阻塞的这段时间任务A等到了队列的消息,任务A就会从阻塞态变成就绪态,如果此时任务A比当前运行的任务优先级还高,那么任务A就会得到消息并且运行;假如1000个tick都过去了,队列还没消息,那任务A就不等了,从阻塞态中唤醒,返回一个没等到消息的错误代码,然后继续执行任务A的其他代码;
第三个选择,任务A死等,不等到消息就不走了,这样子任务A就会进入阻塞态,直到完成读取队列的消息。
假如有多个任务阻塞在一个消息队列中,那么这些阻塞的任务将按照任务优先级进行排序,优先级高的任务将优先获得队列的访问权。
如果发送消息的时候用户选择广播消息,那么在等待中的任务都会收到一样的消息。
4、消息队列的应用场景
消息队列可以应用于发送不定长消息的场合,包括任务与任务间的消息交换。
队列是μC/OS中任务与任务间、中断与任务间主要的通讯方式,发送到队列的消息是通过引用方式实现的,这意味着队列存储的是数据的地址,我们可以通过这个地址将这个数据读取出来。
这样无论数据量是多大,其操作时间都是一定的。
5、消息队列的结构
μC/OS的消息队列由多个元素组成。
在信号量被创建时,需要由我们自己定义消息队列(也可以称之为消息队列句柄),因为它是用于保存消息队列的一些信息的。
消息队列的数据结构OS_Q除了队列必须的一些基本信息外,还有PendList链表与MsgQ,为的是方便系统来管理消息队列。
数据结构具体见下图:
数据结构代码如下:
struct os_q {
/* ------------------ GENERIC MEMBERS ------------------ */
OS_OBJ_TYPE Type; //(1)
CPU_CHAR *NamePtr; //(2)
OS_PEND_LIST PendList; //(3)
#if OS_CFG_DBG_EN > 0u
OS_Q *DbgPrevPtr;
OS_Q *DbgNextPtr;
CPU_CHAR *DbgNamePtr;
#endif
/* ------------------ SPECIFIC MEMBERS ------------------ */
OS_MSG_Q MsgQ; //(4)
};
- (1):消息队列的类型,用户无需理会。
- (2):消息队列的名字。
- (3):等待消息队列的任务列表。
- (4):消息列表,这里才是用户要留意的地方,这是一个真正管理队列中消息的地方
消息列表结构如下:
struct os_msg_q { /* OS_MSG_Q */
OS_MSG *InPtr; //(1)/*指向要插入队列的下一个OS_MSG的指针*/
OS_MSG *OutPtr; //(2)/*指向要从队列中提取的下一个OS_MSG的指针*/
OS_MSG_QTY NbrEntriesSize;//(3)/*队列中允许的最大消息个数*/
OS_MSG_QTY NbrEntries; //(4)/* 队列中当前的消息个数*/
OS_MSG_QTY NbrEntriesMax;//(5)/*队列中的消息个数峰值*/
};
(1)、(2):队列中消息也是用单向链表串联起来的,但存取消息不像消息池只是从固定的一端。队列存取消息有两种方式,一种是FIFO模式,即先进先出,这个时候消息的存取是在单向链表的两端,一个头一个尾,存取位置可能不一样就产生了这两个输入指针和输出指针,具体见图FIFO模式。
另一种是LIFO模式,后进先出,这个时候消息的存取都是在单向链表的一端,仅仅用OutPtr就足够指示存取的位置,具体见图LIFO模式。
当队列中已经存在比较多的消息没有处理,这个时候有个紧急的消息需要马上传送到其他任务去的时候就可以在发布消息的时候选择LIFO模式。
(3):消息队列最大可用的消息个数,消息队列创建的时候由用户指定这个值的大小。
(4):记录消息队列中当前的消息个数,每发送一个消息,若没有任务在等待该消息队列的消息,那么新发送的消息被插入此消息队列后此值加1,NbrEntries的大小不能超过NbrEntriesSize。
(5):记录队列最多的时候拥有的消息个数。