消息队列
消息队列是一种任务间通信的机制,它可以用于发送不定长消息的场合。消息队列也有缓冲区(消息队列资源池),可以缓存一定数量的消息。消息队列发送的消息内容长度可以是任意的(其最大长度可以在消息队列初始化时设置),消息队列在发送时会将整个消息内容复制到消息队列的缓冲区中,接收消息时会把消息队列缓冲区中的消息内容复制到接收端指定的地址。
消息队列没满的情况下,可以一直往消息队列里面发送消息,如果消息队列满了可以选择超时等待;消息队列有消息的情况下,可以从消息队列里面接收消息,如果没有消息,可以选择超时等待。消息队列和邮箱类似,但是也有不同,消息队列可以发送不定长的数据,灵活性较高;邮箱发送的数据长度固定且长度较小,相较于消息队列,邮箱发送效率更高。
一、消息队列简介与API函数
1.1、消息队列简介
消息队列是为了任务与任务、任务与中断之间的通信而准备的,可以在任务与任务、任务与中断之间传递消息,消息队列中可以存储有限的、大小固定的数据项目。任务与任务、任务与中断之间要交流的数据保存在队列中,叫做队列项目。队列所能保存的最大数据项目数量叫做队列的长度,创建队列的时候会指定数据项目的大小和队列的长度。下面我们介绍一下消息队列的特点。
1)数据存储
通常消息队列采用先进先出(FIFO)的存储缓冲机制,也就是往消息队列发送数据的时候(也叫入队)永远都是发送到队列的尾部,而从消息队列提取数据的时候(也叫出队)是从队列的头部提取的。但是也可以使用 LIFO 的存储缓冲,也就是后进先出,OneOS 中的队列也提供了 LIFO的存储缓冲机制。当然 OneOS 也是支持优先级存储缓冲机制,也就是说:多个任务同时获取消息时,首先判断任务的优先级,优先级高的任务就最先获取消息。
2)多任务访问
队列不是属于某个特别指定的任务的,任何任务都可以向队列中发送消息,或者从队列中提取消息。
3)出队阻塞
当任务尝试从一个消息队列中读取消息的时候可以指定一个阻塞时间,这个阻塞时间就是当任务从消息队列中读取消息无效的时候任务阻塞的时间。出队就是就从消息队列中读取消息,出队阻塞是针对从消息队列中读取消息的任务而言的。
比如任务A用于处理串口接收到的数据,串口接收到数据以后就会放到消息队列 Q 中,任务 A 从消息队列 Q 中读取数据。但是如果此时消息队列 Q 是空的,说明还没有数据,任务 A 这时候来读取的话肯定是获取不到任何东西,那该怎么办呢?任务 A 现在有三种选择,一:直接走,二:等一会再决定,三:一直等,直到有数据!选哪一个就是由这个阻塞时间决定的,这个阻塞时间单位是时钟节拍数。阻塞时间为 0 的话就是不阻塞,没有数据的话就马上返回任务继续执行接下来的代码,对应第一种选择。如果阻塞时间为 0~ 最大值,当任务没有从消息队列中获取到消息的话就进入阻塞态,阻塞时间指定了任务进入阻塞态的时间,当阻塞时间到了以后还没有接收到数据的话就退出阻塞态,返回任务接着运行下面的代码,如果在阻塞时间内接收到了数据就立即返回,执行任务中下面的代码,这种情况对应第二种选择。当阻塞时间设置为 OS_WAIT_FOREVER 的话,任务就会一直进入阻塞态等待,直到接收到数据为止!这个就是第三种选择。
4)入队阻塞
入队说的是向消息队列中发送消息,将消息加入到消息队列中。和出队阻塞一样,当一个任务向消息队列发送消息的话也可以设置阻塞时间。比如任务 B 向消息队列 Q 发送消息,但是此时消息队列 Q 是满的,那肯定是发送失败的。此时任务 B 就会遇到和上面任务 A 一样的问题,这两种情况的处理过程是类似的,只不过一个是向消息队列 Q 发送消息,一个是从消息队列 Q 读取消息而已。
1.2、任务间消息队列实现原理
消息队列有两个任务阻塞队列,因为消息发送和接收都有可能导致阻塞。当没有消息时,就会导致接收消息任务阻塞,任务被放到阻塞队列,等待另一个任务发送消息,阻塞任务被唤醒,并放到就绪队列;当消息队列满了,就会导致发送消息任务阻塞,后续的处理过程和接收消息类似。另外,消息队列还有两个资源池相关的队列,一个是缓存消息队列,这个队列用于管理缓存的消息块(这些消息块包含的消息还没有被读取),有效消息头指针和尾指针记录第一个缓存消息和最后一个缓存消息,便于找到读取和写入消息的位置,一个是空闲消息队列,这个队列用于管理空闲的消息块,空闲消息块指针记录第一个空闲消息块,便于找到保存新消息的位置。
下图描述了任务接收消息被阻塞,然后等待另一个任务发送消息的处理过程。
图中(1),任务 1 先运行。
图中(2),任务 1 获取消息,由于此时没有消息,获取失败。
图中(3),任务 1 被放到接收阻塞队列。
图中(4),任务 2 运行。
图中(5),任务 2 发送消息。
图中(6),任务 2 发送的消息,通过空闲消息块指针和消息尾指针找到位置保存。
图中(7),任务 1 被放到就绪队列。
图中(8),任务 1 运行,通过消息头指针读取消息。
1.3、API函数
消息队列中常用的函数如下表所示:
描述 | 函数及结构体 |
消息队列控制块 | os_mq_t |
消息控制块 | os_mq_msg |
创建消息队列 | os_mq_create() |
发送消息 | os_mq_send() 和 os_mq_send_urgent() |
接收消息 | os_mq_recv() |
销毁消息队列 | os_mq_destroy() |
1.3.1、消息队列结构体
(1)创建队列:首先,一个或多个消息队列被创建。这通常由操作系统或消息队列服务来完成,并为队列分配必要的资源。
(2)消息发送:进程(或线程)将要发送的数据封装成一个消息,然后发送到队列中。消息可以包含有效载荷(即要传输的数据)和一些元数据(如消息类型、优先级等)。
(3)排队:消息发送到队列时,它被添加到队列的末尾。通常最先发送的消息将最先被接收。 (4)消息接收:另一个进程(或线程)从队列中接收消息。它可以通过几种方式来接收消息, 例如:
阻塞接收:如果队列为空,接收进程将等待直到有消息到达。
非阻塞接收:接收进程检查队列,如果有消息则接收,否则立即返回。
注意:在发送和接受过程中都是可能被阻塞的。
(5)消息移除:消息被接受后会从消息队列中移除。
注意:消息也有优先级,较高优先级的消息可能会先被处理,尽管它不是最先被发送的。
1.3.2、创建消息队列
消息队列的创建使用函数 os_mq_create()或者函数 os_mq_init(),前者是以动态方式创建并初始化消息队列,后者是以静态的方式创建消息队列。下面主要介绍os_mq_create()。该函数以动态方式创建并初始化消息队列,消息队列对象的内存空间和消息队列缓冲区的内存空间都是通过动态申请内存的方式获得,简略源码如下:
函数os_mq_create(精简) 申请内存、成员变量初始化
申请内存
成员变量初始化
1.3.3、发送消息
函数os_mq_send——发送消息
os_mq_send ()的四个形参描述如下:
参数 | 描述 |
mq | 消息队列控制块 |
buffer | 待发送的消息的地址,也就是要发送消息的内容 |
buffer_size | buffer 的长度 |
time | 消息暂时不能发送的等待超时时间。若为 OS_NO_WAIT,则等待 时间为 0;若为 OS_WAIT_FOREVER,则永久等待直到消息发送; 若为其它值,则等待 timeout 时间或者消息发送为止,并且其他 值时 timeout 必须小于 OS_TICK_MAX / 2 |
函数_k_mq_send(精简)发送消息主要分为复制消息、发送或保存消息。在复制消息的时候又分为消息池未满和已满两种情况。
复制消息:消息池未满
复制消息:消息池已满
发送或保存消息
1.3.3.1 、消息池的解释:
消息队列中的消息池(Message Pool)是一个非常重要的概念,它的作用和优势如下:
1)消息重用:消息池允许消息在被消费后返回到池中,以便再次使用。这样可以减少消息创建和销毁的开销,特别是在高频率消息发送的场景中,能够提高系统的性能。
2)内存管理:通过消息池,系统可以更有效地管理内存。消息池可以预先分配一定数量的消息,避免了频繁的内存分配和回收,从而减少了内存碎片和提高了内存使用效率。
3)提高效率:消息池可以减少消息处理的延迟。当消费者处理完消息后,消息可以迅速返回到池中,供生产者再次使用,这样可以减少等待新消息被创建的时间。
4)负载均衡:在分布式系统中,消息池可以帮助实现负载均衡。通过监控消息池的状态,系统可以动态地调整消息的分配,确保各个节点的处理能力得到充分利用。
5)可靠性:消息池可以提高消息处理的可靠性。如果消息在传输过程中丢失或损坏,可以从消息池中快速重新获取一个消息实例,而不是重新创建一个新的消息。
消息池的工作原理通常涉及到消息的创建、使用、回收和再利用。当消息被发送到队列时,它们会被存储在消息池中。当消费者从队列中取出消息并处理完毕后,消息会被返回到消息池,而不是被销毁。这样,消息池就充当了一个缓冲区,允许消息在不同的生产者和消费者之间高效地流转。
在实际应用中,消息池特别适合于那些消息生成和消费频率高、对性能要求严格的场景。例如,在高性能的交易系统中,消息池可以确保消息能够快速地在不同的服务和组件之间传递,同时减少系统资源的消耗。
总结来说,消息池是消息队列中的一个重要组成部分,它通过消息的重用、内存管理和提高效率等方式,为分布式系统提供了一个高效、可靠和可扩展的消息处理机制。 探索一下 复制分享
1.3.4、接收消息
函数os_mq_recv(精简)——获取消息
函数os_mq_recv(精简)——复制消息
1.3.5、销毁消息队列
函数os_mq_destroy(精简) 取消阻塞队列 释放内存空间
二、实验部分
本实验主要使用 OneOS 实时操作系统的动态消息队列示例。它展示了如何创建动态消息队列,以及如何通过两个任务(task1 和 task2)发送和接收消息。以下是代码的详细注释和分析:
2.1、代码及其解释
#include <oneos_config.h> // 包含OneOS的配置头文件
#include <dlog.h> // 包含日志输出功能的头文件
#include <os_errno.h> // 包含错误码的头文件
#include <os_task.h> // 包含任务管理的头文件
#include <shell.h> // 包含shell功能的头文件
#include <string.h> // 包含字符串操作的头文件
#include <os_memory.h> // 包含内存管理的头文件
#include <os_mq.h> // 包含消息队列的头文件
// 定义相关的宏
#define TEST_TAG "TEST" // 用于日志输出的标签
#define TASK_STACK_SIZE 1024 // 任务栈大小
#define TASK1_PRIORITY 15 // task1的优先级
#define TASK2_PRIORITY 16 // task2的优先级
#define MQ_MAX_MSG 10 // 消息队列最大消息数
#define TEST_NAME_MAX 16 // 学生姓名的最大长度
#define STUDENT_NUM 5 // 学生数量
static os_msgqueue_id mq_dynamic; // 静态变量,用于保存动态创建的消息队列ID
// 学生成绩结构体
struct student_score
{
char name[TEST_NAME_MAX]; // 学生姓名
uint32_t score; // 学生分数
};
// task1的入口函数
static void task1_entry(void *para)
{
uint32_t i = 0; // 循环变量
struct student_score student_data; // 学生数据
char *name[STUDENT_NUM] = {"xiaoming", "xiaohua", "xiaoqiang", "xiaoli", "xiaofang"}; // 学生姓名数组
uint32_t score[STUDENT_NUM] = {80, 85, 90, 95, 96}; // 学生分数数组
// 循环发送学生成绩
for (i = 0; i < STUDENT_NUM; i++)
{
memset(student_data.name, 0, TEST_NAME_MAX); // 清空姓名缓冲区
strncpy(student_data.name, name[i], TEST_NAME_MAX); // 复制姓名
student_data.score = score[i]; // 设置分数
// 发送消息到消息队列,如果成功,打印日志
if (OS_SUCCESS == os_msgqueue_send(mq_dynamic, &student_data, sizeof(struct student_score), OS_WAIT_FOREVER))
{
LOG_W(TEST_TAG, "task1 send -- name:%s score:%d", student_data.name, student_data.score);
}
os_task_msleep(100); // 任务休眠100毫秒
}
}
// task2的入口函数
static void task2_entry(void *para)
{
struct student_score student_data; // 学生数据
os_size_t recv_size = 0; // 接收到的消息大小
while (1)
{
// 从消息队列接收消息,如果成功,打印日志
if (OS_SUCCESS ==
os_msgqueue_recv(mq_dynamic, &student_data, sizeof(struct student_score), OS_WAIT_FOREVER, &recv_size))
{
LOG_W(TEST_TAG, "task2 recv -- name:%s score:%d", student_data.name, student_data.score);
}
}
}
// 消息队列示例函数
static void msgqueue_dynamic_sample(void)
{
os_task_id task1; // task1的ID
os_task_id task2; // task2的ID
// 动态创建消息队列
mq_dynamic = os_msgqueue_create_dynamic("mq_dynamic", sizeof(struct student_score), MQ_MAX_MSG);
if (!mq_dynamic)
{
LOG_W(TEST_TAG, "msgqueue_dynamic_sample msgqueue create ERR");
}
// 创建task1
task1 = os_task_create(OS_NULL, OS_NULL, TASK_STACK_SIZE, "task1", task1_entry, OS_NULL, TASK1_PRIORITY);
if (task1)
{
LOG_W(TEST_TAG, "msgqueue_dynamic_sample startup task1");
os_task_startup(task1); // 启动task1
}
os_task_msleep(200); // 任务休眠200毫秒
// 创建task2
task2 = os_task_create(OS_NULL, OS_NULL, TASK_STACK_SIZE, "task2", task2_entry, OS_NULL, TASK2_PRIORITY);
if (task2)
{
LOG_W(TEST_TAG, "msgqueue_dynamic_sample startup task2");
os_task_startup(task2); // 启动task2
}
}
int main()
{
msgqueue_dynamic_sample(); // 调用消息队列示例函数
}
// 将消息队列示例函数导出为shell命令
SH_CMD_EXPORT(dynamic_msgqueue, msgqueue_dynamic_sample, "test dynamic msgqueue");
代码分析如下:
1)这段代码首先包含了OneOS操作系统所需的头文件,以便使用日志、任务、消息队列等功能。 2)定义了一系列宏和全局变量,包括任务栈大小、任务优先级、消息队列最大消息数等。
3)定义了一个struct student_score结构体,用于存储学生姓名和分数。
4)task1_entry函数是任务1的入口函数,它循环发送学生成绩到消息队列,并在发送成功后打印日志。
5)task2_entry函数是任务2的入口函数,它无限循环接收消息队列中的消息,并在接收成功后打印日志。
6)msgqueue_dynamic_sample函数是消息队列示例的主函数,它动态创建了一个消息队列,并创建了两个任务。
7)main函数调用msgqueue_dynamic_sample函数启动消息队列示例。
8)SH_CMD_EXPORT宏将msgqueue_dynamic_sample函数导出为shell命令,允许在OneOS的shell中通过命令行运行该函数。
这段代码展示了如何在OneOS操作系统中使用动态消息队列进行任务间通信。通过两个任务的创建和通信,展示了消息队列的发送和接收操作。
2.2、实验问题与思考
在实验结果中,我查看串口打印的数据,发现整个程序并不是循环打印的,但是明明task2就是无限循环的,我后续思考是否是task1发送至队列的消息被task2已经获取完了,队列里面没有消息了,所以task2就没办法一直打印。
我将task1的任务函数更改为无限循环向队列发送消息,最后结果如下:
在修改了task1的任务函数代码后结果如上图,现在能做到无限循环输出消息。
注意:在设置无限向消息队列发送消息的时候,在每次发送消息后让任务休眠100毫秒,这是为了防止任务过快地发送消息,导致消息队列迅速填满或者给系统带来过大压力。
修改的task1_entry函数如下:
static void task1_entry(void *para)
{
struct student_score student_data;
char *name[STUDENT_NUM] = {"xiaoming", "xiaohua", "xiaoqiang", "xiaoli", "xiaofang"};
uint32_t score[STUDENT_NUM] = {80, 85, 90, 95, 96};
uint32_t i = 0; // 用于循环遍历学生数组
while (1) // 无限循环,持续发送消息
{
for (i = 0; i < STUDENT_NUM; i++) // 循环发送每个学生的成绩
{
memset(student_data.name, 0, TEST_NAME_MAX); // 清空姓名缓冲区
strncpy(student_data.name, name[i], TEST_NAME_MAX); // 复制姓名
student_data.score = score[i]; // 设置分数
if (OS_SUCCESS == os_msgqueue_send(mq_dynamic, &student_data, sizeof(struct student_score), OS_WAIT_FOREVER))
{
LOG_W(TEST_TAG, "task1 send -- name:%s score:%d", student_data.name, student_data.score);
}
else
{
LOG_E(TEST_TAG, "Failed to send message to message queue.");
}
os_task_msleep(100); // 任务休眠100毫秒,可以根据需要调整
}
}
}
邮箱
一、邮箱简介
OneOS操作系统中的邮箱是一种进程间通信机制,它允许任务之间通过发送和接收信息来进行通信。在OneOS中,邮箱用于交换固定大小(通常是4字节)的数据,适合传输占用较小空间的信息或者指针信息。邮箱通信机制包括阻塞和非阻塞两种方式:
阻塞:
如果一个任务尝试从邮箱接收/发送消息,而邮箱中没有消息(邮箱已满)可接收,该任务将会被挂起(即放入等待队列),直到有其他任务发送消息到邮箱中(邮箱中有空位)。
适用范围:阻塞方式适用于那些可以容忍等待的任务,或者那些必须确保消息接收到才能继续执行的任务。
非阻塞
任务尝试接收消息时,如果邮箱为空,接收操作会立即返回一个错误或特殊值(如NULL或特定的错误码),而不是挂起任务。
适用范围:这种方式适用于那些不能或不愿意等待消息的任务,它们可能需要立即继续执行其他操作,即使消息发送或接收失败。
注:在OneOS中邮箱和消息队列的区别暂时只有数据大小的区别,消息队列传递的数据是不定长度的,邮箱传递的数据是固定长度的,其实现原理基本一致,后续就不再赘述邮箱的实现原理以及API函数。