3. 消息队列核心API设计:创建/销毁队列、发送消息、接收消息、队列状态查询

消息队列的API设计,是整个事件驱动架构的基石。说白了,你后面所有的模块通信、任务调度、异步处理,都得靠这几个接口撑着。我做了这么多年嵌入式,见过太多项目因为API设计得不好,后面改得痛不欲生。今天咱们就把这五个核心接口——创建、销毁、发送、接收、状态查询——一次讲透。

3.1 队列控制块:一切的基础

在动手写API之前,得先定义好队列的数据结构。我个人习惯用一个结构体来管理队列的所有元信息,这样不管是创建还是查询状态,都方便得很。

/* 队列控制块 */
typedef struct {
    uint8_t *buffer;        /* 存储消息的环形缓冲区 */
    uint32_t msg_size;      /* 每条消息的固定大小(字节) */
    uint32_t capacity;      /* 队列最大消息条数 */
    uint32_t head;          /* 读指针(出队位置) */
    uint32_t tail;          /* 写指针(入队位置) */
    uint32_t count;         /* 当前队列中的消息数量 */
    uint32_t peak_count;    /* 历史最高使用量(调试用) */
    uint32_t overflow_cnt;  /* 溢出次数统计 */
    uint8_t  initialized;   /* 初始化标志 */
} mq_t;

嗯,这里要注意:msg_size是固定值。为什么不用变长消息?我在项目中遇到过,变长消息的内存管理太容易出碎片了,尤其是在MCU上。固定大小虽然浪费一点空间,但胜在稳定、可预测。

3.2 创建队列:mqx_create

创建队列,说白了就是给队列控制块分配内存,并且把环形缓冲区准备好。我建议把队列控制块和缓冲区放在一起管理,这样释放的时候不容易漏。

mq_t *mqx_create(uint32_t capacity, uint32_t msg_size) {
    mq_t *mq;

    /* 参数校验:容量和消息大小必须大于0 */
    if (capacity == 0 || msg_size == 0) return NULL;

    /* 一次性分配控制块 + 缓冲区 */
    mq = (mq_t *)malloc(sizeof(mq_t) + capacity * msg_size);
    if (mq == NULL) return NULL;

    mq->buffer    = (uint8_t *)(mq + 1);  /* 缓冲区紧跟在控制块后面 */
    mq->msg_size  = msg_size;
    mq->capacity  = capacity;
    mq->head      = 0;
    mq->tail      = 0;
    mq->count     = 0;
    mq->peak_count = 0;
    mq->overflow_cnt = 0;
    mq->initialized = 1;

    return mq;
}
我的经验: 创建时就把 initialized 置为1,后面所有操作都先检查这个标志。我曾经在一个项目里忘了初始化队列就直接发送消息,结果写指针乱飞,把整个内存区都踩坏了。从那以后,我每个API入口都加一句 if (!mq->initialized) return ERROR;

3.3 销毁队列:mqx_destroy

销毁很简单,但容易踩坑。你想想看,如果队列里还有消息没处理完,直接free掉内存,那这些消息就永远丢了。所以销毁前最好做个检查。

int mqx_destroy(mq_t *mq) {
    if (mq == NULL || !mq->initialized) return -1;

    /* 安全起见:如果队列非空,可以打印警告 */
    if (mq->count > 0) {
        /* 这里可以记录日志,或者回调用户注册的清理函数 */
        printf("[WARN] Destroying queue with %u messages pending\n", mq->count);
    }

    mq->initialized = 0;
    free(mq);  /* 注意:控制块和缓冲区是一起分配的,一次free即可 */
    return 0;
}
警告: 千万不要单独free mq->buffer!因为缓冲区是紧跟在控制块后面的,不是独立malloc出来的。如果你分开分配,那就要分开释放,否则内存泄漏。我建议统一分配,省心。

3.4 发送消息:mqx_send

发送消息的核心逻辑就是:把数据拷贝到环形缓冲区的tail位置,然后移动tail指针。如果队列满了怎么办?这就涉及到策略问题了。

int mqx_send(mq_t *mq, const void *msg) {
    uint8_t *dest;

    if (mq == NULL || !mq->initialized || msg == NULL) return -1;

    /* 队列满了? */
    if (mq->count >= mq->capacity) {
        mq->overflow_cnt++;
        return -2;  /* 返回队列满的错误码 */
    }

    /* 计算目标地址 */
    dest = mq->buffer + (mq->tail * mq->msg_size);
    memcpy(dest, msg, mq->msg_size);

    /* 移动tail指针(环形) */
    mq->tail = (mq->tail + 1) % mq->capacity;
    mq->count++;

    /* 更新峰值 */
    if (mq->count > mq->peak_count) {
        mq->peak_count = mq->count;
    }

    return 0;
}

