4. 内核并发与同步:自旋锁、互斥锁、读写锁、原子操作在驱动中的应用
在 Linux 内核驱动开发中,并发访问共享资源是导致竞态条件(Race Condition)和安全漏洞的主要根源。中断处理、内核抢占、多核 SMP 并发以及用户空间访问都可能同时进入驱动临界区。本章系统讲解内核提供的四种核心同步机制,并结合安全驱动开发场景分析其选型与使用规范。
4.1 并发场景与竞态条件
驱动中的并发来源可归纳为以下四类,理解这些场景是正确选择同步机制的前提。
| 并发来源 | 典型场景 | 安全风险 |
|---|---|---|
| SMP 多核并行 | 两个 CPU 核同时执行驱动中的同一个 ioctl 函数 | 共享变量写-写冲突 |
| 内核抢占 | 进程 A 持有锁时被高优先级进程 B 抢占 | 锁持有者被抢占导致死锁 |
| 中断处理 | 进程上下文与中断处理程序共享设备状态寄存器 | 中断打断进程导致数据不一致 |
| 进程睡眠与唤醒 | 等待队列操作与中断完成标志的同步 | 丢失唤醒事件 |
4.2 原子操作:最轻量的同步原语
原子操作(Atomic Operations)是硬件级保证的不可分割的读-改-写序列,适用于计数器、标志位等简单整型变量的并发安全操作。它不涉及锁的获取与释放,不会导致睡眠或上下文切换。
4.2.1 原子类型与 API
| API 函数 | 功能描述 | 安全上下文 |
|---|---|---|
atomic_set(v, i) |
设置原子变量 v 的值为 i | 进程/中断 |
atomic_read(v) |
读取原子变量 v 的值 | 进程/中断 |
atomic_add(i, v) |
原子地将 i 加到 v | 进程/中断 |
atomic_inc(v) |
原子递增 v | 进程/中断 |
atomic_dec_and_test(v) |
原子递减 v,若结果为 0 返回 true | 进程/中断 |
atomic_cmpxchg(v, old, new) |
比较交换:若 v==old 则交换为 new | 进程/中断 |
atomic_t 而非普通 int。
4.2.2 驱动示例:设备打开计数
#include <linux/atomic.h>
static atomic_t device_open_count = ATOMIC_INIT(0);
static int my_device_open(struct inode *inode, struct file *file)
{
if (!atomic_add_unless(&device_open_count, 1, MAX_OPEN_COUNT))
return -EBUSY; /* 已达最大打开数 */
return 0;
}
static int my_device_release(struct inode *inode, struct file *file)
{
atomic_dec(&device_open_count);
return 0;
}
4.3 自旋锁:短临界区的首选
自旋锁(Spinlock)是一种忙等锁,当锁被持有时,等待的 CPU 会原地循环检测锁状态。它适用于临界区执行时间极短(微秒级)且不允许睡眠的场景,如中断上下文。
4.3.1 自旋锁变体与选择
| 变体 | 禁用抢占 | 禁用本地中断 | 适用场景 |
|---|---|---|---|
spin_lock() |
是 | 否 | 进程上下文,无中断共享 |
spin_lock_irq() |
是 | 是 | 进程与中断上下文共享 |
spin_lock_irqsave() |
是 | 是(保存状态) | 不确定中断是否已禁用 |
spin_trylock() |
是 | 否 | 非阻塞尝试获取 |
copy_from_user()、kmalloc(GFP_KERNEL) 等可能睡眠的函数,会导致内核崩溃。自旋锁临界区必须原子执行,不可睡眠。
4.3.2 驱动示例:中断与进程共享寄存器
static DEFINE_SPINLOCK(dev_lock);
static u32 device_status;
/* 中断处理程序 */
static irqreturn_t my_interrupt(int irq, void *dev_id)
{
spin_lock(&dev_lock);
device_status = readl(REG_STATUS);
/* 处理中断 */
spin_unlock(&dev_lock);
return IRQ_HANDLED;
}
/* 进程上下文读状态 */
static ssize_t my_read(struct file *file, char __user *buf, size_t len, loff_t *off)
{
u32 status;
unsigned long flags;
spin_lock_irqsave(&dev_lock, flags);
status = device_status;
spin_unlock_irqrestore(&dev_lock, flags);
if (copy_to_user(buf, &status, sizeof(status)))
return -EFAULT;
return sizeof(status);
}
4.4 互斥锁:允许睡眠的长期锁
互斥锁(Mutex)是一种睡眠等待锁,当锁被持有时,等待的进程会进入睡眠状态,不会占用 CPU。它适用于临界区执行时间较长(毫秒级或更长)或需要睡眠的场景,但不能在中断上下文使用。
4.4.1 Mutex 与 Spinlock 对比
| 特性 | Mutex | Spinlock |
|---|---|---|
| 等待行为 | 睡眠等待,不占 CPU | 忙等,占用 CPU |
| 中断上下文 | 禁止使用 | 允许使用 |
| 临界区时长 | 任意长度 | 极短(微秒级) |
| 递归锁定 | 不支持(会死锁) | 不支持 |
| 优先级反转 | 内核已处理(PI) | 不涉及 |
4.4.2 驱动示例:配置操作保护
static DEFINE_MUTEX(config_mutex);
static struct device_config config_data;
static long my_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
struct device_config __user *uconfig = (struct device_config __user *)arg;
switch (cmd) {
case SET_CONFIG:
if (mutex_lock_interruptible(&config_mutex))
return -ERESTARTSYS;
if (copy_from_user(&config_data, uconfig, sizeof(config_data))) {
mutex_unlock(&config_mutex);
return -EFAULT;
}
/* 应用配置到硬件 */
apply_config(&config_data);
mutex_unlock(&config_mutex);
return 0;
case GET_CONFIG:
mutex_lock(&config_mutex);
if (copy_to_user(uconfig, &config_data, sizeof(config_data))) {
mutex_unlock(&config_mutex);
return -EFAULT;
}
mutex_unlock(&config_mutex);
return 0;
}
return -ENOTTY;
}
mutex_lock_interruptible() 而非 mutex_lock() 允许进程在等待锁时被信号中断,避免驱动导致用户空间进程无法被杀死的“D 状态”问题。
4.5 读写锁:区分读/写访问模式
读写锁(rwlock / rwsem)允许多个读者并发访问,但写者必须独占。当读操作远多于写操作时,读写锁能显著提升并发性能。
4.5.1 自旋读写锁(rwlock_t)
rwlock_t 是自旋锁的读写变体,适用于中断上下文中的读多写少场景。
static DEFINE_RWLOCK(my_rwlock);
static u32 statistics[64];
/* 多个读者可同时进入 */
static u32 read_statistic(int index)
{
u32 val;
read_lock(&my_rwlock);
val = statistics[index];
read_unlock(&my_rwlock);
return val;
}
/* 写者必须独占 */
static void write_statistic(int index, u32 val)
{
write_lock(&my_rwlock);
statistics[index] = val;
write_unlock(&my_rwlock);
}
4.5.2 信号量读写锁(rwsem)
struct rw_semaphore 是互斥锁的读写变体,允许读者睡眠,适用于进程上下文。
| 操作 | 读者 API | 写者 API |
|---|---|---|
| 获取锁 | down_read() |
down_write() |
| 可中断获取 | down_read_interruptible() |
down_write_interruptible() |
| 释放锁 | up_read() |
up_write() |
| 尝试获取 | down_read_trylock() |
down_write_trylock() |
4.6 同步机制选型决策树
在驱动开发中选择同步机制时,可按照以下决策流程快速定位合适的原语:
- 临界区是否在中断上下文执行?
- 是 → 只能使用自旋锁或原子操作
- 否 → 进入下一步
- 临界区是否可能睡眠?
- 是(如 copy_from_user、kmalloc 等)→ 使用 Mutex 或 rwsem
- 否 → 进入下一步
- 临界区执行时间是否极短(微秒级)?
- 是 → 自旋锁
- 否 → Mutex
- 读操作是否远多于写操作?
- 是 → 考虑读写锁(rwlock / rwsem)
- 否 → 使用基本锁即可
- 是否仅操作简单整型变量?
- 是 → 优先使用原子操作,避免锁开销
4.7 死锁预防与调试
死锁是驱动开发中最危险的并发问题之一。内核提供了多种工具帮助检测和预防死锁:
| 工具/机制 | 功能 | 启用方式 |
|---|---|---|
| Lockdep | 运行时锁依赖验证,检测潜在死锁 | 内核配置 CONFIG_LOCKDEP=y |
| Spinlock 调试 | 检测自旋锁使用错误(如睡眠时持有锁) | CONFIG_DEBUG_SPINLOCK=y |
| Mutex 调试 | 检测互斥锁使用错误 | CONFIG_DEBUG_MUTEXES=y |
| 原子操作调试 | 检测原子变量溢出或未初始化 | CONFIG_DEBUG_ATOMIC_SLEEP=y |
4.7.1 常见死锁场景与规避
- AB-BA 死锁: 线程 1 持有锁 A 等待锁 B,线程 2 持有锁 B 等待锁 A。规避:所有代码路径按相同顺序获取锁。
- 自死锁: 递归调用或同一个函数再次获取已持有的非递归锁。规避:明确锁的层次,避免递归。
- 中断死锁: 进程持有锁时被中断打断,中断处理程序尝试获取同一锁。规避:在进程上下文使用
spin_lock_irqsave()禁用本地中断。 - 优先级反转: 低优先级进程持有锁,高优先级进程等待,中优先级进程抢占低优先级进程。规避:使用支持优先级继承的 Mutex(内核默认支持)。
4.8 本章小结
内核并发同步是安全驱动开发的基石。原子操作为简单计数提供无锁方案;自旋锁保护中断上下文中的短临界区;互斥锁适用于进程上下文中可能睡眠的长操作;读写锁优化读多写少场景。选择同步机制时,必须结合执行上下文、临界区时长和访问模式综合判断。启用 Lockdep 等调试工具,在开发阶段彻底消除死锁风险,是生产级驱动的基本要求。