1. 并发控制基础:并发编程概念、竞态条件、临界区、原子操作概述
在嵌入式 Linux 驱动开发中,并发控制是保障系统稳定性和数据一致性的基石。本章节从最基础的并发编程概念出发,逐步剖析竞态条件的成因、临界区的划定原则,以及原子操作这一最底层的同步原语。理解这些基础概念,是后续学习 RCU(Read-Copy-Update)等高级同步机制的前提。
1.1 并发编程核心概念
并发(Concurrency)是指多个执行单元在宏观上同时推进的执行方式。在嵌入式 Linux 系统中,并发来源主要包括:
- 对称多处理(SMP):多个 CPU 核心同时执行内核代码。
- 中断与底半部:硬件中断、软中断、tasklet、工作队列等异步执行路径。
- 内核抢占:高优先级进程抢占低优先级进程的执行。
- 进程与线程:多个用户态进程或内核线程共享资源。
关键认知: 并发 ≠ 并行。并发是逻辑上的同时执行(单核时间片轮转也可实现),并行是物理上的同时执行(多核)。驱动开发者需要同时应对这两种情况。
1.2 竞态条件(Race Condition)
竞态条件是指多个执行单元对共享资源进行非原子访问时,最终结果依赖于执行顺序的不可预测性。竞态条件通常导致数据损坏、系统崩溃或安全漏洞。
| 竞态类型 | 典型场景 | 后果 |
|---|---|---|
| 读-写竞态 | 一个线程读取共享变量,同时另一个线程写入 | 读取到不一致的中间值 |
| 写-写竞态 | 两个线程同时写入同一共享变量 | 最终值取决于写入顺序,可能丢失更新 |
| 检查-使用竞态 | 先检查条件再使用资源,但条件在检查后改变 | 使用已释放或无效的资源 |
驱动开发警示: 竞态条件在单核非抢占系统也可能发生(如中断与进程上下文之间)。不要假设“单核无竞态”。
1.3 临界区(Critical Section)
临界区是指访问共享资源的代码片段,在同一时刻只允许一个执行单元进入。临界区的划定直接影响并发控制的粒度与性能。
临界区设计原则:
- 最小化原则:临界区应只包含必要的共享资源访问代码,避免无关操作。
- 不可嵌套过度:避免在临界区内调用可能引起阻塞或调度的函数。
- 明确边界:清晰标记临界区的入口与出口,防止遗漏。
1.4 原子操作(Atomic Operation)
原子操作是不可被中断的单一操作,要么全部执行完成,要么完全不执行。在 Linux 内核中,原子操作是构建所有高级同步机制的基础砖块。
1.4.1 原子操作分类
| 类型 | API 示例 | 说明 |
|---|---|---|
| 原子整数操作 | atomic_t / atomic_inc() / atomic_dec_and_test() |
对 32 位整数的原子加减、测试 |
| 原子位操作 | set_bit() / clear_bit() / test_and_set_bit() |
对内存中某一位的原子置位、清除、测试 |
| 原子长整型操作 | atomic64_t / atomic64_add() |
64 位架构下的原子操作(ARM64、x86_64) |
1.4.2 原子操作使用示例
#include <linux/atomic.h>
static atomic_t usage_count = ATOMIC_INIT(0);
/* 驱动打开时原子递增 */
int driver_open(struct inode *inode, struct file *filp) {
if (!atomic_add_unless(&usage_count, 1, MAX_USERS))
return -EBUSY;
return 0;
}
/* 驱动关闭时原子递减 */
int driver_release(struct inode *inode, struct file *filp) {
atomic_dec(&usage_count);
return 0;
}
原子操作优势: 无锁、无阻塞、无上下文切换开销,适用于简单的计数器、标志位、引用计数等场景。但原子操作无法解决复杂的资源保护问题(如链表遍历与修改)。
1.5 并发控制层次结构
从原子操作到 RCU,Linux 内核提供了一套层次化的并发控制机制。理解各层次的适用场景和性能特征,是驱动开发者的核心能力。
1.6 本章小结
- 并发来源:SMP、中断、抢占、多进程/线程是嵌入式 Linux 驱动的四大并发源头。
- 竞态条件:当多个执行单元无序访问共享资源时产生,是驱动 bug 的主要来源。
- 临界区:保护共享资源的代码段,应遵循最小化原则。
- 原子操作:最底层的同步原语,适用于简单计数和标志位保护,是构建锁和 RCU 的基础。