Zephyr 消息队列

简介

  • message queue 用于中断和线程之间进行数据传输的一项服务,其本质是一个环形缓冲区,例如串口驱动中通常会使用环形缓冲区来接收数据,缓冲区中每一项的大小是固定的。
  • Zephyr 中的 msgq 是消息队列的具体实现,它需要提供一个缓冲区用于存储数据,缓冲区的大小是缓存项大小的整数倍,与 Zephyr 中的 queue 不同,消息队列写入和读取会拷贝数据。

数据结构

k_msgq

  • k_msgq 是 Zephyr 中用于描述 消息队列的一个结构体,其成员变量包含了实现消息队列的必要元素:
struct k_msgq {
	/** Message queue wait queue */
	_wait_q_t wait_q;
	/** Lock */
	struct k_spinlock lock;
	/** Message size */
	size_t msg_size;
	/** Maximal number of messages */
	uint32_t max_msgs;
	/** Start of message buffer */
	char *buffer_start;
	/** End of message buffer */
	char *buffer_end;
	/** Read pointer */
	char *read_ptr;
	/** Write pointer */
	char *write_ptr;
	/** Number of used messages */
	uint32_t used_msgs;

	_POLL_EVENT;

	/** Message queue */
	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)
{
	/* 当 k_msgq_put 在中断中调用时,timeout 不等于 K_NO_WAIT 会触发断言 */
	__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);

	/* 消息队列未满时,如果等待队列中存在等待线程,
	 * 可将数据写入 swap_data 指向的内存中(线程在等待时已将缓存首地址放入swap_data),
	 * 反之如果没有线程处于等待状态,将数据放入环形缓冲区中
	 */
	if (msgq->used_msgs < msgq->max_msgs) {
		/* message queue isn't full */
		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);

			/* give message to waiting thread */
			(void)memcpy(pending_thread->base.swap_data, data,
			       msgq->msg_size);
			/* wake up waiting thread */
			arch_thread_return_value_set(pending_thread, 0);
			z_ready_thread(pending_thread);
			z_reschedule(&msgq->lock, key);
			return 0;
		} else {
			/* put message in queue */
			(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 /* CONFIG_POLL */
		}
		result = 0;
	
	} 
	/* 队列满且等待时间为0,返回 -ENOMSG */
	else if (K_TIMEOUT_EQ(timeout, K_NO_WAIT)) {
		/* don't wait for message space to become available */
		result = -ENOMSG;
	} 
	/* 队列满且等待时间不为0,将当前线程的 swap_data 指向数据首地址,然后将调用线程挂起 */
	else {
		SYS_PORT_TRACING_OBJ_FUNC_BLOCKING(k_msgq, put, msgq, timeout);

		/* wait for put message success, failure, or 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) {
		/* take first available message from queue */
		(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;
	}
	/* 缓冲区中没有数据,且等待时间为0,返回 -ENOMSG */
	 else if (K_TIMEOUT_EQ(timeout, K_NO_WAIT)) {
		/* don't wait for a message to become available */
		result = -ENOMSG;
	} 
	/* 缓冲区中没有数据,且等待时间不为0,将调用线程添加到等待队列并挂起 */
	else {
		SYS_PORT_TRACING_OBJ_FUNC_BLOCKING(k_msgq, get, msgq, timeout);

		/* wait for get message success or 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);

	/* 唤醒等待队列中被阻塞的线程,然后返回-ENOMSG */
	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 的值。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

咕咚.萌西

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值