6.2 队列、信号量与互斥量


文档摘要

6.2 队列、信号量与互斥量 上一节的任务各跑各的还很和谐,可一旦中断收到的数据要交给任务、两个任务要用同一个串口、一个任务的结果是另一个任务的原料——交接问题就来了。FreeRTOS 把交接拆成三种原语:队列传数据、信号量传事件、互斥量保资源。分清"传的是数据还是事件、护的是资源还是顺序",并发 bug 就少了八成。 队列:任务之间的传送带 队列是定长、定深度的先进先出缓冲,自带两件法宝:拷贝语义(进去的数据被整块复制,收发双方不共享指针,天然没有"谁先改"的纠纷)与阻塞语义(队列空时接收方睡觉,不占 CPU;队列满时发送方睡觉或返回失败)。这是它能替代裸机时代所有"标志位 + 全局数组"方案的根本原因。 经典应用:串口中断收字节、任务解析报文。

6.2 队列、信号量与互斥量

上一节的任务各跑各的还很和谐,可一旦中断收到的数据要交给任务、两个任务要用同一个串口、一个任务的结果是另一个任务的原料——交接问题就来了。FreeRTOS 把交接拆成三种原语:队列传数据、信号量传事件、互斥量保资源。分清"传的是数据还是事件、护的是资源还是顺序",并发 bug 就少了八成。

队列:任务之间的传送带

队列是定长、定深度的先进先出缓冲,自带两件法宝:拷贝语义(进去的数据被整块复制,收发双方不共享指针,天然没有"谁先改"的纠纷)与阻塞语义(队列空时接收方睡觉,不占 CPU;队列满时发送方睡觉或返回失败)。这是它能替代裸机时代所有"标志位 + 全局数组"方案的根本原因。

经典应用:串口中断收字节、任务解析报文。中断里一行代码完成交接:

#include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/queue.h" static QueueHandle_t uart_rx_queue; typedef struct { uint8_t data[32]; size_t len; } uart_msg_t; void uart_rx_isr(void *arg) /* ESP32 的中断处理注册形式 */ { uart_msg_t msg; msg.len = uart_read_bytes_now(&msg.data, sizeof(msg.data)); /* 快速取走数据 */ BaseType_t woken = pdFALSE; /* ISR 里必须用 FromISR 后缀版本:普通版可能阻塞,在中断里阻塞是灾难 */ xQueueSendFromISR(uart_rx_queue, &msg, &woken); portYIELD_FROM_ISR(woken); /* 若唤醒了高优先级任务,退出后立刻切换 */ } void parser_task(void *arg) { uart_msg_t msg; for (;;) { /* 队列空则在此阻塞睡去,一个时钟节拍都不浪费 */ if (xQueueReceive(uart_rx_queue, &msg, portMAX_DELAY) == pdTRUE) { parse_frame(msg.data, msg.len); /* 解析的慢活放到任务里做 */ } } } void app_main(void) { uart_rx_queue = xQueueCreate(10, sizeof(uart_msg_t)); /* 深度 10:扛突发 */ /* 安装串口中断、创建 parser_task …… */ }

三个工程细节决定这套结构的成败。ISR 专用 API 不是建议是强制:普通 API 的阻塞逻辑出现在中断上下文会直接挂掉系统,所有 FreeRTOS 接口都有 FromISR 对应版。队列深度按突发量估:115200 波特的字节流如果逐字节入队,深度 10 撑不过 0.9 毫秒,正确做法是中断攒一帧再入队(本例的 msg 结构就是为此);深度给足还能吸收解析任务的偶发迟到。拷贝语义决定了别传大块头:几 KB 的图像帧进队列会拖慢拷贝,这时传指针(配合静态分配的缓冲池)才是正解。

图 6-2:中断与任务经队列交接的完整链路

图 6-2:中断与任务经队列交接的完整链路

信号量:传事件,不传数据

