跳到正文

任务间通信与资源

本文性质声明:本机无开发板,全部内容未实测,为手册级框架。站内对照: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);   // 唤醒了更高优先级?立即切换

三个纪律:

  1. ISR 版 API 不能阻塞:portMAX_DELAY 在中断里是未定义的——中断里等的不是”一个 tick”,是”永远”。
  2. portYIELD_FROM_ISRX 别忘:ISR 里唤醒高优先级任务后,如果让 ISR 自然返回,要等下一个 tick 才切——立即切换的写法就是这个宏,漏了它实时性差一个 tick(本站 emb-rtos-basics 的调度延迟量法在这里用得上)。
  3. 队列满/空的策略要想好:满时覆盖最旧(xQueueOverwrite)、丢最新、还是阻塞等——传感器采集用覆盖(新数据重要),命令通道用阻塞(不能丢命令)。

三、优先级反转:故事、代价与解法

经典剧情(火星探路者号真实事故):

低优先级任务 L 拿着互斥锁 M(操作共享总线)
高优先级任务 H 也要 M → 阻塞等 L
中优先级任务 C(不碰 M)就绪 → 抢占 L
→ L 跑不了,M 不释放,H 被 C 无限期堵着
→ H 错过它的截止期,看门狗重启系统

根因:优先级和锁是两套独立机制,中间插进来的中优先级任务”不知情”地破坏了高优先级的等待链。

解法两级:

  1. 优先级继承:L 持锁等待期间,临时把 L 的优先级提到 H 的水平——C 抢不动 L,L 赶紧跑完释放锁,H 立刻拿到。互斥锁(不是二值信号量)才支持这个(xSemaphoreCreateMutex 建的就是带继承的互斥锁)。
  2. 优先级天花板:创建锁时指定”天花板优先级”,任何持锁者立即升到天花板——比继承更快(无临时提权开销),但可能过度阻塞中优先级任务。

工程纪律:临界区短到”中优先级任务插不进来”是终极解——本站 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——该机不可达,未涉及。