1. Linux内核锁机制概述:自旋锁、互斥锁、读写锁、RCU锁的基本概念与适用场景

在多核、多线程并发的内核环境中,保护共享资源免受竞态条件(Race Condition)的破坏是驱动开发的核心挑战。Linux内核提供了多种锁机制,每种锁在设计哲学、开销、睡眠行为与适用场景上存在显著差异。理解这些锁的本质是后续死锁分析与lockdep应用的基础。

核心原则:锁的选择本质是“并发粒度”与“上下文约束”的权衡。在中断上下文只能使用自旋锁,在进程上下文且允许睡眠时优先考虑互斥锁。

1.1 自旋锁(Spinlock)

自旋锁是最基础的锁机制。当锁被持有时,尝试获取锁的线程会忙等待(Busy-Wait),即在一个循环中不断检查锁是否可用,不会主动让出CPU。这种机制避免了上下文切换的开销,但浪费了CPU周期。

特性 说明
等待行为 忙等待(自旋)
睡眠能力 不可睡眠
上下文限制 可用于中断上下文、软中断、进程上下文
开销 低(无上下文切换)
临界区要求 必须短且快速执行
适用场景:保护短小的临界区,例如修改一个设备寄存器、更新一个共享计数器、操作一个链表节点。在中断处理函数(ISR)中,自旋锁是唯一可用的睡眠锁替代方案。
// 自旋锁典型用法
spinlock_t my_lock;
spin_lock_init(&my_lock);

spin_lock(&my_lock);
// 临界区:快速操作共享数据
shared_counter++;
spin_unlock(&my_lock);
避坑提示:在自旋锁持有的临界区内,绝对不能调用可能导致睡眠的函数(如 kmalloc(GFP_KERNEL)、copy_from_user()、mutex_lock())。这会导致内核崩溃或死锁。
自旋锁忙等待流程图 CPU 1 持有锁 执行临界区 释放锁 CPU 2 尝试获取锁 自旋等待(忙等) 获取锁,进入临界区 锁可用? 否 是 获取锁 说明:CPU 2 在自旋等待期间持续占用CPU,直到CPU 1释放锁

1.2 互斥锁(Mutex)

互斥锁是Linux内核中最常用的睡眠锁。当锁被持有时,尝试获取锁的线程会主动睡眠,将CPU让给其他可运行的任务。当锁被释放时,等待队列中的任务会被唤醒。这种机制避免了CPU空转,但引入了上下文切换的开销。

特性 说明
等待行为 睡眠等待(上下文切换)
睡眠能力 可以睡眠
上下文限制 仅限进程上下文(可睡眠上下文)
开销 较高(有上下文切换)
临界区要求 可长可短,但不宜过短(否则切换开销大于自旋)
适用场景:保护较长的临界区,例如设备I/O操作、数据拷贝、内存分配。当临界区可能包含睡眠操作时,必须使用互斥锁。
// 互斥锁典型用法
struct mutex my_mutex;
mutex_init(&my_mutex);

mutex_lock(&my_mutex);
// 临界区:可能包含睡眠操作
if (copy_from_user(kbuf, user_buf, size)) {
    mutex_unlock(&my_mutex);
    return -EFAULT;
}
device_write(kbuf, size);
mutex_unlock(&my_mutex);
避坑提示:互斥锁不能在中断上下文或软中断中使用,因为在这些上下文中不能睡眠。如果在中断中尝试获取互斥锁,会导致内核报错“scheduling while atomic”。
互斥锁睡眠等待流程图 线程A 持有锁 执行临界区 释放锁,唤醒等待者 线程B 尝试获取锁 睡眠(让出CPU) 被唤醒,获取锁 唤醒 说明:线程B在等待期间不占用CPU,系统可以调度其他任务执行

1.3 读写锁(Read-Write Lock)

读写锁是对自旋锁或互斥锁的扩展,区分了“读”与“写”两种访问模式。多个读者可以同时持有读锁,但写者必须独占锁。这种机制在读多写少的场景下能显著提升并发性能。

