同步设计先回答“谁能修改这份数据”和“何时转交”。任务数量增加后,靠几个 volatile 标志维持状态容易出现丢事件、重复处理和内存可见性问题。选一个原语,还需要明确超时、失败和生命周期。
按语义选择原语
| 需要表达的关系 | 常见选择 | 要检查的边界 |
|---|---|---|
| 把数据从生产者交给消费者 | 队列、流缓冲、消息缓冲 | 复制还是传指针,满了怎么办 |
| 一位任务等待事件或计数 | 任务通知、信号量 | 位覆盖、计数溢出、事件是否可合并 |
| 多位任务保护共享资源 | 互斥锁 | 锁顺序、持有时间、超时、优先级继承 |
| 多个状态条件组合 | 事件组等 | 位是状态还是次数,清除时机 |
| 很短的不可中断更新 | 合适的临界区或原子操作 | ISR、单核/多核、屏蔽范围和内存顺序 |
FreeRTOS 队列复制固定大小的元素。如果元素是一根指针,复制的是地址,不会复制所指缓冲区。生产者在消费者读完前重用那块内存,会使队列里看似不同的记录全部指向同一份最新数据。
优先级反转怎样发生
低优先级任务 L 持有互斥锁,高优先级任务 H 等锁,中优先级任务 M 不需要锁却持续抢占 L。H 会被间接拖延。优先级继承让锁持有者临时获得等待者的较高优先级,尽快释放资源。
下载 优先级反转模型,运行 python mutex.py。模型中 L 需要三个执行单位,M 需要六个,H 在 t=1 请求锁。无继承时 H 到 t=9 才开始;有继承时 t=3 即可开始。该结果只针对脚本约定,不是 FreeRTOS 各种嵌套锁行为的完整模拟。
继承并不能解决死锁,也不会使持锁网络请求突然变快。避免锁内进行不受控 I/O;必要时把资源集中给一位所有者任务,由请求队列串行处理。多个锁要统一获取顺序,失败路径也要释放已经获得的资源。
队列容量必须有理由
假设生产速率为 100 条/秒,消费者最多连续停顿 80 ms,在停顿开始时队列为空且没有额外突发,至少需要容纳约八条。边界时刻、已有积压、调度抖动和 DMA 批量提交还需增加余量。若长期平均消费速率低于生产速率,任何有限队列最终都会满。
队列满时可以丢最新、丢最旧、阻塞生产者或进入故障状态,但应由业务决定并记录丢弃计数。闭环控制通常更关心新鲜度,审计记录可能要求完整性;两者不该共用未经说明的策略。
固定池与静态分配便于界定内存峰值。动态分配需检查失败、碎片和生命周期。任务栈高水位是有用的观测值,但某次路径没执行并不代表那条路径的栈需求不存在。
从 ISR 交接数据
使用端口支持的 FromISR 接口,不在 ISR 等待普通互斥锁。若唤醒更高优先级任务,按端口规定请求切换;同时满足中断优先级限制。多核系统里的本地关中断也无法阻止另一个核心修改同一对象,迁移到 ESP-IDF SMP 时要重新检查锁与临界区。
练习:给含指针的队列画出四个时刻的缓冲区所有者;增加取消和超时路径,证明每块缓冲区最终恰好归还一次。再把模型改成 L 持锁期间主动睡眠,观察优先级继承为何不能消除等待。
面试自查:互斥锁与二值信号量的语义差异是什么?队列元素复制能否保证对象线程安全?为什么调大队列会让延迟恶化?优先级继承为何不能替代锁顺序?