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 来避免中断嵌套。
spinlock 忙等机制 CPU 0(持有锁) CPU 1(自旋等待) spinlock locked=1 持有 while(locked) 轮询 关键特征 • 忙等不睡眠 • 临界区必须极短 • 中断上下文可用 • 不支持递归 • 需关中断防嵌套

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。
mutex 睡眠等待 vs spinlock 忙等 时间→ 持有者执行 睡眠 唤醒后执行 释放锁 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 核心原理

  1. 读端:通过 rcu_read_lock() / rcu_read_unlock() 标记临界区,实际不阻塞。
  2. 写端:复制数据,更新指针,等待所有读者退出后释放旧数据。
  3. 宽限期(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 消除了每次读取的锁开销,显著降低帧延迟抖动。
RCU 读-写-更新流程 旧数据 A 新数据 B ptr→A 写者:复制 B 写者:更新 ptr→B 宽限期等待 释放旧数据 A 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 锁选型决策树

以下决策树帮助你在实际开发中快速选择正确的锁机制:

锁选型决策树 临界区需要保护? 仅简单变量操作? atomic_ops 中断上下文? spinlock 读多写少? RCU / rwlock 临界区长度? ← 上一章 📖 返回目录 下一章 →