特性 读锁(共享) 写锁(独占)
并发性 多个读者可同时持有 仅一个写者可持有
与写锁互斥 是(读锁阻塞写锁) 是(写锁阻塞读锁和写锁)
变体 rwlock_t(自旋)、rwsem(睡眠) 同上
适用场景 读远多于写的数据结构 写操作较少且需要独占
适用场景:保护频繁读取但极少修改的数据结构,例如路由表、设备状态标志、配置参数。在驱动中,常用于保护只读寄存器映射或统计计数器。
// 读写自旋锁用法
rwlock_t my_rwlock;
rwlock_init(&my_rwlock);

// 读路径
read_lock(&my_rwlock);
// 多个读者可以同时进入
read_data = shared_table[index];
read_unlock(&my_rwlock);

// 写路径
write_lock(&my_rwlock);
// 写者独占,阻塞所有读者和其他写者
shared_table[index] = new_value;
write_unlock(&my_rwlock);
避坑提示:读写锁存在“写者饥饿”问题——如果读者持续不断,写者可能永远无法获得锁。在实时性要求高的系统中,应谨慎使用读写锁,或考虑使用RCU作为替代。
读写锁并发访问模型 共享资源 读者1 读者2 读者3 写者(等待中) 说明:读者1、2、3可以同时读取共享资源;写者必须等待所有读者释放锁后才能写入 写者持有锁时,所有读者和其他写者都必须等待

1.4 RCU锁(Read-Copy-Update)

RCU是Linux内核中最高性能的同步机制之一,专为读多写极少的场景设计。读者不需要获取任何锁,直接读取数据;写者通过“复制-修改-发布”的方式更新数据,并在所有旧读者退出后回收旧数据。

特性 说明
读者开销 几乎为零(无锁、无原子操作、无内存屏障)
写者开销 较高(需要复制数据、等待宽限期)
上下文限制 读者可在中断上下文使用;写者不能在中断上下文等待宽限期
适用场景 读频率极高、写频率极低的数据结构
适用场景:网络路由表、文件系统dentry缓存、设备链表遍历。在驱动中,RCU常用于保护热插拔设备列表、中断处理函数中访问的全局数据结构。
// RCU读者端(在中断上下文也可使用)
rcu_read_lock();
// 读者不需要获取锁,直接读取指针
struct device *dev = rcu_dereference(global_dev_list);
if (dev)
    process_device(dev);
rcu_read_unlock();

// RCU写者端
struct device *new_dev = kmalloc(sizeof(*new_dev), GFP_KERNEL);
// 复制并修改
*new_dev = *old_dev;
new_dev->status = DEVICE_ACTIVE;
// 发布新指针
rcu_assign_pointer(global_dev_list, new_dev);
// 等待所有旧读者退出后,再释放旧数据
synchronize_rcu();
kfree(old_dev);
避坑提示:在RCU读者临界区(rcu_read_lock() 与 rcu_read_unlock() 之间)不能睡眠、不能阻塞、不能调用可能导致调度的函数。写者端的 synchronize_rcu() 会阻塞直到所有读者退出,不能在中断上下文调用。
RCU更新机制:宽限期(Grace Period)示意图 时间 读者1(读旧数据) 读者2(读旧数据) 读者3(读旧数据) 写者复制数据 发布新指针 synchronize_rcu() 等待 释放旧数据 宽限期(Grace Period):等待所有旧读者退出 说明:写者发布新指针后,必须等待所有正在读取旧数据的读者退出,才能安全释放旧数据

1.5 四种锁机制对比总结

锁类型 等待方式 上下文限制 读者并发 临界区长度 典型开销
自旋锁 忙等待 任意上下文 独占 极短 低(CPU空转)
互斥锁 睡眠等待 仅进程上下文 独占 可长可短 中(上下文切换)
读写锁 忙等/睡眠 取决于变体 多读者 短/中 中
RCU 无等待(读者) 读者任意上下文 无限读者 读者无限制 极低(读者)
选型建议:
  • 中断上下文 + 短临界区 → 自旋锁
  • 进程上下文 + 可能睡眠 → 互斥锁
  • 读多写少 + 需要简单实现 → 读写锁
  • 读极多写极少 + 极致性能 → RCU