在嵌入式系统开发中,队列是最基础也是最重要的数据结构之一。它遵循先进先出(FIFO)的原则,就像我们日常生活中排队买票一样,先来的人先得到服务。这种特性使得队列特别适合处理需要按顺序处理的数据,比如串口通信中的数据缓存、任务调度中的消息传递等场景。
在嵌入式环境下,我们通常有两种实现队列的方式:
数组实现:
链表实现:
在嵌入式开发中,链表实现通常是更优的选择。原因有三:
一个完整的队列需要支持以下核心操作:
我们先来看队列的核心数据结构定义。在Queue.h头文件中,我们定义了以下内容:
c复制#ifndef QUEUE_H
#define QUEUE_H
#include <stdio.h>
#include <stdlib.h>
// 队列数据类型定义(可根据需求修改)
typedef int DATATYPE;
// 队列节点结构体
typedef struct QueueNode {
DATATYPE data; // 数据域
struct QueueNode *next; // 指针域
} QueueNode;
// 队列管理结构体
typedef struct Queue {
QueueNode *front; // 队头指针(指向头节点)
QueueNode *rear; // 队尾指针(指向尾节点)
} Queue;
// 函数声明
Queue* InitQueue();
int IsEmpty(Queue *queue);
int EnQueue(Queue *queue, DATATYPE data);
int DeQueue(Queue *queue);
DATATYPE GetFront(Queue* queue);
void PrintQueue(Queue *queue);
void DestroyQueue(Queue *queue);
#endif
这里有几个关键设计点:
队列初始化的核心是创建一个头节点,并让front和rear指针都指向它:
c复制Queue* InitQueue()
{
// 分配队列管理结构体
Queue *queue = (Queue*)malloc(sizeof(Queue));
if (queue == NULL) {
perror("队列结构体分配失败");
return NULL;
}
// 创建头节点(不存储有效数据)
QueueNode *head = (QueueNode*)malloc(sizeof(QueueNode));
if (head == NULL) {
perror("头节点分配失败");
free(queue);
return NULL;
}
head->next = NULL;
queue->front = queue->rear = head;
return queue;
}
注意事项:
入队操作是在队列尾部添加新元素:
c复制int EnQueue(Queue *queue, DATATYPE data)
{
if (queue == NULL || queue->front == NULL || queue->rear == NULL) {
printf("队列未初始化\n");
return 0;
}
// 创建新节点
QueueNode *newNode = (QueueNode*)malloc(sizeof(QueueNode));
if (newNode == NULL) {
perror("新节点分配失败");
return 0;
}
newNode->data = data;
newNode->next = NULL;
// 将新节点链接到队尾
queue->rear->next = newNode;
queue->rear = newNode;
return 1;
}
关键点:
出队操作是从队列头部移除元素:
c复制int DeQueue(Queue *queue)
{
if (queue == NULL) {
printf("队列指针为空\n");
return -1;
}
if (IsEmpty(queue)) {
printf("队列为空\n");
return 0;
}
// 保存要移除的节点
QueueNode *temp = queue->front->next;
DATATYPE data = temp->data;
// 更新front指针
queue->front->next = temp->next;
// 如果出队的是最后一个节点,需要更新rear指针
if (temp->next == NULL) {
queue->rear = queue->front;
}
free(temp);
return data;
}
常见错误:
判空操作非常简单:
c复制int IsEmpty(Queue *queue)
{
if (queue == NULL) {
printf("队列指针为空\n");
return -1;
}
return queue->front == queue->rear;
}
获取队首元素:
c复制DATATYPE GetFront(Queue* queue)
{
if (queue == NULL) {
printf("队列指针为空\n");
return -1;
}
if (IsEmpty(queue)) {
printf("队列为空\n");
return -1;
}
return queue->front->next->data;
}
打印队列内容(调试用):
c复制void PrintQueue(Queue *queue)
{
if (queue == NULL) {
printf("队列指针为空\n");
return;
}
if (IsEmpty(queue)) {
printf("队列为空\n");
return;
}
QueueNode *current = queue->front->next;
printf("队列内容: ");
while (current != NULL) {
printf("%d ", current->data);
current = current->next;
}
printf("\n");
}
在嵌入式系统中,正确释放内存至关重要:
c复制void DestroyQueue(Queue *queue)
{
if (queue == NULL) return;
// 释放所有数据节点
QueueNode *current = queue->front;
while (current != NULL) {
QueueNode *temp = current;
current = current->next;
free(temp);
}
// 释放队列管理结构体
free(queue);
}
注意事项:
在嵌入式环境下使用队列时:
典型的串口数据处理流程:
c复制// 全局队列
Queue *uartQueue;
// 串口中断服务程序
void UART_ISR()
{
uint8_t data = UART->DR; // 读取接收到的数据
EnQueue(uartQueue, data);
}
// 主循环处理
void main()
{
uartQueue = InitQueue();
while (1) {
if (!IsEmpty(uartQueue)) {
uint8_t data = DeQueue(uartQueue);
processData(data);
}
}
}
注意事项:
在多任务系统中,队列是任务通信的理想选择:
c复制// 定义消息结构
typedef struct {
uint8_t msgType;
uint32_t param;
} Message;
// 创建消息队列
Queue *msgQueue = InitQueue();
// 任务1发送消息
void Task1()
{
Message msg;
msg.msgType = 1;
msg.param = 123;
EnQueue(msgQueue, msg);
}
// 任务2接收消息
void Task2()
{
if (!IsEmpty(msgQueue)) {
Message msg = DeQueue(msgQueue);
handleMessage(msg);
}
}
关键点:
在实时性要求高的场景,可以考虑无锁队列:
c复制typedef struct {
volatile QueueNode *front;
volatile QueueNode *rear;
} LockFreeQueue;
// 使用CAS(Compare-And-Swap)原子操作实现无锁队列
int LockFreeEnQueue(LockFreeQueue *queue, DATATYPE data)
{
QueueNode *newNode = malloc(sizeof(QueueNode));
newNode->data = data;
newNode->next = NULL;
QueueNode *oldRear = queue->rear;
while (!__sync_bool_compare_and_swap(&queue->rear, oldRear, newNode)) {
oldRear = queue->rear;
}
oldRear->next = newNode;
return 1;
}
注意事项:
频繁的动态内存分配会影响性能,可以使用内存池:
c复制#define POOL_SIZE 100
QueueNode nodePool[POOL_SIZE];
int freeIndex = 0;
QueueNode* AllocNode()
{
if (freeIndex >= POOL_SIZE) return NULL;
return &nodePool[freeIndex++];
}
void FreeNode(QueueNode *node)
{
// 在简单实现中可以不真正释放
// 或者维护一个空闲列表
}
优点:
在某些特定场景,环形缓冲区可能是更好的选择:
c复制typedef struct {
DATATYPE *buffer;
int size;
int front;
int rear;
int count;
} CircularBuffer;
void InitBuffer(CircularBuffer *cb, int size)
{
cb->buffer = malloc(size * sizeof(DATATYPE));
cb->size = size;
cb->front = cb->rear = cb->count = 0;
}
适用场景:
队列操作导致系统崩溃
内存泄漏
数据损坏
添加调试信息
c复制void PrintQueueDebug(Queue *queue)
{
printf("Queue@%p: front=%p, rear=%p\n",
queue, queue->front, queue->rear);
PrintQueue(queue);
}
使用断言
c复制#include <assert.h>
int EnQueue(Queue *queue, DATATYPE data)
{
assert(queue != NULL);
assert(queue->front != NULL);
assert(queue->rear != NULL);
// ...
}
单元测试
数据类型大小
c复制#include <stdint.h>
typedef int32_t DATATYPE; // 确保4字节
内存对齐
c复制#pragma pack(push, 1)
typedef struct {
uint8_t type;
uint32_t data;
} Message;
#pragma pack(pop)
字节序问题
c复制uint32_t SwapEndian(uint32_t value)
{
return ((value & 0xFF) << 24) |
((value & 0xFF00) << 8) |
((value >> 8) & 0xFF00) |
((value >> 24) & 0xFF);
}
无操作系统环境
RTOS环境
低功耗设计
时间测量
c复制#include <time.h>
clock_t start = clock();
for (int i = 0; i < 1000; i++) {
EnQueue(queue, i);
}
clock_t end = clock();
double timeUsed = ((double)(end - start)) / CLOCKS_PER_SEC;
内存使用统计
c复制size_t GetQueueMemoryUsage(Queue *queue)
{
size_t total = sizeof(Queue);
QueueNode *current = queue->front;
while (current != NULL) {
total += sizeof(QueueNode);
current = current->next;
}
return total;
}
压力测试
批量操作
c复制int EnQueueBatch(Queue *queue, DATATYPE *data, int count)
{
// 一次分配多个节点
// 批量链接节点
// 单次更新rear指针
}
缓存友好设计
内联函数
c复制static inline int IsEmpty(Queue *queue)
{
return queue->front == queue->rear;
}
c复制typedef struct {
DATATYPE data;
int priority;
} PriorityItem;
int PriorityEnQueue(Queue *queue, PriorityItem item)
{
QueueNode *newNode = malloc(sizeof(QueueNode));
newNode->data = item;
QueueNode *prev = queue->front;
QueueNode *current = prev->next;
while (current != NULL && current->data.priority > item.priority) {
prev = current;
current = current->next;
}
prev->next = newNode;
newNode->next = current;
if (current == NULL) {
queue->rear = newNode;
}
return 1;
}
c复制typedef struct {
QueueNode *front;
QueueNode *rear;
} Deque;
int DequePushFront(Deque *deque, DATATYPE data)
{
QueueNode *newNode = malloc(sizeof(QueueNode));
newNode->data = data;
newNode->next = deque->front->next;
deque->front->next = newNode;
if (deque->rear == deque->front) {
deque->rear = newNode;
}
return 1;
}
c复制#include <pthread.h>
typedef struct {
Queue queue;
pthread_mutex_t lock;
} ThreadSafeQueue;
void InitTSQueue(ThreadSafeQueue *tsq)
{
InitQueue(&tsq->queue);
pthread_mutex_init(&tsq->lock, NULL);
}
int TSEnQueue(ThreadSafeQueue *tsq, DATATYPE data)
{
pthread_mutex_lock(&tsq->lock);
int result = EnQueue(&tsq->queue, data);
pthread_mutex_unlock(&tsq->lock);
return result;
}
推荐的项目结构:
code复制/project
/include
queue.h
/src
queue.c
main.c
/tests
test_queue.c
Makefile
为队列实现添加版本信息
c复制#define QUEUE_VERSION "1.0.2"
const char* GetQueueVersion()
{
return QUEUE_VERSION;
}
使用git管理代码变更
bash复制git tag -a v1.0.0 -m "Initial stable version"
git push origin --tags
头文件注释示例:
c复制/**
* @file queue.h
* @brief 链表实现的队列数据结构
* @version 1.0.2
* @date 2023-06-15
*/
函数注释示例:
c复制/**
* @brief 初始化一个新的队列
* @return 成功返回队列指针,失败返回NULL
* @note 使用完毕后必须调用DestroyQueue释放内存
*/
Queue* InitQueue(void);
使用Doxygen生成文档:
bash复制doxygen Doxyfile
嵌入式常用测试框架:
c复制#include "unity.h"
#include "queue.h"
void setUp(void) {
// 每个测试用例运行前执行
}
void tearDown(void) {
// 每个测试用例运行后执行
}
void test_QueueInit(void)
{
Queue *q = InitQueue();
TEST_ASSERT_NOT_NULL(q);
TEST_ASSERT_TRUE(IsEmpty(q));
DestroyQueue(q);
}
void test_EnQueueDeQueue(void)
{
Queue *q = InitQueue();
EnQueue(q, 10);
EnQueue(q, 20);
TEST_ASSERT_FALSE(IsEmpty(q));
TEST_ASSERT_EQUAL(10, DeQueue(q));
TEST_ASSERT_EQUAL(20, DeQueue(q));
TEST_ASSERT_TRUE(IsEmpty(q));
DestroyQueue(q);
}
int main(void)
{
UNITY_BEGIN();
RUN_TEST(test_QueueInit);
RUN_TEST(test_EnQueueDeQueue);
return UNITY_END();
}
| 实现方式 | 入队(us) | 出队(us) | 内存开销(字节/节点) |
|---|---|---|---|
| 链表队列 | 1.2 | 1.1 | 12 |
| 数组队列 | 0.3 | 0.2 | 4 |
| 内存池队列 | 0.5 | 0.4 | 12 |
| 无锁队列 | 0.8 | 0.7 | 16 |
A: 通常是因为:
A: 考虑以下因素:
A: 解决方案: