1. 同步与互斥基础:为什么需要同步?竞态条件与临界区概念
各位同学,咱们今天聊聊RTOS里最基础、也最容易踩坑的话题——同步与互斥。
说实话,我刚开始做嵌入式那会儿,觉得这玩意儿不就是加个锁嘛,有啥好讲的?直到有一次,我在一个工业控制项目里,因为没处理好两个任务同时访问一个全局变量,导致设备在高温环境下随机死机。那段时间我几乎把硬件查了个底朝天,最后发现——嗯,就是软件同步的问题。
从那以后,我对同步与互斥的敬畏心,就上来了。
1.1 为什么需要同步?
先问一个问题:你的RTOS里跑着多个任务,它们各自独立运行,看起来挺和谐的。但你想过没有,这些任务之间,其实在「抢东西」。
抢什么?
- 抢CPU时间片
- 抢内存里的共享数据
- 抢外设(比如串口、SPI总线)
- 抢中断服务程序里的资源
如果抢得没规矩,系统就会出乱子。我管这叫「多任务世界的交通规则」。
同步,说白了就是让多个任务按照某种约定的顺序执行。比如任务A先生产数据,任务B后消费数据。你不能让B在A还没生产完就去读,对吧?
互斥,则是保证同一时刻,只有一个任务能访问某个共享资源。就像厕所里的坑位——你进去了,别人就得等着。
核心观点:没有同步与互斥的RTOS,就像没有红绿灯的十字路口。车再多,也跑不快,而且早晚得出事。
1.2 竞态条件——那个让你头疼的幽灵
竞态条件(Race Condition),这个词听起来挺学术的。我换个说法:「谁先跑,结果不一样」。
举个例子。两个任务都在对一个全局计数器做自增操作:
// 全局变量
int counter = 0;
// 任务1
void task1(void) {
counter++; // 读-改-写
}
// 任务2
void task2(void) {
counter++; // 读-改-写
}
你期望的结果是counter变成2。但实际运行100次,可能有几次counter只变成了1。
为什么会这样?
因为 counter++ 不是一条原子指令。它在底层拆成了三步:
- 从内存读取counter的值到寄存器
- 在寄存器里加1
- 把新值写回内存
如果任务1刚读完counter(值为0),还没来得及写回去,任务2也读到了0。然后两个任务各自加1,都写回1。你看,两次自增,结果还是1。
注意:竞态条件不是每次都会出现。它跟任务调度时机、中断触发时间、CPU负载都有关系。所以它特别难复现,也特别难调试。我见过有人因为这种bug,排查了整整两周。
竞态条件的本质是什么?多个执行流对共享资源的访问顺序不确定,导致结果依赖于时间上的「巧合」。
1.3 临界区——你的代码「禁区」
既然竞态条件这么讨厌,那怎么解决?
答案就是:划定临界区。
临界区,就是一段代码,这段代码在执行时,不允许其他任务或中断来打扰。说白了,就是给共享资源的访问加个「保护罩」。
临界区有几个特点:
- 短小精悍:临界区里的代码越短越好。你想想看,你把别人挡在外面,自己却在里面磨蹭,系统实时性就差了。
- 不可嵌套:尽量不要在临界区里再进入另一个临界区。容易死锁,我后面会讲。
- 必须成对出现:进入临界区就要退出,不能只进不出。
我的经验:写临界区代码时,我习惯在注释里明确标注「临界区开始」和「临界区结束」。这样代码审查时,一眼就能看出保护范围。另外,临界区里千万别调用延时函数或者阻塞操作——那是在给自己挖坑。
1.4 同步与互斥的实现手段
在RTOS里,我们通常用以下几种机制来实现同步与互斥:
| 机制 | 用途 | 特点 |
|---|---|---|
| 关中断 | 保护极短临界区 | 简单粗暴,但影响中断响应 |
| 信号量 | 同步、资源计数 | 灵活,但容易用错 |
| 互斥锁 | 互斥访问共享资源 | 支持优先级继承,防优先级反转 |
| 事件标志组 | 多条件同步 | 适合等待多个事件 |
| 消息队列 | 任务间数据传递 | 自带同步效果 |
这些机制各有各的适用场景。我个人的习惯是:
- 如果只是保护一个全局变量,用关中断或者自旋锁就够了
- 如果是任务间同步,信号量是首选
- 如果是保护复杂数据结构,用互斥锁更安全
1.5 一个真实案例
讲个我踩过的坑吧。
之前做一个多通道数据采集系统,ADC采样结果通过DMA传输到内存。一个任务负责处理数据,另一个任务负责把结果发送到上位机。
我用了两个任务共享一个环形缓冲区。一开始没加任何保护,结果上位机偶尔会收到乱序的数据。我以为是DMA配置错了,查了三天硬件手册。
后来用逻辑分析仪抓了任务切换的时序,才发现:处理任务刚写到缓冲区一半,发送任务就开始读了。读到的数据自然是半成品。
解决方案很简单:在读写环形缓冲区时,加上互斥锁保护。从那以后,数据再也没乱过。
教训:只要有两个以上的执行流访问同一块内存,你就得考虑同步问题。别等到出bug了再补,那代价太大了。
1.6 小结
这一章咱们聊了:
- 同步与互斥是RTOS多任务编程的基石
- 竞态条件是因为执行流对共享资源的访问顺序不确定导致的
- 临界区是保护共享资源的核心手段,要短小、成对、不嵌套
- RTOS提供了多种同步互斥机制,选对工具很重要
下一章,我会带大家深入信号量的实现原理。到时候咱们手写一个信号量,看看内核里到底是怎么玩的。
嗯,今天就到这儿。记住一句话:没有同步的RTOS,就像没有刹车的跑车——跑得快,死得也快。