一种简单安全的消息队列的C语言解决方案
文章概要
- 〇、背景
- 一、基本思路
-
- 1.1 消息队列的属性要求
- 1.2 消息队列的结构定义
- 1.3 写入、读取数据的长度信息处理
- 二、基本实现
-
- 2.1 消息队列的初始化
- 2.2 消息队列的写入操作
-
- 2.2.1 消息队列的基础写入操作
- 2.2.2 消息队列的写入操作
- 2.3 消息队列的读取操作
-
- 2.3.1 消息队列的基础读取操作
- 2.3.2 消息队列的读取操作
- 2.4 消息队列的清除操作
- 2.5 消息队列的空间计算
-
- 2.5.1 消息队列的空闲长度
- 2.5.2 消息队列的已使用长度
- 三、功能测试
-
- 3.1 功能测试代码
- 3.2 测试效果显示
- 四、源码下载
〇、背景
基于嵌入式编程,对于有安全等级要求的项目,一般都会对编码有诸多安全性考虑的规则限制。在实际的编程中,消息队列的使用还是比较频繁的,但是对于不使用操作系统的情况下,消息队列相关的功能就需要手动实现。下面将会介绍一种简单的、安全等级较高的消息队列的实现方式。
一、基本思路
1.1 消息队列的属性要求
作为一个存储用的消息队列,一般情况下需要几个属性。
- 消息队列的读指针:也就是数据从什么位置开始读取
- 消息队列的写指针:也就是数据从什么位置开始写入
- 消息队列的现存数据长度:也就是读取多少数据
- 消息队列的空闲容量:也就是目前数据缓存区剩余的大小
- 消息队列的已使用容量:也就是目前数据缓存区使用了的大小
- 消息队列的容量:一般情况下,此容量表示的数据缓存区的大小
- 消息队列数据缓冲区:也就是队列用于存放数据的地方
1.2 消息队列的结构定义
考虑到表示的精准性、使用的简洁性等方面,所以需要尽可能少的变量成员定义。本例中定义的结构如下。
/* 定义消息队列的数据类型 */
typedef uint8_t QueueDataType_t;
/* 定义消息队列的数据结构 */
typedef struct _message_queue_class_struct
{
uint32_t read_pos; /* 写队列数据指针 */
uint32_t write_pos; /* 读队列数据指针 */
uint32_t queue_size; /* 队列容量 */
QueueDataType_t *queue_data; /* 队列数据 */
} MsgQueueClass_st;
在如上的定义中,并没有直接定义 “消息队列的现存数据长度”、 “消息队列的空闲容量”、 “消息队列的已使用容量” 等信息变量,是因为上述信息均可以通过结构中定义的 “消息队列的读指针”、 “消息队列的写指针” 进行相关的运算获得,后面代码中会有相关的实现。
1.3 写入、读取数据的长度信息处理
通过上述的消息队列的数据结构定义,不难看出来成员 queue_data
指向的是一个 uint8_t
的数据缓存区(数组),对于每一次写入操作,如何记录本次写入数据的长度其实很重要,因为本消息队列中存放的数据可能是通过多次写入的,而且每次写入的数据长度还不相同,那么在进行数据读取的时候,也不可能一下子读取缓存区中所有的数据,也不可能只读取一个随便长度的数据,这样的话整个队列管理起来就很困难。
所以为了方便消息队列的管理,设定每一次读取数据的长度即为此数据写入的时候的长度。
对于写入数据的长度信息的记录,比较方便记录和获取的方式就是写入到实际数据的头部,其占用的字节数可以自定义,本例中采用最常用的 uint16_t
来标识。所以:
- 每次在消息队列中写入的数据时候,先在队列中写入本次数据的长度,然后再写入数据。
- 每次在消息队列中读取的数据时候,先在队列中读取本次数据的长度,然后再读取目标长度数据。
二、基本实现
2.1 消息队列的初始化
消息队列的初始化,主要就是初始化读指针、写指针、绑定数据缓冲区、初始化缓冲区容量等基础属性的设置。其实现代码也比较简单。
/*
* @fn MsgQueue_Init
* @brief 初始化队列,队列结构体首地址,队列数据类型,函数将初始化队列数据为初始化值状态
* @param[in] queue_handle 队列结构体首地址
* @param[in] queue_buffer 队列数据类型
* @param[in] queue_size 队列数据容量
* @return true 初始化成功 false 初始化失败
*/
bool MsgQueue_Init(MsgQueueClass_st *queue_handle, QueueDataType_t *queue_buffer, uint32_t queue_size)
{
/** 参数判断 */
if ((NULL == queue_handle) || (0U == queue_size) || (NULL == queue_buffer))
{
return false;
}
memset(queue_buffer, 0U, queue_size);
/** 初始化队列数据 */
queue_handle->read_pos = 0U;
queue_handle->write_pos = 0U;
queue_handle->queue_size = queue_size;
queue_handle->queue_data = queue_buffer;
/** 返回 */
return true;
}
2.2 消息队列的写入操作
2.2.1 消息队列的基础写入操作
消息队列数据的写入操作,需要考虑几个方面的问题:
- 当前空闲容量是否足够
- 当前写入数据位置是否需要翻转(也就是写入数据指针位置 + 写入数据的长度 是否 超过缓存区末尾)
- 当前写入数据指针是否需要翻转
综上所述,消息队列的写入的基础操作,编码如下。
/*
* @fn MsgQueue_WriteData
* @brief 队列写操作,输入队列结构体首地址、待写入数据和长度,函数将数据存在队列中
* @param[in] queue_handle 队列结构体首地址
* @param[in] write_buffer 待写入队列的数据
* @param[in] write_data_len 待写入队列的数据长度
* @return NONE
* @exception 此函数对输入指针参数未判断是否为NULL,如果任一指针参数为NULL,则可能引发core dump的异常。所以需要调用者保证指针参数的有效性
*/
static void MsgQueue_WriteData(MsgQueueClass_st *queue_handle, const QueueDataType_t *write_buffer, uint32_t write_data_len)
{
register uint32_t i, j;
register QueueDataType_t *t_in_ptr;
/* 由于已经能够判定有足够的空间,不需要再对 read_pos 值进行判断 */
j = queue_handle->queue_size - (queue_handle->write_pos);
t_in_ptr = &(queue_handle->queue_data[queue_handle->write_pos]);
if (j > write_data_len)
{
/* 写入时可以不用翻转 */
/* 用自加指令可以明显提高指令的执行速度 */
for (i = 0U; i < write_data_len; i++)
{
*t_in_ptr = *write_buffer;
t_in_ptr++;
write_buffer++;
}
queue_handle->write_pos += write_data_len;
}
else
{
/* 写入过程中必然翻转 */
/* 将数据从当前指针到缓冲区尾部,写入的长度为j */
for (i = 0U; i < j; i++)
{
*t_in_ptr = *write_buffer;
t_in_ptr++;
write_buffer++;