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++ 不是一条原子指令。它在底层拆成了三步:

  1. 从内存读取counter的值到寄存器
  2. 在寄存器里加1
  3. 把新值写回内存

如果任务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,就像没有刹车的跑车——跑得快,死得也快