信号量内部只有一个计数,专门表达"某事发生了"。二值信号量最常见于"中断通知任务":中断给信号量,任务等到信号量去干活——与队列的区别在于它不带数据(数据自己走全局缓冲或队列)。计数信号量则用计数表达资源余量(比如缓冲池里还剩几块)。

static SemaphoreHandle_t adc_done_sem; static volatile uint16_t adc_result; /* 数据走缓冲,事件走信号量 */ void adc_isr(void) { adc_result = ADC_READ(); xSemaphoreGiveFromISR(adc_done_sem, &woken); /* 通知:转换完成了 */ } void filter_task(void *arg) { for (;;) { xSemaphoreTake(adc_done_sem, portMAX_DELAY); /* 等事件,到了再干 */ int32_t f = iir_filter(adc_result); /* 慢活在任务里 */ printf("filtered=%ld\n", (long)f); } }

互斥量:给共享资源上锁

两个任务抢同一个外设(都往串口打印、都刷同一块屏),输出会交错成乱码。互斥量(mutex)就是那把锁:谁拿到谁独占,用完释放,另一个才进去。它比"先来后到的轮询标志"多一层制度保障,也最容易与信号量混淆——信号量用于"通知发生了",互斥量用于"保护使用权",二者不可互换:

原语 语义 典型场景 能否 ISR 使用
队列 传数据 中断交字节给任务 FromISR 版
二值信号量 传事件 中断通知任务 FromISR 版
计数信号量 资源余量 缓冲池配额 FromISR 版
互斥量 资源独占 多任务共用串口/屏幕 禁止
static SemaphoreHandle_t uart_mutex; void log_task(void *arg) /* 多个任务都想打印时,锁住整条消息再发 */ { for (;;) { char line[96]; wait_for_log(line); if (xSemaphoreTake(uart_mutex, pdMS_TO_TICKS(100)) == pdTRUE) { uart_puts(line); xSemaphoreGive(uart_mutex); } } }

互斥量存在的理由不只是加锁,更是它比普通信号量多出的两件武器。其一,优先级继承:低优先级任务持锁时,若高优先级任务来等锁,前者临时被抬到后者的优先级——这直接拆解了著名的"优先级反转"死局(高优先级被低优先级间接阻塞,而中优先级任务趁虚狂奔;火星探路者号当年就栽在这上面)。其二,递归持有持有者校验:谁锁谁解、重复锁不死锁在自家门口。代价是互斥量禁止在中断里使用——中断没有"任务身份",继承机制无从谈起。

最后补一个选型口诀,遇到交接需求时按序自问:要传数据吗? 传 → 队列;只通知事件吗? 是 → 信号量;要独占资源吗? 是 → 互斥量。三连问下来选错工具的概率趋近于零。剩下的工程变量(队列深度、等待超时值)都有经验起点:队列深度按突发速率乘最大处理延迟放大两倍,超时值宁可选"有限超时 + 出错日志"而不是无限等待——无限等待在嵌入式里几乎总是错的,它让故障从"可观测的超时"退化成"无声的挂死"。

锁的纪律:少锁、快出、顺序一致

锁是把双刃剑,三条纪律保平安。临界区尽量短:锁内只做读写共享资源的必要动作,printf、计算、再取锁统统挪出去——持锁时间就是其他任务的等待时间。多把锁按固定顺序取:任务甲先锁 A 再锁 B、任务乙先锁 B 再锁 A,就构成经典的死锁双环;全项目约定"永远按地址排序取锁"即可免疫。能不用锁就不用:单字节数据用原子访问,生产者消费者用队列,事件同步用信号量——互斥量是并发工具链的最后一道防线,不是第一反应。

到这里,FreeRTOS 的三件套齐了:任务与调度立骨架,队列信号量互斥量通血脉。最后一章回到产品视角:程序怎么从上电跑起来(启动流程)、怎么装进芯片(烧录调试)、以及量产后怎么在用户手里完成升级(OTA)——从裸机点灯到可靠产品,还剩这最后一段路。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U