简介
- message queue 用于中断和线程之间进行数据传输的一项服务,其本质是一个环形缓冲区,例如串口驱动中通常会使用环形缓冲区来接收数据,缓冲区中每一项的大小是固定的。
- Zephyr 中的 msgq 是消息队列的具体实现,它需要提供一个缓冲区用于存储数据,缓冲区的大小是缓存项大小的整数倍,与 Zephyr 中的 queue 不同,消息队列写入和读取会拷贝数据。
数据结构
k_msgq
- k_msgq 是 Zephyr 中用于描述 消息队列的一个结构体,其成员变量包含了实现消息队列的必要元素:
struct k_msgq {
_wait_q_t wait_q;
struct k_spinlock lock;
size_t msg_size;
uint32_t max_msgs;
char *buffer_start;
char *buffer_end;
char *read_ptr;
char *write_ptr;
uint32_t used_msgs;
_POLL_EVENT;
uint8_t flags;
SYS_PORT_TRACING_TRACKING_FIELD(k_msgq)
};
- wait_q
- 保存因缓冲区为空被阻塞的接收线程,或者是因缓冲区满被阻塞的发送线程。
- lock
- msg_size
- max_msgs
- buffer_start
- buffer_end
- read_ptr
- 类似于环形缓冲区中的 head,每读取一次地址向后偏移固定长度。
- write_ptr
- 类似于环形缓冲区中的 tail,每写入一次地址向后偏移固定长度。
- used_msgs
- 已经存放的消息条数,每读取一次减1,写入一次加1。
- flags
- 消息队列支持两种初始化方式,一种是用户提供缓冲区,另一种是由系统分配缓冲区,当选择由系统分配缓冲区时,在消息队列销毁时需要回收内存,为了标记内存来源,因此使用 flags 保存标志,下面是对应的标志位。
#define K_MSGQ_FLAG_ALLOC BIT(0)
定义消息队列
- 消息队列包含3个重要属性,单条消息的大小,缓冲区最大消息容量,缓冲区对齐大小。
- 定义一个静态缓冲区可使用 K_MSGQ_DEFINE 宏
#define K_MSGQ_DEFINE(q_name, q_msg_size, q_max_msgs, q_align) \
static char __noinit __aligned(q_align) \
_k_fifo_buf_##q_name[(q_max_msgs) * (q_msg_size)]; \
STRUCT_SECTION_ITERABLE(k_msgq, q_name) = \
Z_MSGQ_INITIALIZER(q_name, _k_fifo_buf_##q_name, \
q_msg_size, q_max_msgs)
- 还可通过函数 k_msgq_init 初始化消息队列,该函数需要提供环形缓冲区和一个 k_msgq 对象:
struct msg_item_type {
int val;
};
static char __aligned(4) s_msgq_buffer[10 * sizeof(struct msg_item_type )];
static struct k_msgq s_msgq;
k_msgq_init(&s_msgq, s_msgq_buffer, sizeof(struct msg_item_type ), 10);
- k_msgq_init 的另一个版本是 k_msgq_alloc_init,它可以从系统中动态分配一段空间作为环形缓冲区:
static struct k_msgq s_msgq;
k_msgq_alloc_init(&s_msgq, sizeof(struct msg_item_type ), 10);
发送消息
k_msgq_put
- 消息队列的消息发送函数:
- int k_msgq_put(struct k_msgq *msgq, const void *data, k_timeout_t timeout)
- 该函数的实现函数为 z_impl_k_msgq_put
int z_impl_k_msgq_put(struct k_msgq *msgq, const void *data, k_timeout_t timeout)
{
__ASSERT(!arch_is_in_isr() || K_TIMEOUT_EQ(timeout, K_NO_WAIT), "");
struct k_thread *pending_thread;
k_spinlock_key_t key;
int result;
key = k_spin_lock(&msgq->lock);
SYS_PORT_TRACING_OBJ_FUNC_ENTER(k_msgq, put, msgq, timeout);
if (msgq->used_msgs < msgq->max_msgs) {
pending_thread = z_unpend_first_thread(&msgq->wait_q);
if (pending_thread != NULL) {
SYS_PORT_TRACING_OBJ_FUNC_EXIT(k_msgq, put, msgq, timeout, 0);
(void)memcpy(pending_thread->base.swap_data, data,
msgq->msg_size);
arch_thread_return_value_set(pending_thread, 0);
z_ready_thread(pending_thread);
z_reschedule(&msgq->lock, key);
return 0;
} else {
(void)memcpy(msgq->write_ptr, data, msgq->msg_size);
msgq->write_ptr += msgq->msg_size;
if (msgq->write_ptr == msgq->buffer_end) {
msgq->write_ptr = msgq->buffer_start;
}
msgq->used_msgs++;
#ifdef CONFIG_POLL
handle_poll_events(msgq, K_POLL_STATE_MSGQ_DATA_AVAILABLE);
#endif
}
result = 0;
}
else if (K_TIMEOUT_EQ(timeout, K_NO_WAIT)) {
result = -ENOMSG;
}
else {
SYS_PORT_TRACING_OBJ_FUNC_BLOCKING(k_msgq, put, msgq, timeout);
_current->base.swap_data = (void *) data;
result = z_pend_curr(&msgq->lock, key, &msgq->wait_q, timeout);
SYS_PORT_TRACING_OBJ_FUNC_EXIT(k_msgq, put, msgq, timeout, result);
return result;
}
SYS_PORT_TRACING_OBJ_FUNC_EXIT(k_msgq, put, msgq, timeout, result);
k_spin_unlock(&msgq->lock, key);
return result;
}
- k_msgq_put 用于将消息写入环形缓冲区,该函数允许在中断中使用,在中断使用时 timeout 必须是0.
- 当等待队列非空时,可将数据直接拷贝至 tcb 中 swap_data 指向的内存中,相比放入队列后再从队列中将数据拷贝到用户空间,这种方式提高了效率。
- 只有当等待队列为空时,才会将数据暂存入环形缓冲区中,等待线程读取。
接收消息
k_msgq_get
- 消息队列的消息接收函数:
- int k_msgq_get(struct k_msgq *msgq, void *data, k_timeout_t timeout)
- 该函数的实现函数为 z_impl_k_msgq_get
int z_impl_k_msgq_get(struct k_msgq *msgq, void *data, k_timeout_t timeout)
{
__ASSERT(!arch_is_in_isr() || K_TIMEOUT_EQ(timeout, K_NO_WAIT), "");
k_spinlock_key_t key;
struct k_thread *pending_thread;
int result;
key = k_spin_lock(&msgq->lock);
SYS_PORT_TRACING_OBJ_FUNC_ENTER(k_msgq, get, msgq, timeout);
if (msgq->used_msgs > 0U) {
(void)memcpy(data, msgq->read_ptr, msgq->msg_size);
msgq->read_ptr += msgq->msg_size;
if (msgq->read_ptr == msgq->buffer_end) {
msgq->read_ptr = msgq->buffer_start;
}
msgq->used_msgs--;
pending_thread = z_unpend_first_thread(&msgq->wait_q);
if (pending_thread != NULL) {
SYS_PORT_TRACING_OBJ_FUNC_BLOCKING(k_msgq, get, msgq, timeout);
(void)memcpy(msgq->write_ptr, pending_thread->base.swap_data,
msgq->msg_size);
msgq->write_ptr += msgq->msg_size;
if (msgq->write_ptr == msgq->buffer_end) {
msgq->write_ptr = msgq->buffer_start;
}
msgq->used_msgs++;
arch_thread_return_value_set(pending_thread, 0);
z_ready_thread(pending_thread);
z_reschedule(&msgq->lock, key);
SYS_PORT_TRACING_OBJ_FUNC_EXIT(k_msgq, get, msgq, timeout, 0);
return 0;
}
result = 0;
}
else if (K_TIMEOUT_EQ(timeout, K_NO_WAIT)) {
result = -ENOMSG;
}
else {
SYS_PORT_TRACING_OBJ_FUNC_BLOCKING(k_msgq, get, msgq, timeout);
_current->base.swap_data = data;
result = z_pend_curr(&msgq->lock, key, &msgq->wait_q, timeout);
SYS_PORT_TRACING_OBJ_FUNC_EXIT(k_msgq, get, msgq, timeout, result);
return result;
}
SYS_PORT_TRACING_OBJ_FUNC_EXIT(k_msgq, get, msgq, timeout, result);
k_spin_unlock(&msgq->lock, key);
return result;
}
- k_msgq_get 首先会判断缓冲区中是否有数据,如果缓冲区中有数据,会从中取出一条数据拷贝到用户空间。
- 缓冲区中有数据就意味着等待队列中可能存在因缓冲区满无法写入而被阻塞的线程,此时由于从缓冲区中读出一条数据,可以进行写入,便将被挂起线程中未写入的数据拷贝到缓冲区中,同时解除挂起。
- 如果缓冲区无数据且等待时间为0,返回 -ENOMSG。
- 如果缓冲区无数据且等待时间不为0,将当前线程挂起。
wait_q 的双重身份
- 消息队列与其他 IPC 对象中的 wait_q 有所不同,以Mutex为例,只有当加锁时需要进入等待,因此 wait_q 中存放的一定是因加锁被阻塞的线程。
- 而在消息队列中无论是发送数据还是接收数据都可能进入等待,并且它们使用的是同一个等待队列。
- 首先来看一下进入等待队列的条件:
- 发送线程在缓冲区满,且等待时间不为0时,将被放入等待队列中。
- 接收线程在缓冲区空,且等待时间不为0时,将被放入等待队列中。
- 两种情况不可能同时存在,因此可以共用一个等待队列。
清理消息队列
k_msgq_cleanup
- 清理消息队列主要作用是,回收系统分配的缓冲区。
- 为了安全,在清理时会检查消息队列是否仍然在被使用
int k_msgq_cleanup(struct k_msgq *msgq)
{
SYS_PORT_TRACING_OBJ_FUNC_ENTER(k_msgq, cleanup, msgq);
CHECKIF(z_waitq_head(&msgq->wait_q) != NULL) {
SYS_PORT_TRACING_OBJ_FUNC_EXIT(k_msgq, cleanup, msgq, -EBUSY);
return -EBUSY;
}
if ((msgq->flags & K_MSGQ_FLAG_ALLOC) != 0U) {
k_free(msgq->buffer_start);
msgq->flags &= ~K_MSGQ_FLAG_ALLOC;
}
SYS_PORT_TRACING_OBJ_FUNC_EXIT(k_msgq, cleanup, msgq, 0);
return 0;
}
重置消息队列
k_msgq_purge
- void k_msgq_purge(struct k_msgq *msgq)
void z_impl_k_msgq_purge(struct k_msgq *msgq)
{
k_spinlock_key_t key;
struct k_thread *pending_thread;
key = k_spin_lock(&msgq->lock);
SYS_PORT_TRACING_OBJ_FUNC(k_msgq, purge, msgq);
while ((pending_thread = z_unpend_first_thread(&msgq->wait_q)) != NULL) {
arch_thread_return_value_set(pending_thread, -ENOMSG);
z_ready_thread(pending_thread);
}
msgq->used_msgs = 0;
msgq->read_ptr = msgq->write_ptr;
z_reschedule(&msgq->lock, key);
}
- 该函数用于清空消息队列,如果有线程因数据发送或接收而进入等待,同时会将被挂起的线程唤醒。
读取数据
k_msgq_peek
- int k_msgq_peek(struct k_msgq *msgq, void *data)
- 该函数以先进先出的方式从消息队列中读取一条消息,但是不会修改 read_str 将队列项移出队列,因此该操作无需等待。
k_msgq_peek_at
- int k_msgq_peek_at(struct k_msgq *msgq, void *data, uint32_t idx)
- k_msgq_peek_at 是 k_msgq_peek 的另一个版本,以 read_str 作为第一项,向后偏移 idx 项之后的数据,将其拷贝到 data 指向的内存中。
缓冲区容量
k_msgq_num_free_get
- uint32_t k_msgq_num_free_get(struct k_msgq *msgq)
- k_msgq_num_free_get 用于获取缓冲区中剩余容量,返回值代表可存储消息的数量。
k_msgq_num_used_get
- uint32_t k_msgq_num_used_get(struct k_msgq *msgq)
- k_msgq_num_used_get 用于获取已经使用的容量,即 used_msgs 的值。