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())。这会导致内核崩溃或死锁。
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”。
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.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() 会阻塞直到所有读者退出,不能在中断上下文调用。
1.5 四种锁机制对比总结
| 锁类型 | 等待方式 | 上下文限制 | 读者并发 | 临界区长度 | 典型开销 |
|---|---|---|---|---|---|
| 自旋锁 | 忙等待 | 任意上下文 | 独占 | 极短 | 低(CPU空转) |
| 互斥锁 | 睡眠等待 | 仅进程上下文 | 独占 | 可长可短 | 中(上下文切换) |
| 读写锁 | 忙等/睡眠 | 取决于变体 | 多读者 | 短/中 | 中 |
| RCU | 无等待(读者) | 读者任意上下文 | 无限读者 | 读者无限制 | 极低(读者) |
选型建议:
- 中断上下文 + 短临界区 → 自旋锁
- 进程上下文 + 可能睡眠 → 互斥锁
- 读多写少 + 需要简单实现 → 读写锁
- 读极多写极少 + 极致性能 → RCU