Zephyr sem

简介

  • 在 Zephyr 中,信号量(semaphore)是一种用于实现任务之间同步和互斥的同步原语。

互斥

  • 互斥(Mutual Exclusion):当多个任务需要访问共享资源时,信号量可以确保在同一时刻只有一个任务能够访问该资源。这可以防止多个任务同时修改共享资源,从而避免数据不一致和竞态条件的发生。
    • 例如,假设有两个任务 A 和 B 都需要访问一个共享的数据结构。通过使用信号量,我们可以确保在任务 A 访问数据结构时,任务 B 会被阻塞,直到任务 A 完成对数据结构的访问。这样就可以避免任务 A 和任务 B 同时修改数据结构,导致数据不一致的问题。

同步

  • 同步(Synchronization):信号量还可以用于协调多个任务之间的执行顺序。当一个任务需要等待另一个任务完成某个操作时,可以使用信号量来实现这种等待关系。
    • 例如,假设有两个任务 A 和 B,任务 A 需要等待任务 B 完成某个操作后才能继续执行。通过使用信号量,我们可以让任务 A 在等待信号量时阻塞,直到任务 B 完成操作并释放信号量。这样就实现了任务 A 和任务 B 之间的同步。

数据结构

struct k_sem {
	_wait_q_t wait_q;
	unsigned int count;
	unsigned int limit;
	_POLL_EVENT;
	SYS_PORT_TRACING_TRACKING_FIELD(k_sem)
};
  • wait_q 等待队列,当线程调用 k_sem_take 函数时,如果当前计数值为 0 且等待时间不为 0,调用线程将会被添加到等待队列中。
  • count 信号量当前计数值。
  • limit 信号量最大计数值。
  • 除此之外,如果使能了CONFIG_POLL,还会在每个对象中增加一个双向链表,调用 k_poll 时,如果当前count为0,等待时间不为0,会把调用者加入到链表中进入等待,直到超时或者信号量不为 0 时被唤醒。
#ifdef CONFIG_POLL
#define _POLL_EVENT_OBJ_INIT(obj) \
	.poll_events = SYS_DLIST_STATIC_INIT(&obj.poll_events),
#define _POLL_EVENT sys_dlist_t poll_events
#else
#define _POLL_EVENT_OBJ_INIT(obj)
#define _POLL_EVENT
#endif

信号量初始化

  • 信号量的初始化包含三个步骤:
    • 初始化等待队列
    • 设置信号量初始值
    • 设置信号量最大值
  • 需要注意的是,初始值必须小于等于最大值,最大值不能是0。

Z_SEM_INITIALIZER

#define Z_SEM_INITIALIZER(obj, initial_count, count_limit) \
	{ \
	.wait_q = Z_WAIT_Q_INIT(&obj.wait_q), \
	.count = initial_count, \
	.limit = count_limit, \
	_POLL_EVENT_OBJ_INIT(obj) \
	}

int k_sem_init (struct k_sem *sem, unsigned int initial_count, unsigned int limit)

int z_impl_k_sem_init(struct k_sem *sem, unsigned int initial_count,
		      unsigned int limit)
{
	/*
	 * Limit cannot be zero and count cannot be greater than limit
	 */
	CHECKIF(limit == 0U || limit > K_SEM_MAX_LIMIT || initial_count > limit) {
		SYS_PORT_TRACING_OBJ_FUNC(k_sem, init, sem, -EINVAL);

		return -EINVAL;
	}

	sem->count = initial_count;
	sem->limit = limit;

	SYS_PORT_TRACING_OBJ_FUNC(k_sem, init, sem, 0);

	z_waitq_init(&sem->wait_q);
#if defined(CONFIG_POLL)
	sys_dlist_init(&sem->poll_events);
#endif
	z_object_init(sem);

	return 0;
}

获取信号量

int k_sem_take(struct k_sem *sem, k_timeout_t timeout)

int z_impl_k_sem_take(struct k_sem *sem, k_timeout_t timeout)
{
	int ret = 0;

	__ASSERT(((arch_is_in_isr() == false) ||
		  K_TIMEOUT_EQ(timeout, K_NO_WAIT)), "");

	k_spinlock_key_t key = k_spin_lock(&lock);

	SYS_PORT_TRACING_OBJ_FUNC_ENTER(k_sem, take, sem, timeout);

	/* 信号量大于0则直接退出并返回0 */
	if (likely(sem->count > 0U)) {
		sem->count--;
		k_spin_unlock(&lock, key);
		ret = 0;
		goto out;
	}
	
	/* 信号量等于0,且timeout为0,退出并返回等待超时 */
	if (K_TIMEOUT_EQ(timeout, K_NO_WAIT)) {
		k_spin_unlock(&lock, key);
		ret = -EBUSY;
		goto out;
	}

	SYS_PORT_TRACING_OBJ_FUNC_BLOCKING(k_sem, take, sem, timeout);
	
	/* 否则刮起当前线程 */
	ret = z_pend_curr(&lock, key, &sem->wait_q, timeout);

out:
	SYS_PORT_TRACING_OBJ_FUNC_EXIT(k_sem, take, sem, timeout, ret);

	return ret;
}

