任务间通信与资源
本文性质声明:本机无开发板,全部内容未实测,为手册级框架。站内对照:conc-condvar(谓词循环与通知)、conc-mutex(锁单价与数据竞争)、conc-task-queue(任务队列与线程池)、linux-pthread(pthread 侧同族机制)。
一、四件套的选型
RTOS 的原语就四个,选型按”要表达什么”对号入座:
| 原语 | 语义 | 用途 |
|---|---|---|
| 队列 | FIFO 传数据(值拷贝) | 任务间传消息、ISR 给任务喂数据、缓冲解耦 |
| 二值信号量 | 0/1 标志 | 中断给任务发通知(“事件发生了”)、任务间同步 |
| 计数信号量 | 0..N 计数 | 表示资源数量(N 个缓冲区、N 个通道) |
| 互斥锁 | 带归属的锁 | 保护共享资源;特有优先级继承(第三节) |
| 事件组 | 多位标志 | 任务等”多个条件的组合”(A 且 B、A 或 B) |
两个高频误用:用二值信号量保护资源(它没有归属,谁都能 give——保护不了互斥,这是互斥锁存在的全部理由);用队列传大数据(队列拷贝的是值——4 字节指针可以,1KB 结构体就是性能灾难;传大数据用”指针队列 + 独立内存池”)。
二、队列:从 ISR 安全的那一套 API 说起
队列是四件套里唯一”传数据”的,也是唯一有两套 API 的(任务版 vs 中断版):
xQueueSend(q, &data, portMAX_DELAY); // 任务里:可阻塞
xQueueSendFromISR(q, &data, &hp_task); // 中断里:不阻塞 + 可能要触发切换
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xQueueSendFromISR(q, &data, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken); // 唤醒了更高优先级?立即切换
三个纪律:
- ISR 版 API 不能阻塞:
portMAX_DELAY在中断里是未定义的——中断里等的不是”一个 tick”,是”永远”。 portYIELD_FROM_ISRX别忘:ISR 里唤醒高优先级任务后,如果让 ISR 自然返回,要等下一个 tick 才切——立即切换的写法就是这个宏,漏了它实时性差一个 tick(本站 emb-rtos-basics 的调度延迟量法在这里用得上)。- 队列满/空的策略要想好:满时覆盖最旧(
xQueueOverwrite)、丢最新、还是阻塞等——传感器采集用覆盖(新数据重要),命令通道用阻塞(不能丢命令)。
三、优先级反转:故事、代价与解法
经典剧情(火星探路者号真实事故):
低优先级任务 L 拿着互斥锁 M(操作共享总线)
高优先级任务 H 也要 M → 阻塞等 L
中优先级任务 C(不碰 M)就绪 → 抢占 L
→ L 跑不了,M 不释放,H 被 C 无限期堵着
→ H 错过它的截止期,看门狗重启系统
根因:优先级和锁是两套独立机制,中间插进来的中优先级任务”不知情”地破坏了高优先级的等待链。
解法两级:
- 优先级继承:L 持锁等待期间,临时把 L 的优先级提到 H 的水平——C 抢不动 L,L 赶紧跑完释放锁,H 立刻拿到。互斥锁(不是二值信号量)才支持这个(
xSemaphoreCreateMutex建的就是带继承的互斥锁)。 - 优先级天花板:创建锁时指定”天花板优先级”,任何持锁者立即升到天花板——比继承更快(无临时提权开销),但可能过度阻塞中优先级任务。
工程纪律:临界区短到”中优先级任务插不进来”是终极解——本站 conc-mutex 实测过:锁的 uncontended 单价 16.65ns 级,把临界区压到几十行指令,反转窗口在工程上不存在。继承是保险,短临界区是疫苗。
四、内存池与静态分配
malloc/free 在实时系统里三个问题:时间不确定(碎片整理可能慢)、碎片化(长期运行后分配失败)、不可重入(多任务并发分配要锁)。RTOS 的标准答案是内存池:
// 固定大小块的池:分配/释放都是 O(1) 链表操作,无碎片
NetBuf *p = pvPortGetBlock(pool); // 从空闲链表摘一个
vPortPutBlock(pool, p); // 还回链表
三种池的组织:固定大小块(最高效,UDP 缓冲、消息缓冲的标配)、分区池(几个大小档);栈式分配器(ISR 与任务间的”一次性内存”,任务结束自动回收——适合请求-响应周期)。
静态分配一切:任务(xTaskCreateStatic)、队列(xQueueCreateStatic)、信号量(xSemaphoreCreateBinaryStatic)——编译期定 RAM,启动即知够不够,运行时绝不失败。代价是灵活性(不能动态创建任务)——嵌入式系统绝大多数场景这个交换划得来(本站 c-restricted-environment 的”无堆策略”是同一思想的应用层)。
五、中断与任务的边界纪律
一条总规则:ISR 做最少的事(清标志、拷数据、发通知),其余全在任务里做。展开成清单:
| ISR 里做 | ISR 里不做 |
|---|---|
| 读硬件状态、清中断标志 | 浮点运算(无 FPU 上下文时要保存,代价大) |
| DMA 描述符推进 | printf(慢且可能不可重入) |
| 队列/信号量的 FromISR 操作 | 拿互斥锁(会阻塞 = 死) |
置 volatile 标志 | 动态内存分配 |
共享数据的三种保护按场景选:单字变量(volatile + 原子读写,32 位机上 32 位访问是单指令);ISR 写任务读的标志(关中断/关调度包住复合操作);真正的共享结构体(互斥锁 + 短临界区)。“大部分时间没人抢”的变量用关中断保护,别上锁——本站 conc-mutex 的粒度原则原样适用。
六、排障索引
| 症状 | 入口 |
|---|---|
| 高优先级任务偶发错过截止期 | 优先级反转(第三节);示波器量调度延迟定位窗口 |
| 队列只丢不收/只收不发 | FromISR 的指针参数没初始化(第二节的 &xHigherPriorityTaskWoken) |
| 运行几天后分配失败 | 用了动态内存;改内存池 + 静态分配(第四节) |
| 中断里调 API 后系统崩 | 用了任务版 API(会阻塞);查 configASSERT |
| 偶发错乱但加锁没用 | 锁错了对象(信号量当互斥用);或复合操作没全包住 |
六、未实测清单
- 本文所有 RTOS 行为与成本——无开发板,未测。
- 优先级继承的实际效果——未测(本站 conc-sync 量过 Windows 侧自旋/互斥的相对成本做参照)。
- 内存池的碎片行为——未测。
- 用户 Linux 开发机上的项目 RTOS——该机不可达,未涉及。