1. 任务同步基础:为什么需要同步?竞态条件与临界区概念
好,咱们开始第一讲。
做嵌入式开发,尤其是跑 VxWorks 这种实时系统,你迟早会碰到一个问题:多个任务同时访问同一个资源,系统就乱了。
这不是理论上的“可能”,而是实际中“必然”会遇到的坑。我刚开始做 VxWorks 项目时,就因为这个栽过跟头。当时一个数据采集任务和一个显示任务共享一个缓冲区,结果数据经常错乱,显示的画面一跳一跳的。查了两天,最后发现是同步没做好。
所以,咱们得先搞清楚:为什么需要同步?
1.1 竞态条件:你永远不知道谁先跑完
先看一个最简单的场景。
假设有两个任务:TaskA 和 TaskB。它们都要对一个全局变量 counter 做加1操作。
// 伪代码
int counter = 0;
void TaskA() {
counter = counter + 1; // 读-改-写
}
void TaskB() {
counter = counter + 1; // 读-改-写
}
你想想看,如果两个任务同时执行,会发生什么?
理想情况:counter 从 0 变成 2。
实际情况:可能还是 1。
为什么会这样?因为 counter = counter + 1 不是一条原子指令。它分三步:
- 从内存读取 counter 的值(比如 0)
- 在 CPU 寄存器里加 1(变成 1)
- 把结果写回内存
如果 TaskA 刚读完(步骤1),还没来得及写回(步骤3),TaskB 也来读了。这时候 TaskB 读到的也是 0。两个任务各自加 1,最后写回的都是 1。
这就是典型的竞态条件(Race Condition)。
竞态条件:多个任务并发访问共享数据,最终结果取决于任务执行的精确时序。说白了,谁先谁后不确定,结果就不可控。
我在项目中遇到过最离谱的一次,是一个电机控制任务和日志任务共享了一个状态变量。竞态导致电机状态被错误覆盖,电机直接反转。嗯,那次差点把设备搞坏。
1.2 临界区:保护共享资源的“禁地”
既然竞态条件这么危险,那怎么解决?
核心思路就一句话:同一时间,只允许一个任务访问共享资源。
这个“只允许一个任务访问”的代码段,就叫临界区(Critical Section)。
临界区有三个基本原则:
- 互斥:一个任务在临界区里,其他任务不能进来
- 有限等待:不能一直占着不放,得让其他任务有机会
- 空闲让进:没人用的时候,谁想进谁进
说白了,临界区就像厕所的隔间。一个人进去了,其他人就得排队。你不能进去不出来,也不能没人用的时候锁着门。
个人经验:我习惯把临界区写得尽量短。只保护真正需要保护的代码,比如读写共享变量的那几行。千万别把整个任务函数都包进去,那样性能会崩。
1.3 VxWorks 中的同步机制概览
VxWorks 提供了几种同步工具。咱们先列个表,后面几章会逐个深入。
| 机制 | 适用场景 | 特点 |
|---|---|---|
| 二进制信号量 | 互斥访问、任务同步 | 简单高效,只有 0 和 1 两种状态 |
| 计数信号量 | 资源池管理(如缓冲区) | 可以记录可用资源数量 |
| 互斥锁(Mutex) | 互斥访问(带优先级继承) | 解决优先级反转问题 |
| 读写锁 | 读多写少的场景 | 多个读任务可同时访问 |
你可能会问:信号量和互斥锁有什么区别?
简单说:信号量更轻量,适合通知事件;互斥锁更安全,适合保护共享数据。具体区别,咱们后面章节会详细讲。
1.4 一个真实案例:缓冲区覆盖
最后,分享一个我早期踩过的坑。
当时做一个数据采集系统。一个任务负责从传感器读数据,写到环形缓冲区。另一个任务负责把数据通过网口发出去。
代码大概长这样:
// 伪代码 - 有竞态问题
char buffer[1024];
int write_index = 0;
int read_index = 0;
void ProducerTask() {
buffer[write_index] = read_sensor();
write_index = (write_index + 1) % 1024;
}
void ConsumerTask() {
char data = buffer[read_index];
read_index = (read_index + 1) % 1024;
send_data(data);
}
看起来没问题?
但实际运行时,数据经常丢包。原因就是:Producer 在写 buffer 的时候,Consumer 可能正在读同一个位置。或者 Producer 更新了 write_index,但 Consumer 还没读到,导致读到了旧数据。
避坑指南:我曾经以为只要读写索引分开就安全了。但忽略了“读-改-写”不是原子的。后来加了一个二进制信号量保护临界区,问题才解决。
1.5 小结
这一章咱们讲了三个核心概念:
- 竞态条件:多个任务同时访问共享数据,结果不可预测
- 临界区:需要互斥保护的代码段
- 同步机制:VxWorks 提供了信号量、互斥锁等工具
记住一句话:凡是共享的,都要保护。别指望任务调度顺序永远正确,那是不可能的。
下一章,咱们开始实战:二进制信号量的创建、获取与释放。到时候我会手把手带你写代码。