2、读写锁基础:读写自旋锁(rwlock_t)、读写信号量(struct rw_semaphore)的API与使用场景

读写锁是内核中针对“读多写少”场景设计的同步原语。与普通自旋锁或互斥锁不同,读写锁允许多个读者同时进入临界区,但写者必须独占访问。这种机制能显著提升并发读性能,但若使用不当,极易引发死锁或优先级反转。

2.1 读写自旋锁(rwlock_t)

rwlock_t 是基于自旋的读写锁,适用于中断上下文或不可睡眠的临界区。读者与写者均通过自旋等待锁释放,不会导致进程睡眠。

2.1.1 核心API

API 说明 上下文要求
rwlock_init(lock) 动态初始化读写自旋锁 进程上下文
read_lock(lock) 获取读锁,自旋等待 任意上下文(含中断)
read_unlock(lock) 释放读锁 与获取锁相同上下文
write_lock(lock) 获取写锁,自旋等待 任意上下文(含中断)
write_unlock(lock) 释放写锁 与获取锁相同上下文
read_lock_irqsave(lock, flags) 获取读锁并保存中断状态 中断上下文
write_lock_irqsave(lock, flags) 获取写锁并保存中断状态 中断上下文
关键区别: read_lock() 与 write_lock() 在中断上下文中使用时,必须配合 _irqsave 变体,否则可能因中断抢占导致死锁。

2.1.2 典型使用场景

  • 全局配置表保护: 多个CPU核心并发读取配置项,写者极少(如模块参数更新)。
  • 设备状态标志: 中断处理程序读取设备状态,进程上下文偶尔修改状态。
  • 统计计数器: 读者频繁累加统计值,写者重置计数器。

2.1.3 代码示例

#include <linux/rwlock.h>

rwlock_t my_rwlock;
int shared_counter;

void reader_function(void)
{
    read_lock(&my_rwlock);
    pr_info("Counter value: %d\n", shared_counter);
    read_unlock(&my_rwlock);
}

void writer_function(void)
{
    write_lock(&my_rwlock);
    shared_counter++;
    write_unlock(&my_rwlock);
}
死锁陷阱: 写者不能嵌套获取读锁,读者也不能在持有读锁时尝试获取写锁。这种“锁升级”操作在 rwlock_t 中会导致死锁。

2.2 读写信号量(struct rw_semaphore)

struct rw_semaphore 是允许睡眠的读写锁,适用于进程上下文。读者获取锁时若不可用则进入睡眠等待,写者同样会睡眠。它不支持中断上下文。

2.2.1 核心API

API 说明 上下文要求
init_rwsem(sem) 初始化读写信号量 进程上下文
down_read(sem) 获取读锁,可能睡眠 进程上下文
up_read(sem) 释放读锁 进程上下文
down_write(sem) 获取写锁,可能睡眠 进程上下文
up_write(sem) 释放写锁 进程上下文
down_read_trylock(sem) 尝试获取读锁,不睡眠 进程上下文
down_write_trylock(sem) 尝试获取写锁,不睡眠 进程上下文

2.2.2 典型使用场景

  • 文件系统元数据: 多个进程并发读取目录项,写者执行创建/删除操作。
  • 内存管理页表: 读者遍历页表,写者修改映射关系。
  • 网络协议栈路由表: 读者查找路由,写者更新路由规则。

2.2.3 代码示例

#include <linux/rwsem.h>

struct rw_semaphore my_rwsem;
struct list_head my_list;

void reader_process(void)
{
    down_read(&my_rwsem);
    list_for_each_entry(item, &my_list, list) {
        pr_info("Item: %s\n", item->name);
    }
    up_read(&my_rwsem);
}

void writer_process(void)
{
    down_write(&my_rwsem);
    list_add_tail(&new_item->list, &my_list);
    up_write(&my_rwsem);
}
性能对比: rwlock_t 适用于短临界区(微秒级),rw_semaphore 适用于长临界区(毫秒级或可能睡眠)。错误选择会导致严重的性能退化。

2.3 读写锁选择决策树

读写锁选择决策树 临界区是否可睡眠? 不可睡眠 → rwlock_t 可睡眠 → rw_semaphore 中断上下文?用 _irqsave 变体 临界区极短(< 10μs) 进程上下文,可睡眠等待 临界区较长(ms级) ⚠ 注意:rwlock_t 不支持锁升级/降级 读者持有锁时不能获取写锁,否则死锁 rw_semaphore 支持写者优先,避免写者饥饿

2.4 读写锁与普通锁的对比

特性 普通自旋锁 rwlock_t 普通信号量 rw_semaphore
并发读者 不允许 允许 不允许 允许
睡眠能力 否 否 是 是
中断上下文 可(需irqsave) 可(需irqsave) 否 否
写者饥饿 不适用 可能(读者多时) 不适用 写者优先
锁升级/降级 不支持 不支持 不支持 不支持
适用临界区长度 极短(μs级) 短(μs级) 中长(ms级) 中长(ms级)

2.5 使用场景总结

工程实践建议:
  • 读远多于写(读占 > 90%): 优先考虑 rwlock_t 或 rw_semaphore,避免普通锁的串行化开销。
  • 临界区极短且不可睡眠: 使用 rwlock_t,注意中断上下文必须使用 _irqsave 变体。
  • 临界区可能触发内存分配或I/O: 必须使用 rw_semaphore,因为 rwlock_t 自旋等待期间不能睡眠。
  • 写者优先级要求高: rw_semaphore 内置写者优先机制,rwlock_t 需要额外处理。
死锁排查要点: 读写锁死锁通常源于锁顺序不一致、锁升级尝试、或中断上下文与进程上下文混用。在调试时,使用 lockdep 内核选项可自动检测这些违规模式。