释放信号量

void k_sem_give(struct k_sem *sem)

void z_impl_k_sem_give(struct k_sem *sem)
{
	k_spinlock_key_t key = k_spin_lock(&lock);
	struct k_thread *thread;

	SYS_PORT_TRACING_OBJ_FUNC_ENTER(k_sem, give, sem);
	
	thread = z_unpend_first_thread(&sem->wait_q);
	
	/* 等待队列中存在等待线程时则直接唤醒等待线程
	 * 如果不存在则需要将技术值加1,如果 poll_events 中不为空,
	 * 需要发送信号唤醒 k_poll 调用者, 然后通过k_sem_take 获取信号量。
	 */
	if (thread != NULL) {
		arch_thread_return_value_set(thread, 0);
		z_ready_thread(thread);
	} else {
		sem->count += (sem->count != sem->limit) ? 1U : 0U;
		handle_poll_events(sem);
	}

	z_reschedule(&lock, key);

	SYS_PORT_TRACING_OBJ_FUNC_EXIT(k_sem, give, sem);
}

获取计数值

unsigned int k_sem_count_get(struct k_sem *sem)

static inline unsigned int z_impl_k_sem_count_get(struct k_sem *sem)
{
	return sem->count;
}
  • count是一个与总线宽度相同的变量,CPU可以用一条指令获取到计数值,本身是一个原子操作,且没有其他数据与其有关,因此可以直接读取该值获得当前计数值。

重置信号量

void k_sem_reset(struct k_sem *sem)

void z_impl_k_sem_reset(struct k_sem *sem)
{
	struct k_thread *thread;
	k_spinlock_key_t key = k_spin_lock(&lock);

	/* 将等待队列中所有线程唤醒并设置返回值为 -EAGAIN */
	while (true) {
		thread = z_unpend_first_thread(&sem->wait_q);
		if (thread == NULL) {
			break;
		}
		arch_thread_return_value_set(thread, -EAGAIN);
		z_ready_thread(thread);
	}
	/* 同时需要将信号量的值清0 */
	sem->count = 0;

	SYS_PORT_TRACING_OBJ_FUNC(k_sem, reset, sem);

	/* 在开启POLL功能之后,如果调用 k_poll 时信号量为 0 且需要进入等待,
	 * 调用线程并不会被添加到 wait_q 中,
	 * 而是将调用者所使用的 k_poll_event 对象地址放入 sem 中的双向链表 poll_events 中,
	 * 随后切换上下文并进入休眠,直到等待超时或者收到 K_POLL_STATE_SEM_AVAILABLE 信号后,
	 * 会将该线程唤醒,然后再通过 k_sem_take 获取信号量。
	 * 除此之外如果在 k_poll 等待过程中,有线程调用了 k_sem_reset,
	 * 也需要将那些因调用 k_poll 进入等待的线程唤醒,避免因此导致出错。
	 * handle_poll_events 函数则用于发送 K_POLL_STATE_SEM_AVAILABLE 信号。
	 */
	handle_poll_events(sem);

	z_reschedule(&lock, key);
}
  • k_sem_reset 用于重置信号量的技术值,重置时需要将等待的线程唤醒,唤醒分为以下两种情况:
    • 被挂起的线程调用的是k_sem_take,该线程会被添加到 wait_q 中,唤醒即将所有队列中的线程取出并设置为就绪态。
    • 被挂起的线程使用了poll机制,以等待多个IPC事件到来,在这种情况下调用线程不会添加到 wait_q 中,而是将调用者所使用的 k_poll_event 对象放入 poll_events 队列中,如果需要将等待线程唤醒,则需要向 poll_events 中的 k_poll_event 对象发送信号。

示例

在 Zephyr 中,信号量的实现是基于内核对象 k_sem。以下是一个简单的信号量使用示例:

#include <zephyr.h>
#include <kernel.h>
#include <misc/printk.h>

K_SEM_DEFINE(my_sem, 0, 1); // 定义一个信号量,初始值为0,最大值为1

void taskA(void)
{
    k_sem_take(&my_sem, K_FOREVER); // 等待信号量
    printk("Task A is running\n");
    k_sem_give(&my_sem); // 释放信号量
}

void taskB(void)
{
    printk("Task B is running\n");
    k_sem_give(&my_sem); // 释放信号量
}

void main(void)
{
    k_thread_spawn(taskA_stack, STACK_SIZE, taskA, NULL, NULL, NULL, PRIORITY, 0, K_NO_WAIT);
    k_thread_spawn(taskB_stack, STACK_SIZE, taskB, NULL, NULL, NULL, PRIORITY, 0, K_NO_WAIT);
}

在这个示例中,我们定义了一个信号量 my_sem,并在两个任务 taskA 和 taskB 之间使用它来实现同步。任务 A 需要等待任务 B 完成后才能执行。通过使用信号量,我们可以确保任务 A 和任务 B 按照预期的顺序执行。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

咕咚.萌西

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

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

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

打赏作者

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

抵扣说明:

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

余额充值