并发与竞态:从RTOS临界区到Linux内核同步

📅 2026-09-04
#来源/立芯星球 #类型/专题提炼 #技术/并发 #技术/FreeRTOS #技术/Linux内核

并发与竞态:从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)。

三、更好的思路:能不用锁就不用锁

四、多核与 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)。

五、方法论小结

  1. 单核 RTOS:短共享片段用临界区,任务间长互斥用互斥锁,能改用队列/通知就不自己加锁;
  2. volatile ≠ 原子性;读-改-写必须保护;
  3. 多核之后,竞态从"确定性错误"变成"概率性时序 BUG",保护手段要前置到接口封装层;
  4. Linux 量产级别的问题靠"复现数据 + 分层定位 + 批量压测"闭环,而不是靠灵光一现。

来源笔记