并发与竞态:从RTOS临界区到Linux内核同步
只要存在两个以上执行流(任务、中断、多核),共享数据就可能被"写到一半被打断"。本文纵向串起星球里的并发话题:RTOS 单核下的临界区与锁 → 任务间通信替代方案 → 多核竞态 → Linux 内核的时序类 BUG。
一、临界区 vs 互斥锁:什么时候用哪个
星球里被反复问起的一对概念(见 老师们,想不通,临界区和互斥锁是一样的效果么?什么时候用临界区 什么时候用互斥锁,Jack 答):
- 临界区:
taskENTER_CRITICAL()/taskEXIT_CRITICAL()靠设置 BASEPRI 屏蔽可管理中断实现,保护的是"任务 vs 中断/其他任务"的极短共享片段,里面绝不能阻塞。它没有"归属者"概念,进出即可。 - 互斥锁:带优先级继承的二值信号量,谁拿谁放,可以长时间持有(相对而言),用于任务与任务之间互斥。见 FreeRTOS中互斥锁和信号(Jack)。
细节坑:uxCriticalNesting 是嵌套计数的,进出必须配对;对临界区嵌套行为的疑问见 taskENTER_CRITICA里会将uxCriticalNesting++(Jack)。临界区期间能否响应硬件中断,取决于被屏蔽的优先级范围(BASEPRI 只屏蔽数值更大——即逻辑优先级更低的部分),见 请问,进入临界区期间,能触发硬(Jack)。
还有一个典型场景:同一个共享变量在主循环和中断里都会被改,uint8_t 出现修改丢失——单核上这类"读-改-写"三步被打断就是竞态,需要临界区保护、volatile 只解决编译器缓存问题不解决原子性。见 你需要把这个变量做一个访问的接(mx 提问)。结论被 Jack 强化为:共享内存操作如果有相同接口,一定要加临界区,见 共享内存的操作,如果有相同接口,一定要一定要加临界区(Jack)。
二、防优先级翻转:锁的进阶语义
互斥锁 = 带优先级继承的二值信号量:高优先级任务阻塞在锁上时,持锁低优先级任务被临时抬到同优先级,让中间优先级任务无法"插队"延长阻塞时间。Jack 用 A/B/C 三线程 + 互斥锁的例子逐帧推演了这个过程(含时间片关闭时的行为),见 关于抢占调度模式(时间片调度不、FreeRTOS中互斥锁和信号、老师: 使用互斥信号量,低优先(均 Jack)。
三、更好的思路:能不用锁就不用锁
- 生产者-消费者:两个线程一生产一消费,单缓冲可以考虑不加锁的无锁写法(核内、严格一读一写、一个变量做索引),但工程上更推荐直接用队列/环形缓冲区让 RTOS 原语兜底。见 两个线程,一个生产,一个消费,能够一个缓冲区,不加锁解决吗:(Jack)、环形缓冲区和队列,在生产者和消费者线程中,如何应用(Jack)。
- 广播事件:队列是一对一的,一发多收要遍历多个队列或改事件组——选对 IPC 形态,很多"锁问题"其实是"通信模型问题"。见 如何实现消息队列的广播,一发多收,并且能保证每-个task都能收到完整消息:比如bu(Jack)。
- 低功耗场景的特殊竞态:"判断可以睡眠"到
__WFI()之间存在窗口,事件可能恰好插入——应复用 port 层已验证的 Tickless 流程而不是自己写简化模板。见 做嵌入式开发,低功耗设计几乎绕不开;带RTOS 的项目尤其如此(刘子奇)。
四、多核与 Linux:竞态变成"概率性时序 BUG"
多核程序开发更多考虑竞态:核间很可能乱序执行各线程,调用链没有完全耦合时就会出偶现问题,所以对数据接口 IO 的封装必须用手段保护起来。见 多核程序开发,考虑更多的是竞态,因为很可能会乱序执行各个线程(Jack)。
到了 Linux 驱动层,Jason 描述了这类问题的工程形态(见 Linux BSP问题闭环流程):
- UT 自测测不出,要靠产线批量压测(如 100 台压 48h 出 2 例);
- 内存屏障
mb()、多核并发属于最 corner 的时序问题——单看代码逻辑永远"没有错",是多个线程在严格时序下叠加才犯错;随机踩内存是另一大类; - 闭环流程必须是:稳定复现(必要时设计劣化实验)→ 分层定位 → 本地验证 → 产线压测 1-2 周 → 规范 commit 合入。
一个真实案例:蓝牙模块重启,表象是串口 DMA 发送线程得不到调度,真凶是网络软中断负载占满 cpu0 的 kworker/ksoftirqd 且关闭了抢占——"你看到的问题不是你的问题,你只是受害者"。定位靠 perfetto trace + HWT coredump backtrace 交叉。见 分享一个近期遇到的 Linux 问题,涉及多个模块(Jason)。
五、方法论小结
- 单核 RTOS:短共享片段用临界区,任务间长互斥用互斥锁,能改用队列/通知就不自己加锁;
volatile≠ 原子性;读-改-写必须保护;- 多核之后,竞态从"确定性错误"变成"概率性时序 BUG",保护手段要前置到接口封装层;
- Linux 量产级别的问题靠"复现数据 + 分层定位 + 批量压测"闭环,而不是靠灵光一现。
来源笔记
- 老师们,想不通,临界区和互斥锁是一样的效果么?什么时候用临界区 什么时候用互斥锁 — Jack
- taskENTER_CRITICA里会将uxCriticalNesting++ — Jack
- 请问,进入临界区期间,能触发硬 — Jack
- 你需要把这个变量做一个访问的接 — mx
- 共享内存的操作,如果有相同接口,一定要一定要加临界区 — Jack
- FreeRTOS中互斥锁和信号 — Jack
- 关于抢占调度模式(时间片调度不 — Jack
- 老师: 使用互斥信号量,低优先 — Jack
- 两个线程,一个生产,一个消费,能够一个缓冲区,不加锁解决吗: — Jack
- 环形缓冲区和队列,在生产者和消费者线程中,如何应用 — Jack
- 如何实现消息队列的广播,一发多收,并且能保证每-个task都能收到完整消息:比如bu — Jack
- 做嵌入式开发,低功耗设计几乎绕不开;带RTOS 的项目尤其如此 — 刘子奇
- 多核程序开发,考虑更多的是竞态,因为很可能会乱序执行各个线程 — Jack
- Linux BSP问题闭环流程 — Jason
- 分享一个近期遇到的 Linux 问题,涉及多个模块 — Jason