目录
概述
k_sem_give,
k_sem_take和k_sem_init
是 Zephyr RTOS 中信号量(Semaphore)操作的核心函数之一,用于获取信号量,释放信号量资源和初始化信号量。本文主要介绍这些函数的功能和用法。
1. 基本概念
信号量是 Zephyr 中用于线程同步和资源管理的基本机制:
-
计数信号量:跟踪可用资源的数量
-
二进制信号量:计数为1的特殊情况,用作互斥锁
2. Zephyr RTOS中信号量相关函数
2.1 k_sem_give函数
2.1.1 函数介绍
函数原型
void k_sem_give(struct k_sem *sem);
参数:
-
sem
:指向信号量对象的指针
2.2.2 基本使用流程
1) 定义和初始化信号量
#include <zephyr/kernel.h>
struct k_sem my_sem;
void main(void)
{
k_sem_init(&my_sem, initial_count, limit_count);
// initial_count: 初始计数
// limit_count: 最大计数
}
2)典型使用场景
生产者-消费者模式示例:
#define STACK_SIZE 512
#define BUFFER_SIZE 10
K_SEM_DEFINE(data_ready_sem, 0, 1); // 二进制信号量
K_SEM_DEFINE(free_slots_sem, BUFFER_SIZE, BUFFER_SIZE); // 计数信号量
void producer_thread(void)
{
while (1) {
// 等待空闲槽位
k_sem_take(&free_slots_sem, K_FOREVER);
// 生产数据...
produce_data();
// 通知消费者
k_sem_give(&data_ready_sem);
}
}
void consumer_thread(void)
{
while (1) {
// 等待数据就绪
k_sem_take(&data_ready_sem, K_FOREVER);
// 消费数据...
consume_data();
// 释放槽位
k_sem_give(&free_slots_sem);
}
}
2.2.3 重要注意事项
线程安全:
k_sem_give
是线程安全的,可以在任何上下文中调用但在ISR中使用时要注意不能阻塞
信号量溢出:
当信号量计数达到最大值时,再次调用
k_sem_give
不会有任何效果不会导致计数增加超过限制值
与
k_sem_take
配对使用:
通常
give
和take
应该成对出现不匹配可能导致死锁或资源泄漏
性能考虑:
信号量操作是相对轻量级的同步机制
但对于高频操作,考虑使用无锁数据结构
2.2 k_sem_init
函数
函数原型
void k_sem_init(struct k_sem *sem, unsigned int initial_count, unsigned int limit);
参数说明
参数 | 类型 | 描述 |
---|---|---|
sem | struct k_sem* | 指向要初始化的信号量对象的指针 |
initial_count | unsigned int | 信号量的初始计数值(表示初始可用的资源数量) |
limit | unsigned int | 信号量的最大计数值(信号量可以达到的最大值) |
2.2.1 主要功能
k_sem_init
主要完成以下工作:
将信号量的当前计数设置为
initial_count
设置信号量的最大计数限制为
limit
初始化信号量的等待队列(用于存储等待该信号量的线程)
2.2.2 信号量类型应用
1)二进制信号量 (Binary Semaphore)
k_sem_init(&bin_sem, 1, 1); // 初始为1,最大为1
特点:
-
用作互斥锁(类似mutex但无优先级继承)
-
只有可用(1)和不可用(0)两种状态
2)计数信号量 (Counting Semaphore)
#define MAX_RESOURCES 5
k_sem_init(&count_sem, MAX_RESOURCES, MAX_RESOURCES); // 初始5,最大5
特点:
-
跟踪多个可用资源
-
常用于资源池管理
3)同步信号量 (Synchronization Semaphore)
k_sem_init(&sync_sem, 0, 1); // 初始为0,最大为1
特点:
-
初始不可用
-
用于线程间同步(一个线程等待另一个线程的信号)
2.3 k_sem_take函数
2.3.1 函数介绍
k_sem_take
是 Zephyr RTOS 中信号量机制的核心函数之一,用于获取信号量资源。以下是该函数的介绍:
函数原型:
int k_sem_take(struct k_sem *sem, k_timeout_t timeout);
参数说明
参数 | 类型 | 描述 |
---|---|---|
sem | struct k_sem* | 指向要获取的信号量对象的指针 |
timeout | k_timeout_t | 指定等待信号量的超时时间 |
返回值
返回值 | 含义 |
---|---|
0 | 成功获取信号量 |
-EAGAIN | 非阻塞调用时信号量不可用 |
-EINVAL | 参数无效(如信号量指针为NULL) |
-EBUSY | 在无效的上下文调用(如在中断上下文中使用K_FOREVER ) |
2.3.2 使用方法
1. 信号量获取机制
当信号量计数(
count
) > 0时:
立即减少计数
立即返回0(成功)
当信号量计数 = 0时:
阻塞模式:将当前线程放入等待队列,直到:
其他线程调用
k_sem_give
指定的超时时间到期
非阻塞模式:立即返回
-EAGAIN
2. 超时行为控制
Zephyr提供了几种典型的超时设定方式:
// 无限等待(阻塞直到获取)
k_sem_take(&sem, K_FOREVER);
// 不等待(非阻塞尝试)
k_sem_take(&sem, K_NO_WAIT);
// 指定毫秒数等待
k_sem_take(&sem, K_MSEC(250));
// 指定系统时钟节拍等待
k_sem_take(&sem, K_TICKS(100));
3 信号量使用场景示例
3.1 资源保护(二进制信号量)
K_SEM_DEFINE(file_sem, 1, 1); // 二进制信号量
void write_to_file(void)
{
if (k_sem_take(&file_sem, K_MSEC(100)) {
printk("Failed to access file (timeout)\n");
return;
}
// 临界区 - 安全地操作文件
file_write_operation();
k_sem_give(&file_sem); // 释放资源
}
3.2 工作队列控制(计数信号量)
#define MAX_JOBS 5
K_SEM_DEFINE(job_sem, MAX_JOBS, MAX_JOBS);
void worker_thread(void)
{
while (1) {
// 等待可用工作项
k_sem_take(&job_sem, K_FOREVER);
// 处理工作项
process_job_item();
// 注意:这里不需要give,由生产者负责
}
}
void add_job(void)
{
// 生产者添加新工作时
if (k_sem_count_get(&job_sem) > 0) {
enqueue_job();
k_sem_give(&job_sem); // 增加可用工作计数
}
}
3.3 线程同步
K_SEM_DEFINE(data_ready, 0, 1); // 初始不可用
void sensor_thread(void)
{
while (1) {
// 采集传感器数据
read_sensor_data();
// 数据就绪,通知处理线程
k_sem_give(&data_ready);
k_sleep(K_MSEC(100));
}
}
void processing_thread(void)
{
while (1) {
// 等待数据就绪
k_sem_take(&data_ready, K_FOREVER);
// 处理数据
process_data();
}
}
3.4 高级用法
1) 中断上下文使用
// 对于关键资源,建议使用mutex(有优先级继承)
K_MUTEX_DEFINE(important_mutex);
void high_priority_task(void)
{
k_mutex_lock(&important_mutex, K_FOREVER);
// 访问共享资源
k_mutex_unlock(&important_mutex);
}
2)优先级继承考虑
// 对于关键资源,建议使用mutex(有优先级继承)
K_MUTEX_DEFINE(important_mutex);
void high_priority_task(void)
{
k_mutex_lock(&important_mutex, K_FOREVER);
// 访问共享资源
k_mutex_unlock(&important_mutex);
}
3)多信号量等待
// 使用k_poll同时等待多个信号量
struct k_poll_event events[] = {
K_POLL_EVENT_INITIALIZER(K_POLL_TYPE_SEM_AVAILABLE,
K_POLL_MODE_NOTIFY_ONLY,
&sem1),
K_POLL_EVENT_INITIALIZER(K_POLL_TYPE_SEM_AVAILABLE,
K_POLL_MODE_NOTIFY_ONLY,
&sem2)
};
void wait_for_any_sem(void)
{
int rc = k_poll(events, ARRAY_SIZE(events), K_MSEC(200));
if (rc == 0) {
// 检查哪个信号量就绪
for (int i = 0; i < ARRAY_SIZE(events); i++) {
if (events[i].state == K_POLL_STATE_SEM_AVAILABLE) {
k_sem_take(events[i].sem, K_NO_WAIT);
// 处理对应事件
}
}
}
}
4 重要注意事项
4.1 应用注意
死锁风险:
1)避免在持有信号量的情况下再次尝试获取
2)确保所有代码路径最终都会释放信号量
性能影响:
1)长时间持有信号量会降低系统并发性
2)考虑将临界区代码最小化
优先级反转:(解决方案:对关键资源使用mutex(有优先级继承))
// 低优先级线程 k_sem_take(&shared_sem, K_FOREVER); // 获取信号量 // 被中优先级线程抢占... // 高优先级线程 k_sem_take(&shared_sem, K_FOREVER); // 被阻塞
4.2 调试技巧
#ifdef CONFIG_DEBUG
printk("Sem count: %d\n", k_sem_count_get(&debug_sem));
#endif