3. 任务间通信:队列、信号量与互斥量

任务间通信,说白了就是让多个任务能“说上话”。

我刚开始做嵌入式开发时,总觉得任务之间各干各的就行。直到有一次,一个传感器采集任务和一个显示任务因为共享变量没处理好,系统跑着跑着就崩了。嗯,从那以后我才真正重视起这个问题。

RTOS 里最常用的三种通信机制就是:队列(Queue)、信号量(Semaphore)和互斥量(Mutex)。它们各有各的脾气,用对了地方,系统稳如泰山;用错了,调试起来能让你怀疑人生。

3.1 队列(Queue)—— 任务间的“邮局”

队列的本质是一个先进先出(FIFO)的缓冲区。一个任务往里放数据,另一个任务从里面取数据。数据是拷贝进去的,不是传指针,这就避免了共享资源竞争的问题。

核心特点:

  • 数据传递:发送方拷贝数据到队列,接收方从队列拷贝数据出来
  • 同步机制:队列空时,接收任务可以阻塞等待;队列满时,发送任务也可以阻塞等待
  • 多对多:多个任务可以往同一个队列发,也可以从同一个队列收

我在项目中遇到过这样一个场景:一个 UART 接收中断服务程序(ISR)把收到的字节放进队列,然后一个处理任务从队列里取出来做协议解析。这样 ISR 只做最轻量的事,剩下的交给任务去处理,系统响应非常快。

队列的典型使用场景

  • 数据流处理:比如传感器数据采集、网络数据包收发
  • 任务解耦:生产者和消费者模式,双方不需要知道对方的存在
  • 中断与任务通信:ISR 中不能阻塞,但可以往队列里放数据

代码示例:创建和使用队列

// 定义队列句柄
QueueHandle_t xDataQueue;

// 创建队列,每个元素 4 字节(一个 int),队列深度 10
xDataQueue = xQueueCreate(10, sizeof(int));

// 发送任务:往队列里放数据
int data = 100;
xQueueSend(xDataQueue, &data, portMAX_DELAY);

// 接收任务:从队列里取数据
int received;
if (xQueueReceive(xDataQueue, &received, pdMS_TO_TICKS(100)) == pdPASS) {
    // 成功收到数据
    printf("Received: %d\n", received);
} else {
    // 超时,没收到数据
    printf("Timeout\n");
}

我的经验:队列深度一定要根据实际数据峰值来估算。我曾经因为队列设得太浅,导致高频数据丢失,查了两天才找到原因。建议留出 20%-30% 的余量。

3.2 信号量(Semaphore)—— 资源的“计数器”

信号量本质上是一个非负整数计数器。它用来管理有限数量的资源访问权。

你想想看,一个停车场有 10 个车位。每进来一辆车,计数器减 1;每出去一辆车,计数器加 1。当计数器为 0 时,后来的车就得等着。这就是信号量的工作原理。

两种信号量

类型 初始值 典型用途
二值信号量 0 或 1 任务同步、中断与任务同步
计数信号量 N(N > 1) 管理多个相同资源(如内存池、缓冲区)

信号量的典型使用场景

  • 任务同步:任务 A 做完某件事后,通过信号量通知任务 B 继续执行
  • 中断同步:ISR 中释放信号量,任务中获取信号量,实现“中断下半部”处理
  • 资源计数:管理有限数量的硬件资源,比如 DMA 通道、硬件定时器

注意:信号量不适合保护共享资源!因为它没有“优先级继承”机制,容易导致优先级反转问题。保护共享资源请用互斥量。

3.3 互斥量(Mutex)—— 资源的“钥匙”

互斥量是二值信号量的“升级版”。它专门用来保护共享资源,防止多个任务同时访问。

为什么不用二值信号量?因为互斥量多了一个关键特性:优先级继承。

优先级反转问题

假设有三个任务:高优先级任务 H、中优先级任务 M、低优先级任务 L。L 先拿到了互斥量,然后 H 来了想拿,但拿不到,只能阻塞等待。这时候 M 开始运行,它不需要互斥量,但它抢占了 L 的 CPU 时间,导致 L 无法释放互斥量。结果就是:H 被 M 间接阻塞了,优先级反转发生了。

互斥量的优先级继承机制能解决这个问题:当 H 被 L 阻塞时,系统会临时把 L 的优先级提升到 H 的级别,这样 M 就无法抢占 L,L 能尽快释放互斥量。

互斥量的核心特性:

  • 优先级继承:防止优先级反转
  • 递归锁定:同一个任务可以多次获取同一个互斥量(需要相同次数的释放)
  • 只能用于任务上下文,不能在 ISR 中使用

互斥量的典型使用场景

  • 保护全局变量:多个任务读写同一个数据结构
  • 保护硬件外设:比如 SPI、I2C 总线,同一时刻只能一个任务使用
  • 保护内存分配:多个任务共享同一个内存池

代码示例:使用互斥量保护共享资源

// 定义互斥量句柄
SemaphoreHandle_t xMutex;

// 创建互斥量
xMutex = xSemaphoreCreateMutex();

// 任务中访问共享资源
void vTaskFunction(void *pvParameters) {
    for (;;) {
        // 获取互斥量
        if (xSemaphoreTake(xMutex, portMAX_DELAY) == pdPASS) {
            // 访问共享资源
            shared_variable++;
            printf("Shared: %d\n", shared_variable);
            
            // 释放互斥量
            xSemaphoreGive(xMutex);
        }
    }
}

我的习惯:获取互斥量后,尽量在最短时间内释放。不要在持有互斥量时调用可能阻塞的函数,比如延时、等待队列等。否则容易造成死锁。

3.4 三者对比与选择

特性 队列 信号量 互斥量
主要用途 数据传递 资源计数/同步 保护共享资源
数据传递 支持(拷贝数据) 不支持 不支持
优先级继承 无 无 有
ISR 中使用 可以(发送) 可以(释放) 不可以
递归锁定 无 无 有
典型场景 生产者-消费者 任务同步 临界区保护

选择其实很简单:

  • 要传数据?用队列。
  • 要同步任务或计数资源?用信号量。
  • 要保护共享资源?用互斥量。

我曾经踩过的坑:在 ISR 里用了互斥量,结果系统直接死机。互斥量只能在任务上下文中使用,ISR 里只能用信号量或队列的发送接口。这个错误很隐蔽,调试时一定要留意上下文环境。

3.5 知识体系图

下面这张图展示了三种通信机制在 RTOS 中的位置和关系:

任务间通信机制关系图 RTOS 任务间通信机制 队列(Queue) 信号量(Semaphore) 互斥量(Mutex) 任务 A 任务 B 任务 C 各机制核心特性 • 队列:FIFO 数据传递,支持多对多,ISR 可发送 • 信号量:资源计数,任务同步,ISR 可释放 • 互斥量:保护共享资源,优先级继承,ISR 不可用 • 选择原则:传数据用队列,同步用信号量,保护资源用互斥量 图:RTOS 任务间通信机制关系与选择指南

这张图把三种机制放在一起对比。你可以看到,任务 A、B、C 通过不同的通信机制交互。队列负责数据流动,信号量负责同步信号,互斥量负责资源保护。各司其职,互不干扰。

我的建议:刚开始学 RTOS 时,先熟练掌握队列和信号量。互斥量虽然重要,但用不好容易引入死锁。等你对任务调度有了更深的理解,再深入使用互斥量。

好了,这一章的内容就到这里。三种通信机制的原理和用法都讲清楚了,剩下的就是在实际项目中多练多用。记住,没有最好的机制,只有最合适的场景。