4. 读写锁API详解
读写锁(rwlock)是内核中解决“读多写少”场景下并发竞态的核心同步原语。与自旋锁不同,读写锁允许多个读者同时进入临界区,但写者必须独占访问。本章将深入剖析 rwlock_t 数据结构的内部布局,并逐一讲解 read_lock、write_lock、read_unlock、write_unlock 等 API 的语义、使用规范与底层实现机制。
4.1 rwlock_t 数据结构与初始化
rwlock_t 是读写锁的核心类型,定义在 <linux/rwlock_types.h> 中。其本质是一个基于 arch_spinlock_t 的封装,在大多数架构(如 x86、ARM64)上通过一个 32 位原子变量来维护锁状态。
| 字段/宏 | 类型 | 说明 |
|---|---|---|
raw_lock |
arch_spinlock_t |
架构相关的底层自旋锁,用于保护读写锁内部计数器的原子操作 |
DEFINE_RWLOCK(name) |
静态初始化宏 | 编译时定义并初始化一个全局读写锁,锁状态为未锁定 |
rwlock_init(lock) |
动态初始化函数 | 运行时初始化动态分配的读写锁,将锁状态清零 |
raw_lock 的低 24 位用于记录读者计数,高 8 位用于写者标志和等待位。这种位域设计使得读者加锁/解锁操作通常只需要一条原子指令(如 LOCK DEC 或 LOCK INC),性能极高。
// 静态初始化示例
DEFINE_RWLOCK(my_rwlock);
// 动态初始化示例
rwlock_t *lock;
lock = kmalloc(sizeof(rwlock_t), GFP_KERNEL);
rwlock_init(lock);
4.2 读者路径:read_lock / read_unlock
读者 API 允许多个执行上下文同时持有读锁。只要没有写者持有锁或正在等待,读者就可以无阻塞地进入临界区。
4.2.1 read_lock()
void read_lock(rwlock_t *lock); — 尝试获取读锁。如果当前没有写者持有锁,读者计数递增,调用者立即返回;否则,读者自旋等待写者释放锁。
4.2.2 read_unlock()
void read_unlock(rwlock_t *lock); — 释放读锁。原子地递减读者计数,并唤醒等待的写者(如果有)。
copy_from_user、kmalloc、mutex_lock)。因为读者持有锁时,写者会自旋等待,若读者睡眠,将导致写者长时间空转,严重降低系统吞吐量。
// 读者路径典型用法
read_lock(&my_rwlock);
/* 只读访问共享数据结构,例如遍历链表、读取计数器 */
if (list_empty(&head)) {
pr_info("链表为空\n");
}
read_unlock(&my_rwlock);
4.3 写者路径:write_lock / write_unlock
写者 API 提供独占访问权限。写者必须等待所有读者释放锁,且后续读者必须等待写者完成。
4.3.1 write_lock()
void write_lock(rwlock_t *lock); — 尝试获取写锁。如果锁完全空闲(无读者、无写者),调用者获取锁并设置写者标志;否则,自旋等待直到锁被完全释放。
4.3.2 write_unlock()
void write_unlock(rwlock_t *lock); — 释放写锁。清除写者标志,并唤醒所有等待的读者和写者。
// 写者路径典型用法
write_lock(&my_rwlock);
/* 修改共享数据结构,例如插入/删除链表节点 */
list_add_tail(&new_node, &head);
write_unlock(&my_rwlock);
4.4 读写锁状态转换与竞争流程
下图展示了读写锁在读者、写者并发访问下的状态转换与等待关系。理解这个流程是进行性能调优的基础。
4.5 读者与写者 API 对比
| API | 语义 | 并发性 | 临界区限制 | 典型使用场景 |
|---|---|---|---|---|
read_lock() |
获取读锁,允许多读者 | 高(多核并行读) | 不可睡眠、不可调用写锁 | 遍历链表、读取统计计数、查询状态 |
read_unlock() |
释放读锁 | — | 必须与 read_lock 配对 | 读者临界区出口 |
write_lock() |
获取写锁,独占访问 | 低(串行化) | 不可睡眠、不可递归 | 插入/删除节点、修改配置、更新指针 |
write_unlock() |
释放写锁 | — | 必须与 write_lock 配对 | 写者临界区出口 |
4.6 中断上下文中的变体:_irq / _irqsave 后缀
读写锁 API 提供了中断安全变体,用于在中断处理程序或下半部(softirq/tasklet)中保护共享数据。
read_lock_irq()/read_unlock_irq():获取读锁的同时禁用本地 CPU 中断,释放时重新启用。write_lock_irq()/write_unlock_irq():获取写锁的同时禁用本地 CPU 中断。read_lock_irqsave()/read_unlock_irqrestore():保存当前中断状态,获取读锁;释放时恢复原中断状态。write_lock_irqsave()/write_unlock_irqrestore():保存当前中断状态,获取写锁;释放时恢复原中断状态。
_irqsave 变体。使用 _irq 变体时,调用者必须确保调用前本地中断处于开启状态,否则释放锁时会错误地开启中断,导致潜在的中断丢失。
// 中断上下文安全示例
unsigned long flags;
read_lock_irqsave(&my_rwlock, flags);
/* 在中断可能发生的情况下安全读取 */
if (device->status & DEVICE_READY) {
process_data(device);
}
read_unlock_irqrestore(&my_rwlock, flags);
4.7 性能调优要点
- 读者临界区应尽可能短:写者会自旋等待所有读者释放锁,过长的读者临界区会显著增加写者延迟。
- 避免读者与写者频繁切换:如果读写操作交替非常频繁(如每 10 微秒切换一次),读写锁的开销可能超过自旋锁。此时应评估是否改用
spinlock或RCU。 - 使用
rwlock_t的原子性:在 x86 上,read_lock通常编译为LOCK DEC指令,read_unlock编译为LOCK INC,缓存行乒乓(cache line bouncing)是主要性能瓶颈。在多 socket 系统中,应尽量将读者数据分散到不同 NUMA 节点。 - 优先使用
_irqsave而非_irq:_irqsave虽然多了一条保存/恢复指令,但避免了因中断状态假设错误导致的难以调试的 bug。
msleep(10) 或 copy_from_user,此时所有读者将被长时间阻塞。这种场景应使用 RCU 或 mutex + 读者副本模式。