3. 读写锁API详解:rwlock_t、read_lock()、write_lock()、read_unlock()、write_unlock()

读写锁(rwlock)是Linux内核中一种重要的同步机制,它允许多个读者同时访问共享资源,但写者必须独占访问。本章节将深入剖析读写锁的核心API,包括数据结构定义、锁操作函数以及使用注意事项,帮助你在内核编程中正确、高效地使用读写锁。

3.1 核心数据结构:rwlock_t

rwlock_t 是读写锁的类型定义,在 <linux/rwlock.h> 中声明。它是一个结构体,内部维护了锁的状态信息,用于区分当前是读锁定还是写锁定状态。

// 定义并初始化一个读写锁
rwlock_t my_rwlock = __RW_LOCK_UNLOCKED(my_rwlock);

// 或者使用宏在运行时初始化
rwlock_t my_rwlock;
rwlock_init(&my_rwlock);
关键点: rwlock_t 是一个轻量级的锁结构,不包含等待队列信息。当写者无法获取锁时,它会自旋等待,因此读写锁适用于临界区执行时间较短的场景。

3.2 读锁操作:read_lock() / read_unlock()

读锁用于保护共享资源的读取操作。多个读者可以同时持有读锁,不会相互阻塞。读锁的获取和释放必须成对出现。

read_lock(&my_rwlock);
// 临界区:读取共享数据
// 例如:int val = shared_data;
read_unlock(&my_rwlock);
读锁行为特征:
  • 并发性: 任意数量的读者可以同时持有读锁。
  • 写者阻塞: 只要有读者持有锁,写者就无法获取写锁,必须自旋等待。
  • 不可递归: 同一个执行上下文不能递归获取读锁,否则可能导致死锁。

3.3 写锁操作:write_lock() / write_unlock()

写锁用于保护共享资源的修改操作。写者必须独占锁,即写锁与任何其他读锁或写锁都互斥。

write_lock(&my_rwlock);
// 临界区:修改共享数据
// 例如:shared_data = new_value;
write_unlock(&my_rwlock);
写锁注意事项:
  • 独占性: 写锁获取时,必须等待所有读者释放读锁,且后续读者会被阻塞。
  • 优先级反转: 如果读者持续持有锁,写者可能长时间无法获取锁,导致写者饥饿。
  • 不可在中断上下文使用: 标准 write_lock() 会调用 spin_lock(),可能引起睡眠,不适合中断上下文。

3.4 读写锁操作流程与状态转换

下面的SVG图展示了读写锁的核心状态转换与操作流程,帮助理解读者和写者之间的互斥关系。

读写锁状态转换与操作流程 读写锁状态转换与操作流程 未锁定状态 读锁定状态 多个读者可同时进入 写锁定状态 独占访问,互斥所有锁 read_lock() write_lock() read_unlock() write_unlock() 写者阻塞(等待读者释放) 读者阻塞(等待写者释放) 读写锁互斥规则 • 读锁与读锁:兼容(多个读者可同时持有) • 读锁与写锁:互斥(写者必须等待所有读者释放) • 写锁与写锁:互斥(写者之间不能同时访问)

3.5 读写锁API对比总结

下表汇总了读写锁核心API的功能、使用场景和注意事项,方便快速查阅。

API函数 功能描述 使用场景 注意事项
read_lock() 获取读锁,允许多个读者并发 读取共享数据,不修改 不可递归;不可在中断上下文使用
read_unlock() 释放读锁 与 read_lock() 配对 必须在同一执行上下文中调用
write_lock() 获取写锁,独占访问 修改共享数据 可能引起写者饥饿;不可在中断上下文使用
write_unlock() 释放写锁 与 write_lock() 配对 必须在同一执行上下文中调用
rwlock_init() 动态初始化读写锁 运行时初始化锁变量 确保在使用前调用

3.6 读写锁使用示例:保护全局计数器

以下代码展示了如何使用读写锁保护一个全局计数器,实现多读者并发读取、写者独占更新的模式。

#include <linux/rwlock.h>

static rwlock_t counter_lock = __RW_LOCK_UNLOCKED(counter_lock);
static int global_counter = 0;

// 读者函数:读取计数器值
int read_counter(void)
{
    int val;
    read_lock(&counter_lock);
    val = global_counter;
    read_unlock(&counter_lock);
    return val;
}

// 写者函数:更新计数器值
void write_counter(int new_val)
{
    write_lock(&counter_lock);
    global_counter = new_val;
    write_unlock(&counter_lock);
}
性能提示: 读写锁在读多写少的场景下性能优异,因为读者之间不会相互阻塞。但如果写操作频繁,写者可能长时间无法获取锁,导致性能下降。此时可以考虑使用RCU(Read-Copy Update)机制作为替代方案。

3.7 常见错误与避坑指南

  • 忘记释放锁: 在错误路径或提前返回时未调用 read_unlock() 或 write_unlock(),导致死锁。
  • 递归获取读锁: 同一个执行上下文再次调用 read_lock(),可能引起自旋死锁。
  • 在中断上下文使用标准读写锁: 标准读写锁会调用 spin_lock(),可能引起睡眠。应使用 read_lock_irqsave() 等变体。
  • 读者持有锁时间过长: 导致写者饥饿,影响系统整体性能。
中断上下文专用API: 如果需要在中断处理程序中使用读写锁,请使用 read_lock_irqsave() / read_unlock_irqrestore() 和 write_lock_irqsave() / write_unlock_irqrestore() 变体,它们会保存和恢复中断状态,避免死锁。

通过本章节的学习,你应该已经掌握了读写锁的核心API及其使用方法。在实际内核编程中,请根据共享资源的访问模式(读多写少 vs 写多读少)和上下文环境(进程上下文 vs 中断上下文)选择合适的锁机制。