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 读写锁选择决策树
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 内核选项可自动检测这些违规模式。