2. 内核读写锁API

读写锁(rwlock)是 Linux 内核中专门为“多读少写”场景设计的同步原语。与自旋锁不同,读写锁允许多个读者同时持有锁,但写者必须独占访问。本章将深入剖析 rwlock_t 数据结构、初始化及核心操作接口,帮助你在内核模块中正确使用读写锁。

核心思想:读写锁通过区分读/写操作,在读者远多于写者的场景下大幅提升并发性能。读者之间不互斥,写者与所有读者、写者之间互斥。

2.1 rwlock_t 数据结构

rwlock_t 是读写锁的核心类型,定义在 <linux/rwlock_types.h> 中。其底层实现依赖于架构相关的原子操作和内存屏障。

// 内核源码:include/linux/rwlock_types.h
typedef struct {
    arch_rwlock_t raw_lock;
#ifdef CONFIG_DEBUG_SPINLOCK
    unsigned int magic;
    unsigned int owner_cpu;
    void *owner;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
    struct lockdep_map dep_map;
#endif
} rwlock_t;
字段 类型 说明
raw_lock arch_rwlock_t 架构相关的底层锁实现,x86 下通常基于 qrwlock(排队读写锁)
magic unsigned int 调试魔数,仅在 CONFIG_DEBUG_SPINLOCK 启用时存在
owner_cpu unsigned int 持有锁的 CPU 编号,用于调试检测死锁
owner void * 持有锁的进程/线程指针,调试用
dep_map struct lockdep_map 锁依赖映射,用于内核锁验证(lockdep)
架构差异:在 x86 架构上,arch_rwlock_t 实际为 struct qrwlock,采用排队机制避免写者饥饿。ARM64 则使用 struct qrwlock 或基于 arch_spinlock_t 的自定义实现。
rwlock_t 数据结构层次 rwlock_t arch_rwlock_t magic / owner_cpu / owner lockdep_map 说明:rwlock_t 封装了架构相关的底层锁实现, 并通过条件编译添加调试和锁依赖跟踪能力。

2.2 读写锁初始化:rwlock_init

rwlock_init 是一个宏,用于在运行时初始化读写锁。它确保锁处于未锁定状态,并正确设置调试和锁依赖字段。

// 使用方式
rwlock_t my_lock;
rwlock_init(&my_lock);

// 宏定义(简化版)
#define rwlock_init(lock) \
    do { \
        *(lock) = __RW_LOCK_UNLOCKED(lock); \
    } while (0)
初始化方式 适用场景 示例
rwlock_init() 动态初始化(函数内、结构体成员) rwlock_init(&lock);
DEFINE_RWLOCK() 静态全局/局部变量定义时初始化 DEFINE_RWLOCK(my_lock);
__RW_LOCK_UNLOCKED() 编译时初始化(用于静态分配) rwlock_t lock = __RW_LOCK_UNLOCKED(lock);
注意:读写锁初始化后,必须确保在第一次使用前完成。未初始化的锁会导致不可预测的行为,包括死锁和数据损坏。

2.3 读锁操作:read_lock / read_unlock

读锁操作允许多个读者同时进入临界区。当没有写者持有锁时,读者可以无等待获取锁;如果有写者等待,读者可能需要自旋等待。

// 读锁获取与释放
read_lock(&my_lock);
// 读临界区:多个读者可以同时执行此处代码
// 读取共享数据...
read_unlock(&my_lock);
接口 行为 阻塞条件
read_lock() 获取读锁,如果写者持有则自旋等待 有写者持有锁或写者等待队列非空
read_unlock() 释放读锁,唤醒等待的写者(如果适用) 无阻塞
read_trylock() 尝试获取读锁,失败立即返回 0 不阻塞,返回 0 表示获取失败
性能关键:在 x86 上,read_lock 使用原子递增操作,通常比写锁快一个数量级。读者之间完全并行,不会触发缓存行乒乓(cache line bouncing)。

2.4 写锁操作:write_lock / write_unlock

写锁是独占锁,写者必须等待所有读者释放锁后才能进入临界区。写者之间也互斥,同一时刻只能有一个写者。

// 写锁获取与释放
write_lock(&my_lock);
// 写临界区:独占访问,修改共享数据
// 修改数据...
write_unlock(&my_lock);
接口 行为 阻塞条件
write_lock() 获取写锁,等待所有读者和写者释放 有读者或写者持有锁
write_unlock() 释放写锁,唤醒所有等待的读者和写者 无阻塞
write_trylock() 尝试获取写锁,失败立即返回 0 不阻塞,返回 0 表示获取失败
写者饥饿问题:在读者密集的场景下,写者可能长时间无法获取锁。Linux 内核通过 qrwlock(排队读写锁)缓解此问题,确保写者最终能获得锁。
读写锁操作流程 锁操作请求 读操作? read_lock() 读临界区 read_unlock() write_lock() 写临界区 write_unlock() 是 否(写)

2.5 接口对比与选择指南

在实际内核编程中,选择合适的读写锁接口至关重要。下表总结了不同接口的适用场景和性能特征:

接口 并发度 自旋开销 适用场景
read_lock/unlock 高(多读者并行) 低(仅写者等待时自旋) 读多写少,读者远多于写者
write_lock/unlock 低(独占) 高(等待所有读者释放) 写操作频繁或需要原子更新
read_trylock 高 无(立即返回) 非阻塞读场景,如中断上下文
write_trylock 低 无(立即返回) 非阻塞写场景,如定时器回调
最佳实践:在中断上下文或软中断中,应使用 read_lock_irqsave / write_lock_irqsave 变体,它们会保存并禁用本地中断,避免死锁。

2.6 常见错误与调试

  • 递归读锁:同一个读者不能递归获取读锁,否则可能导致死锁。如果需要递归,考虑使用 rcu_read_lock 或 mutex。
  • 读锁升级为写锁:持有读锁时尝试获取写锁会导致死锁。必须释放读锁后再获取写锁。
  • 中断上下文使用:在中断处理函数中必须使用 _irqsave 变体,否则可能因中断抢占导致死锁。
  • 锁顺序反转:多个读写锁必须按固定顺序获取,否则可能触发 lockdep 警告。
// 错误示例:读锁升级写锁导致死锁
read_lock(&lock);
// ... 读取数据 ...
write_lock(&lock);  // 死锁!当前线程已持有读锁
// ... 修改数据 ...
write_unlock(&lock);
read_unlock(&lock);

// 正确做法:先释放读锁,再获取写锁
read_lock(&lock);
// ... 读取数据 ...
read_unlock(&lock);
write_lock(&lock);
// ... 修改数据 ...
write_unlock(&lock);
调试工具:启用 CONFIG_DEBUG_LOCK_ALLOC 和 CONFIG_PROVE_LOCKING 后,内核会在锁操作时自动检测死锁、锁顺序反转等问题,并在控制台输出详细警告。