消息队列

消息队列 是实现简单消息队列的内核对象,允许线程和 ISR 异步发送和接收固定大小的数据项。

概念

可以定义任意数量的消息队列(仅受可用 RAM 限制)。每个消息队列都通过其内存地址引用。

消息队列具有以下主要属性:

  • 一个 环形缓冲区,存放已发送但尚未接收的数据项。

  • 数据项大小,以字节为单位。

  • 环形缓冲区可排队存放数据项的 最大数量。

消息队列必须初始化后才能使用。初始化会将其环形缓冲区置空。

线程或 ISR 均可向消息队列 发送 数据项。如果有线程正在等待,则将发送线程所指向的数据项复制给该线程;否则,在有可用空间时将数据项复制到消息队列的环形缓冲区。无论哪种情况,发送的数据区域大小都 必须 等于消息队列的数据项大小。

如果线程在环形缓冲区已满时尝试发送数据项,它可以选择等待可用空间。环形缓冲区已满时,任意数量的发送线程都可以同时等待;空间可用后,会优先交给优先级最高且等待时间最长的发送线程。

线程可以从消息队列 接收 数据项。数据项会被复制到接收线程指定的区域;接收区域大小 必须 等于消息队列的数据项大小。

如果线程在环形缓冲区为空时尝试接收数据项,它可以选择等待数据项到来。环形缓冲区为空时,任意数量的接收线程都可以同时等待;数据项可用后,会被交给优先级最高且等待时间最长的接收线程。

线程还可以 窥视 消息队列头部的消息,而不将其移出队列。数据项会被复制到接收线程指定的区域;接收区域大小 必须 等于消息队列的数据项大小。

备注

内核允许 ISR 从消息队列接收数据项,但消息队列为空时,ISR 不得尝试等待。

备注

消息队列的环形缓冲区无需对齐。底层实现使用与对齐无关的 memcpy(),也不暴露任何内部指针。

实现

定义消息队列

消息队列使用 k_msgq 类型的变量定义,随后必须调用 k_msgq_init() 进行初始化。

以下代码定义并初始化一个空消息队列,最多可容纳 10 个数据项,每项长 12 字节。

struct data_item_type {
    uint32_t field1;
    uint32_t field2;
    uint32_t field3;
};

char my_msgq_buffer[10 * sizeof(struct data_item_type)];
struct k_msgq my_msgq;

k_msgq_init(&my_msgq, my_msgq_buffer, sizeof(struct data_item_type), 10);

也可以使用以下宏之一,在编译时定义并初始化消息队列:

以下代码与上面的代码片段效果相同。注意,该宏同时定义了消息队列及其缓冲区。

K_MSGQ_DEFINE(my_msgq, sizeof(struct data_item_type), 10, 1);

如果在编译时已知队列的数据项类型,可以使用简化宏 K_MSGQ_DEFINE_TYPE。该宏会自动配置队列的数据项大小和对齐方式:

K_MSGQ_DEFINE_TYPE(my_msgq, struct data_item_type, 10);

写入消息队列

调用 k_msgq_put() 可以向消息队列添加数据项。

以下代码延续上面的示例,使用消息队列将数据项从生产者线程传给一个或多个消费者线程。如果消费者处理不及导致消息队列已满,生产者线程会丢弃队列中的所有已有数据,以便保存较新的数据。注意,此 API 会触发重新调度。

void producer_thread(void)
{
    struct data_item_type data;

    while (1) {
        /* create data item to send (e.g. measurement, timestamp, ...) */
        data = ...

        /* send data to consumers */
        while (k_msgq_put(&my_msgq, &data, K_NO_WAIT) != 0) {
            /* message queue is full: purge old data & try again */
            k_msgq_purge(&my_msgq);
        }

        /* data item was successfully added to message queue */
    }
}

读取消息队列

调用 k_msgq_get() 可以从消息队列取出数据项。

以下代码延续上面的示例,使用消息队列处理一个或多个生产者线程生成的数据项。注意,应检查 k_msgq_get() 的返回值,因为调用 k_msgq_purge() 可能使其返回 -ENOMSG。

void consumer_thread(void)
{
    struct data_item_type data;

    while (1) {
        /* get a data item */
        k_msgq_get(&my_msgq, &data, K_FOREVER);

        /* process data item */
        ...
    }
}

窥视消息队列

调用 k_msgq_peek() 可以读取消息队列中的数据项。

以下代码窥视消息队列,读取由一个或多个生产者线程生成、位于队列头部的数据项。

void consumer_thread(void)
{
    struct data_item_type data;

    while (1) {
        /* read a data item by peeking into the queue */
        k_msgq_peek(&my_msgq, &data);

        /* process data item */
        ...
    }
}

使用建议

使用消息队列在线程之间异步传递小数据项。

备注

必要时,消息队列也可以传递大数据项。不过,这可能增加中断延迟,因为读写数据项时会锁定中断。由于需要将整个数据项复制到内存缓冲区或从中复制出来,读写耗时会随数据项大小线性增长。因此,传递大数据项时,通常更适合交换指向它的指针,而非传递数据项本身。

使用内核的邮箱对象可以实现同步传输。

配置选项

相关配置选项:

  • 无。

API 参考

Message Queue APIs