3. 内核锁机制
在 Linux 内核与 Android 驱动开发中,锁机制是保证并发安全的基石。选择错误的锁类型不仅会导致性能下降,更会引发死锁、优先级反转等稳定性问题。本章深入剖析五种核心锁机制的原理、适用场景与差异,帮助你在驱动开发中做出正确的锁选型决策。
核心原则:锁的选择本质是“临界区执行时间”与“上下文环境”的权衡。短临界区(< 100 纳秒)优先 spinlock;长临界区(> 100 微秒)必须 mutex;读多写少用 rwlock;读极多且允许延迟用 RCU;简单原子操作直接使用 atomic_ops。
3.1 锁机制全景对比
下表从等待行为、上下文限制、睡眠能力、适用临界区长度、死锁风险五个维度,对五种锁机制进行横向对比:
| 锁类型 | 等待行为 | 上下文限制 | 能否睡眠 | 临界区长度 | 典型死锁风险 |
|---|---|---|---|---|---|
| spinlock | 忙等(轮询) | 中断上下文可用 | 否 | 极短(< 100 纳秒) | 中断嵌套死锁 |
| mutex | 睡眠等待 | 仅进程上下文 | 是 | 长(> 100 微秒) | 优先级反转 |
| rwlock | 读并发 / 写独占 | 中断上下文可用 | 否 | 中短 | 写者饥饿 |
| rcu_lock | 读无锁 / 写延迟回收 | 读端任意上下文 | 读端否 / 写端可睡眠 | 读极短 / 写可长 | 宽限期未结束 |
| atomic_ops | 无等待(硬件原子) | 所有上下文 | 否 | 单指令 | 无(但需注意内存序) |
Android 驱动实战提示:在中断处理函数(ISR)或软中断(softirq)中,只能使用 spinlock 或 atomic_ops。mutex 会导致睡眠,在中断上下文中调用会触发 BUG。这是驱动稳定性排查中最常见的违规模式之一。
3.2 spinlock:自旋锁
spinlock 是最基础的内核锁。当锁被持有时,等待线程会在一个循环中不断检查锁状态(自旋),不会睡眠。这种忙等行为决定了它只能用于极短临界区。
3.2.1 使用场景
- 中断处理函数:保护 ISR 与进程上下文共享的数据。
- SMP 系统上的短临界区:如更新一个设备寄存器、修改一个标志位。
- 实时性要求高的路径:避免调度延迟。
3.2.2 变体与选择
| 变体 | 说明 | 适用场景 |
|---|---|---|
spin_lock() |
普通自旋锁 | 进程上下文,无中断竞争 |
spin_lock_irqsave() |
保存本地中断状态后加锁 | 中断与进程上下文共享数据 |
spin_lock_bh() |
禁止下半部后加锁 | 进程上下文与 softirq/tasklet 共享 |
// spinlock 典型用法:保护设备状态寄存器
static DEFINE_SPINLOCK(dev_lock);
static int dev_status;
void dev_isr(int irq, void *dev_id) {
unsigned long flags;
spin_lock_irqsave(&dev_lock, flags);
dev_status = readl(DEV_STATUS_REG);
spin_unlock_irqrestore(&dev_lock, flags);
}
int dev_read_status(void) {
unsigned long flags;
int status;
spin_lock_irqsave(&dev_lock, flags);
status = dev_status;
spin_unlock_irqrestore(&dev_lock, flags);
return status;
}
死锁陷阱:如果在持有 spinlock 的代码路径中尝试获取同一个 spinlock(递归加锁),或者触发中断且中断处理函数也尝试获取同一把锁,将导致 self-deadlock。spinlock 不支持递归,必须使用
spin_lock_irqsave 来避免中断嵌套。
3.3 mutex:互斥锁
mutex 是睡眠锁。当锁被持有时,等待线程会进入睡眠状态,让出 CPU。这允许临界区执行较长时间的操作,但代价是上下文切换的开销。
3.3.1 使用场景
- 进程上下文中的长临界区:如文件系统操作、内存分配、硬件初始化序列。
- 需要睡眠的操作:如
copy_from_user()、msleep()、wait_event()。 - 保护复杂数据结构:如链表遍历与修改、树结构操作。
3.3.2 关键特性
| 特性 | 说明 |
|---|---|
| 所有者追踪 | 内核记录当前持有者,支持调试 |
| 优先级继承 | 防止优先级反转(RT 内核默认开启) |
| 递归支持 | 通过 mutex_lock_nested() 支持有限递归 |
| 调试支持 | CONFIG_DEBUG_MUTEXES 提供死锁检测 |
// mutex 典型用法:保护设备打开/关闭计数
static DEFINE_MUTEX(open_mutex);
static int open_count;
int dev_open(struct inode *inode, struct file *file) {
mutex_lock(&open_mutex);
if (open_count >= MAX_OPEN) {
mutex_unlock(&open_mutex);
return -EBUSY;
}
open_count++;
mutex_unlock(&open_mutex);
return 0;
}
int dev_release(struct inode *inode, struct file *file) {
mutex_lock(&open_mutex);
open_count--;
mutex_unlock(&open_mutex);
return 0;
}
Android 驱动最佳实践:在 probe() 函数中初始化设备时,优先使用 mutex 而非 spinlock。probe 过程通常涉及 I2C/SPI 通信、GPIO 配置、内存分配等耗时操作,使用 mutex 可以避免长时间占用 CPU。
3.4 rwlock:读写锁
rwlock 允许多个读者同时进入临界区,但写者必须独占。这种机制在读多写少的场景下能显著提升并发性能。
3.4.1 使用场景
- 读多写少的全局配置:如设备参数表、路由表、状态标志。
- 统计信息收集:多个读者读取计数器,偶尔写者重置。
- 中断上下文中的读操作:rwlock 可在中断上下文中使用。
3.4.2 注意事项
- 写者饥饿:如果读者持续不断,写者可能永远无法获得锁。
- 不可睡眠:rwlock 是自旋锁的变体,临界区必须短。
- RCU 替代:在 Linux 内核中,RCU 已逐渐取代 rwlock 成为读多写少场景的首选。
// rwlock 典型用法:保护设备配置表
static DEFINE_RWLOCK(config_rwlock);
static struct dev_config config_table[MAX_DEV];
void read_config(int id, struct dev_config *out) {
read_lock(&config_rwlock);
*out = config_table[id]; // 短拷贝
read_unlock(&config_rwlock);
}
void update_config(int id, struct dev_config *new) {
write_lock(&config_rwlock);
config_table[id] = *new;
write_unlock(&config_rwlock);
}
性能陷阱:在 ARM64 多核系统上,rwlock 的读端性能可能不如预期。因为读锁获取需要原子操作来更新读者计数,这会导致 cache line bouncing。对于真正读极多的场景,应优先考虑 RCU。
3.5 rcu_lock:RCU 锁
RCU(Read-Copy-Update)是 Linux 内核中最强大的无锁读机制。读者不需要获取任何锁,写者通过“复制-更新-延迟回收”的方式保证数据一致性。
3.5.1 核心原理
- 读端:通过
rcu_read_lock()/rcu_read_unlock()标记临界区,实际不阻塞。 - 写端:复制数据,更新指针,等待所有读者退出后释放旧数据。
- 宽限期(Grace Period):写者必须等待所有在更新前进入 RCU 临界区的读者退出。
3.5.2 使用场景
- 读频率极高、写极少:如网络路由表、文件系统 dentry 缓存、设备链表。
- 读者不能容忍任何锁开销:如网络数据包处理路径。
- 读者在中断上下文:RCU 读端可在任何上下文使用。
// RCU 典型用法:保护设备链表
struct dev_entry {
struct list_head list;
int id;
char name[32];
struct rcu_head rcu;
};
static LIST_HEAD(dev_list);
// 读端:遍历链表
void show_devices(void) {
struct dev_entry *entry;
rcu_read_lock();
list_for_each_entry_rcu(entry, &dev_list, list) {
pr_info("dev %d: %s\n", entry->id, entry->name);
}
rcu_read_unlock();
}
// 写端:删除节点
void delete_device(int id) {
struct dev_entry *entry;
spin_lock(&dev_list_lock); // 保护链表修改
list_for_each_entry_rcu(entry, &dev_list, list) {
if (entry->id == id) {
list_del_rcu(&entry->list);
spin_unlock(&dev_list_lock);
call_rcu(&entry->rcu, free_dev_entry);
return;
}
}
spin_unlock(&dev_list_lock);
}
Android 驱动优化点:在相机驱动中,传感器配置参数表通常使用 RCU 保护。应用层频繁读取当前配置(预览帧率、分辨率),而配置更新仅在切换模式时发生。使用 RCU 消除了每次读取的锁开销,显著降低帧延迟抖动。
3.6 atomic_ops:原子操作
atomic_ops 是硬件级别的原子指令封装,不需要锁。它们保证在 SMP 系统上,对整型变量的读-改-写操作是原子的。
3.6.1 使用场景
- 简单计数器:如引用计数、错误计数、统计计数。
- 位操作:设置/清除/测试设备寄存器中的标志位。
- 无锁状态机:如设备状态切换(IDLE → BUSY → DONE)。
3.6.2 常用 API
| API | 操作 | 返回值 |
|---|---|---|
atomic_inc() |
原子加 1 | 无 |
atomic_dec_and_test() |
原子减 1,测试是否为 0 | bool |
atomic_cmpxchg() |
比较并交换 | 旧值 |
atomic_add_unless() |
除非等于某值,否则加 | bool |
set_bit() |
原子设置位 | 无 |
test_and_clear_bit() |
原子测试并清除位 | 原值 |
// atomic_ops 典型用法:设备引用计数与状态切换
static atomic_t dev_refcount = ATOMIC_INIT(0);
static atomic_t dev_state = ATOMIC_INIT(DEV_IDLE);
int dev_get(struct device *dev) {
if (atomic_add_unless(&dev_refcount, 1, MAX_REF)) {
return 0; // 成功增加引用
}
return -EBUSY;
}
void dev_put(struct device *dev) {
if (atomic_dec_and_test(&dev_refcount)) {
// 最后一个引用释放,触发清理
schedule_work(&dev_cleanup_work);
}
}
// 无锁状态切换:IDLE → BUSY
int dev_start(struct device *dev) {
int old = atomic_cmpxchg(&dev_state, DEV_IDLE, DEV_BUSY);
if (old != DEV_IDLE) {
return -EALREADY;
}
// 启动硬件...
return 0;
}
性能对比数据:在 ARM Cortex-A76 上,一次
atomic_inc() 约 10-20 纳秒;一次 spin_lock/unlock 约 30-50 纳秒;一次 mutex_lock/unlock(无竞争)约 100-200 纳秒。对于简单的计数器或标志位,atomic_ops 是最优选择。
3.7 锁选型决策树
以下决策树帮助你在实际开发中快速选择正确的锁机制: