1、并发与竞态:并发编程基础概念、竞态条件定义、临界区概念、锁的必要性
在嵌入式 Linux 驱动开发中,并发(Concurrency) 与 竞态(Race Condition) 是导致系统崩溃、数据损坏、死锁等严重问题的根源。理解这些基础概念,是正确使用 spinlock 与 mutex 的前提。本章将系统性地建立并发编程的认知框架。
1.1 并发编程基础概念
并发是指多个执行单元(如进程、线程、中断处理程序、软中断、tasklet)在宏观上同时执行,微观上可能交替执行。在单核 CPU 上,并发通过时间片轮转实现;在多核 CPU 上,则是真正的并行执行。
嵌入式 Linux 内核中的并发来源主要包括:
- 多进程/多线程:用户空间多个进程或线程同时访问驱动。
- 中断处理程序:硬件中断触发,抢占当前正在执行的进程上下文。
- 软中断与 tasklet:在中断上下文或特定时机执行,可能抢占进程上下文。
- 内核抢占:高优先级进程可以抢占正在运行的低优先级进程。
- 多核对称多处理(SMP):多个 CPU 核心同时执行内核代码。
关键认知: 驱动开发者必须假设任何时刻都可能被其他执行单元打断或共享数据被同时访问。不假设“单核”、“非抢占”或“无中断”环境。
1.2 竞态条件定义
竞态条件(Race Condition) 是指多个执行单元在没有适当同步的情况下,对共享资源进行并发访问,导致最终结果依赖于执行单元的时间顺序(即“谁先跑完”),从而产生不可预测的错误。
竞态条件的典型特征:
- 结果依赖于执行单元的调度顺序。
- 错误是间歇性的,难以复现和调试。
- 通常发生在“检查-执行”非原子操作序列中。
常见误区: 很多开发者认为“只要代码逻辑正确,就不会出问题”。但在并发环境下,即使每行代码都正确,执行顺序的混乱也会导致逻辑失效。竞态是时序问题,不是逻辑问题。
一个经典的竞态示例:
// 假设共享变量 count 初始为 0
// 进程 A 执行:if (count == 0) { count++; }
// 进程 B 执行:if (count == 0) { count++; }
// 期望结果:count = 1(只应加一次)
// 实际可能:count = 2(两个进程同时检查到 count == 0)
1.3 临界区概念
临界区(Critical Section) 是指访问共享资源的代码片段,这段代码在同一时间只能被一个执行单元执行。临界区的核心要求是 互斥(Mutual Exclusion)。
临界区的设计原则:
- 互斥性:同一时刻最多只有一个执行单元进入临界区。
- 有限等待:任何执行单元在发出进入请求后,必须在有限时间内获得进入许可。
- 前进性:当临界区空闲且没有执行单元在等待时,应允许某个等待单元进入。
| 特性 | 临界区 | 非临界区 |
|---|---|---|
| 访问资源 | 共享资源(全局变量、硬件寄存器、链表等) | 私有资源(局部变量、线程栈数据) |
| 并发要求 | 必须互斥访问 | 可同时访问,无冲突风险 |
| 执行时间 | 应尽可能短,避免阻塞其他执行单元 | 无严格限制 |
| 锁的使用 | 必须使用锁保护 | 不需要锁 |
1.4 锁的必要性
锁(Lock)是实现互斥访问的基本工具。没有锁,竞态条件将导致以下典型问题:
- 数据损坏:多个执行单元同时修改同一变量,导致最终值错误。
- 指针悬挂:一个执行单元释放内存,另一个执行单元仍在访问该内存。
- 硬件寄存器冲突:多个执行单元同时配置硬件寄存器,导致设备状态异常。
- 链表/队列结构破坏:并发插入/删除操作导致链表断裂或循环。
经验法则: 只要存在共享的可写数据,就必须使用锁保护。只读数据不需要锁。锁的粒度(保护的数据范围)应尽可能小,以提高并发性能。
锁的核心作用可以总结为:
- 原子化操作序列:将“检查-执行”等非原子操作序列变为原子操作。
- 内存屏障:保证锁释放前的写操作对其他执行单元可见。
- 调度控制:spinlock 让等待者忙等(不调度),mutex 让等待者睡眠(调度)。
1.5 本章小结
并发与竞态是驱动开发中无法回避的核心问题。理解并发来源、识别竞态条件、划定临界区、使用锁保护,是编写可靠驱动的基石。在后续章节中,我们将深入探讨 spinlock 和 mutex 的具体实现、使用场景、注意事项及调试方法。
核心记忆点: 共享数据 + 并发访问 = 必须加锁。不加锁的共享数据,迟早会出问题,且问题难以复现和定位。