1. 并发与竞态基础:什么是并发、竞态条件以及它们在Linux驱动中的典型场景
在Linux驱动开发中,并发与竞态是导致系统崩溃、数据损坏和死锁的头号根源。本章将建立并发编程的核心概念,并揭示它们在驱动代码中的具体表现形式。
1.1 并发:多个执行流同时活跃
并发(Concurrency)指多个执行单元在时间上重叠地执行。在单核CPU上,并发通过时间片轮转实现宏观上的“同时”;在多核CPU上,则是真正的并行执行。
关键认知: 并发 ≠ 并行。并发是逻辑上的同时发生,并行是物理上的同时执行。驱动开发者必须同时应对这两种情况。
Linux内核中的并发执行流来源:
| 并发源 | 说明 | 典型驱动场景 |
|---|---|---|
| 进程上下文 | 多个用户进程同时调用驱动接口 | open/read/write/ioctl 并发调用 |
| 中断上下文 | 硬件中断触发,抢占当前进程 | 网卡收包中断与数据读取 |
| 软中断/tasklet | 中断下半部,延迟执行 | 网络协议栈处理、定时器回调 |
| 内核线程 | 内核创建的后台工作线程 | 设备轮询、数据批量处理 |
| SMP多核 | 多个CPU核心同时执行内核代码 | 共享硬件寄存器访问 |
1.2 竞态条件:结果依赖于执行时序
竞态条件(Race Condition)是指多个执行流以不可预测的顺序访问共享资源,导致程序行为依赖于各执行流的相对执行速度。当两个或多个执行流同时进入临界区(Critical Section)时,数据完整性被破坏。
竞态三要素: 共享资源 + 并发访问 + 非原子操作 = 竞态条件。缺少任何一个要素都不会产生竞态。
经典的“检查-执行”竞态模式:
/* 竞态示例:设备引用计数 */
if (dev->ref_count > 0) { /* 检查 */
dev->ref_count--; /* 执行 */
/* 此时另一个线程可能已将ref_count减为0 */
if (dev->ref_count == 0) {
cleanup_device(dev); /* 双重释放! */
}
}
上例中,两个线程同时进入if (dev->ref_count > 0)检查,都认为引用计数合法,随后都执行dev->ref_count--,导致引用计数变为-1,且cleanup_device被调用两次。
1.3 驱动中的典型竞态场景
以下是在实际Linux驱动开发中最常遇到的竞态场景:
| 场景 | 竞态描述 | 后果 |
|---|---|---|
| 设备打开/关闭 | 多个进程同时open/close,引用计数操作非原子 | 资源泄漏、双重释放 |
| IOCTL并发 | 多个线程同时发送控制命令,修改设备状态 | 设备状态不一致 |
| 中断与进程共享数据 | 中断处理函数与进程上下文同时访问环形缓冲区 | 数据损坏、指针错乱 |
| DMA缓冲区访问 | CPU与DMA控制器同时操作同一内存区域 | 数据不一致、硬件故障 |
| 定时器与进程交互 | 定时器回调与进程同时修改设备超时参数 | 超时逻辑失效 |
| 多核寄存器访问 | 两个CPU核心同时读写硬件寄存器 | 寄存器值被覆盖 |
1.4 竞态导致的典型问题
- 数据损坏: 共享变量被部分更新,读取到不一致的中间状态
- 内存泄漏: 引用计数错误导致资源无法释放
- 双重释放: 同一内存块被释放两次,导致内核崩溃
- 死锁: 多个执行流互相等待对方释放锁资源
- 优先级反转: 低优先级任务持有锁,阻塞高优先级任务
- 安全漏洞: 竞态窗口被利用进行权限提升或信息泄露
实战警示: 竞态条件是最难调试的bug类型之一。它可能只在特定负载、特定硬件配置下复现,且调试信息本身可能改变执行时序,使bug消失(Heisenbug效应)。因此,预防胜于调试——在设计阶段就考虑并发保护。
1.5 并发控制的核心思路
解决竞态问题的核心是互斥访问——确保同一时刻只有一个执行流可以操作共享资源。Linux内核提供了多种同步机制:
| 同步机制 | 适用上下文 | 特点 |
|---|---|---|
| 自旋锁 (spinlock) | 中断上下文、短临界区 | 忙等待,不可睡眠 |
| 互斥锁 (mutex) | 进程上下文、长临界区 | 可睡眠,允许调度 |
| 信号量 (semaphore) | 进程上下文 | 允许多个读者 |
| RCU (Read-Copy-Update) | 读多写少场景 | 读者无锁,写者需等待 |
| 原子操作 (atomic_t) | 简单计数器、标志位 | 硬件级原子指令 |
本课程后续章节将深入剖析自旋锁和互斥锁的实现原理、使用规范以及死锁的调试方法。