你可能会问:为什么不用链表?我在项目中试过链表队列,插入删除确实灵活,但每个节点都要动态分配内存,在中断里根本不敢用。环形缓冲区+固定大小消息,才是嵌入式系统的王道。

3.5 接收消息:mqx_receive

接收就是从head位置取数据。注意:取完数据后,head指针移动,但缓冲区里的旧数据不用清空——下次写入时会直接覆盖。

int mqx_receive(mq_t *mq, void *msg) {
    uint8_t *src;

    if (mq == NULL || !mq->initialized || msg == NULL) return -1;

    /* 队列空了? */
    if (mq->count == 0) return -3;

    /* 从head位置读取 */
    src = mq->buffer + (mq->head * mq->msg_size);
    memcpy(msg, src, mq->msg_size);

    /* 移动head指针 */
    mq->head = (mq->head + 1) % mq->capacity;
    mq->count--;

    return 0;
}
关键点: 发送和接收都是O(1)时间复杂度,不依赖队列长度。这就是环形缓冲区的魅力——不管队列里有100条还是1000条消息,操作时间都一样。

3.6 队列状态查询:mqx_status

状态查询这个接口,很多人觉得可有可无。但我在实际项目中吃过亏——有一次系统莫名其妙卡死,查了半天才发现是队列满了,发送方一直在重试。从那以后,我每个队列都定期查询状态,提前预警。

typedef struct {
    uint32_t capacity;      /* 总容量 */
    uint32_t count;         /* 当前消息数 */
    uint32_t free_space;    /* 剩余空间 */
    uint32_t peak_count;    /* 历史峰值 */
    uint32_t overflow_cnt;  /* 溢出次数 */
    uint8_t  is_full;       /* 是否已满 */
    uint8_t  is_empty;      /* 是否为空 */
} mq_status_t;

int mqx_status(const mq_t *mq, mq_status_t *status) {
    if (mq == NULL || !mq->initialized || status == NULL) return -1;

    status->capacity    = mq->capacity;
    status->count       = mq->count;
    status->free_space  = mq->capacity - mq->count;
    status->peak_count  = mq->peak_count;
    status->overflow_cnt = mq->overflow_cnt;
    status->is_full     = (mq->count >= mq->capacity);
    status->is_empty    = (mq->count == 0);

    return 0;
}

3.7 核心API一览

API 功能 返回值 注意事项
mqx_create 创建队列 队列指针 / NULL 一次性分配控制块+缓冲区
mqx_destroy 销毁队列 0成功 / -1失败 销毁前检查是否有未处理消息
mqx_send 发送消息 0成功 / -2队列满 固定大小消息,memcpy拷贝
mqx_receive 接收消息 0成功 / -3队列空 非阻塞,空队列立即返回
mqx_status 查询状态 0成功 / -1失败 返回容量、使用量、峰值等

3.8 核心逻辑流程图

下面这张图展示了消息队列的核心工作流程,从创建到发送、接收,再到状态查询和销毁,一目了然。

消息队列核心API工作流程 mqx_create() 初始化? mqx_send() 队列满? 写入环形缓冲区 溢出++ mqx_receive() 队列空? mqx_status() mqx_destroy()

3.9 避坑指南

最后,分享几个我这些年踩过的坑:

  • 中断里调用API要小心: 我曾经在中断服务函数里直接调用 mqx_send,结果主循环也在调用,两个地方同时修改 tail 指针,数据全乱了。解决方案是加临界区保护,或者用无锁队列。
  • 消息大小别算错: 有个同事把 msg_size 设成了结构体的大小,但结构体里有指针。结果发送的是指针值,接收方读到的是野指针。记住:队列里只能存值,不能存引用。
  • 状态查询别太频繁: 每毫秒查一次状态,CPU全耗在查询上了。我一般建议在关键节点(比如发送失败、定时任务)才查一次。

好了,这五个核心API的设计思路和实现细节就讲到这里。你把这些接口封装好了,后面的事件分发、任务调度就都有了坚实的基础。记住:好的API设计,是让调用者用着舒服,让维护者看着明白。


公众号:蓝海资料掘金营,微信deep3321