4. 线程同步之信号量
任务目标
本案例在 Zephyr RTOS 中使用信号量实现两个核心功能:任务同步(一个任务等待另一个任务完成某动作)与资源计数(有限数量资源的分配与释放)。通过经典的生产者-消费者问题来验证信号量的正确性:生产者任务生成数据放入共享缓冲区,消费者任务取出数据,两者通过信号量协调,确保消费者不会在缓冲区为空时取数据,生产者不会在缓冲区满时放数据。
环境与依赖
- Zephyr RTOS 3.x 及以上版本
- 开发板:任意支持 Zephyr 的板卡(本文以
qemu_x86为例) - 所需头文件:
zephyr/kernel.h、zephyr/sys/printk.h
核心实现
使用两个信号量:empty_sem 表示缓冲区空位数量(初始为缓冲区大小),full_sem 表示缓冲区中已填充的数据数量(初始为0)。生产者每次放入数据前获取 empty_sem,放入后释放 full_sem;消费者对称操作。这样信号量天然实现了同步与互斥,无需额外锁保护缓冲区索引(单生产者单消费者场景下)。
#include <zephyr/kernel.h>
#include <zephyr/sys/printk.h>
#define BUF_SIZE 4
/* 共享缓冲区 */
static int buffer[BUF_SIZE];
static int write_idx = 0;
static int read_idx = 0;
/* 信号量:empty 计数空位,full 计数数据项 */
K_SEM_DEFINE(empty_sem, BUF_SIZE, BUF_SIZE);
K_SEM_DEFINE(full_sem, 0, BUF_SIZE);
/* 生产者线程 */
void producer_thread(void *arg1, void *arg2, void *arg3)
{
int item = 0;
ARG_UNUSED(arg1);
ARG_UNUSED(arg2);
ARG_UNUSED(arg3);
while (1) {
/* 等待空位(若缓冲区满则阻塞) */
k_sem_take(&empty_sem, K_FOREVER);
/* 放入数据 */
buffer[write_idx] = item;
write_idx = (write_idx + 1) % BUF_SIZE;
printk("Producer: put item %d\n", item);
item++;
/* 通知消费者有新数据 */
k_sem_give(&full_sem);
k_sleep(K_MSEC(500)); /* 模拟生产耗时 */
}
}
/* 消费者线程 */
void consumer_thread(void *arg1, void *arg2, void *arg3)
{
int item;
ARG_UNUSED(arg1);
ARG_UNUSED(arg2);
ARG_UNUSED(arg3);
while (1) {
/* 等待数据(若缓冲区空则阻塞) */
k_sem_take(&full_sem, K_FOREVER);
/* 取出数据 */
item = buffer[read_idx];
read_idx = (read_idx + 1) % BUF_SIZE;
printk("Consumer: got item %d\n", item);
/* 释放空位 */
k_sem_give(&empty_sem);
k_sleep(K_MSEC(1000)); /* 模拟消费耗时 */
}
}
/* 定义并启动线程 */
K_THREAD_DEFINE(producer_tid, 1024, producer_thread, NULL, NULL, NULL,
5, 0, 0);
K_THREAD_DEFINE(consumer_tid, 1024, consumer_thread, NULL, NULL, NULL,
5, 0, 0);
void main(void)
{
printk("Semaphore producer-consumer demo started\n");
/* 线程已由 K_THREAD_DEFINE 自动启动,main 仅作提示 */
}
构建与运行
- 将上述代码保存为
main.c,放入一个 Zephyr 应用工程的src目录(工程需包含prj.conf,本案例无需额外配置)。 - 在应用根目录执行:
west build -b qemu_x86 .
west build -t run
验证方法
观察串口输出,应满足以下条件即判定通过:
- 输出中出现
Producer: put item N与Consumer: got item N,且同一编号N的 put 必然先于 got 出现(消费者不会取到未生产的数据)。 - 由于消费者休眠时间(1秒)是生产者(0.5秒)的两倍,缓冲区会逐渐填满。当缓冲区满时,生产者会阻塞等待空位,表现为输出中连续出现多个
Consumer: got后才有下一个Producer: put,证明信号量正确限制了生产者写入速度。 - 程序持续运行不崩溃、不死锁,编号
N单调递增无重复或跳号。
预期输出片段(前几行,实际输出因调度时序略有差异):
Semaphore producer-consumer demo started
Producer: put item 0
Consumer: got item 0
Producer: put item 1
Producer: put item 2
Consumer: got item 1
...
关键说明
- 信号量
empty_sem初始计数为BUF_SIZE(4),最大也为4,表示缓冲区初始有4个空位;full_sem初始为0,表示无数据。 - 本例为单生产者单消费者,因此无需额外互斥锁保护索引变量。若扩展为多生产者/多消费者,需增加互斥量保护
write_idx/read_idx的更新。 - 线程栈大小1024字节足够本案例使用;优先级5为协作式优先级,两个线程同优先级时按时间片轮转。