2. 读写锁(RW Lock)原理

读写锁是内核中解决“读多写少”场景的核心同步原语。其设计哲学在于:允许多个读者同时进入临界区,但写者必须独占访问。本章深入剖析读写锁的设计哲学、公平性策略以及自旋与睡眠两种实现路径。

2.1 读写锁的设计哲学

读写锁的核心思想来源于对现实世界“读操作不修改数据”这一事实的观察。当多个线程同时读取共享数据时,它们之间不会产生竞态条件;只有写操作才会破坏数据一致性。因此,读写锁提供了两种不同的锁模式:

  • 读模式(共享锁):任意数量的读者可以同时持有读锁。
  • 写模式(独占锁):只有一个写者可以持有写锁,且写锁与所有读锁互斥。
设计哲学要点:读写锁通过分离“读”与“写”的语义,在保证数据一致性的前提下,最大化读操作的并发度。这是对传统互斥锁“一刀切”独占模式的重大改进。

下图展示了读写锁的基本状态转换与并发模型:

读写锁状态与并发模型 未锁定 读锁定 写锁定 读锁获取 写锁获取 读锁释放 写锁释放 并发读者组(读锁可同时持有) 读者 1 读者 2 读者 3 读者 N 写者必须等待所有读者释放锁后才能进入

2.2 读者与写者的公平性

公平性是读写锁设计中最为棘手的挑战。不同的公平性策略会直接影响系统的吞吐量和延迟分布。内核中常见的公平性策略包括:

策略 描述 优点 缺点
读者优先 只要存在至少一个读者持有锁,后续读者可直接进入;写者必须等待所有读者释放。 读操作延迟低,读密集型场景吞吐量高。 写者可能被无限期阻塞(写者饥饿)。
写者优先 一旦有写者等待,后续读者必须排队等待,直到写者完成。 写者延迟有上限,避免写者饥饿。 读操作延迟增加,读并发度下降。
公平排队 按照请求到达顺序排队,读者和写者依次获得锁。 严格公平,无饥饿。 实现复杂,可能降低整体吞吐量。
内核实践注意:Linux 内核中的 rwlock_t 默认采用读者优先策略。这意味着在极端情况下,持续不断的读者流可能导致写者永远无法获得锁。对于需要写者及时响应的场景(如实时系统),应谨慎使用。

下图对比了读者优先与写者优先两种策略下的锁获取流程:

读者优先 vs 写者优先 流程对比 读者优先策略 写者到达,锁被读者持有 新读者到达,直接获取读锁 更多读者持续进入 写者无限期等待(饥饿) 写者优先策略 写者到达,锁被读者持有 新读者到达,被阻塞排队 写者等待现有读者释放 写者获取锁,读者继续等待

2.3 自旋与睡眠

读写锁的实现方式决定了等待线程的行为模式。内核提供了两种主要的实现变体:自旋读写锁和睡眠读写锁。

2.3.1 自旋读写锁(rwlock_t)

rwlock_t 是 Linux 内核中最基础的读写锁实现。当锁不可用时,等待的读者或写者会在 CPU 上忙等(自旋),不断检查锁状态。其核心 API 如下:

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

// 读者路径
read_lock(&my_rwlock);   // 获取读锁(自旋等待)
// 读取共享数据...
read_unlock(&my_rwlock); // 释放读锁

// 写者路径
write_lock(&my_rwlock);  // 获取写锁(自旋等待)
// 修改共享数据...
write_unlock(&my_rwlock);// 释放写锁
自旋锁适用场景:临界区执行时间极短(通常只有几条指令),且持有锁的线程不会被调度出去。自旋锁避免了线程切换的开销,但会浪费 CPU 周期。

2.3.2 睡眠读写锁(rwsem)

当临界区执行时间较长或可能阻塞时,自旋等待会严重浪费 CPU 资源。此时应使用信号量风格的读写锁——struct rw_semaphore。等待的线程会睡眠并让出 CPU,直到锁可用时被唤醒。

// 定义并初始化读写信号量
struct rw_semaphore my_rwsem;
init_rwsem(&my_rwsem);

// 读者路径
down_read(&my_rwsem);   // 获取读锁(可能睡眠)
// 读取共享数据...
up_read(&my_rwsem);     // 释放读锁

// 写者路径
down_write(&my_rwsem);  // 获取写锁(可能睡眠)
// 修改共享数据...
up_write(&my_rwsem);    // 释放写锁
关键区别:自旋读写锁(rwlock_t)不能在持有锁时睡眠或调用可能睡眠的函数;而睡眠读写锁(rwsem)允许持有者在临界区内阻塞。错误地在自旋锁临界区内睡眠会导致内核死锁。

2.3.3 自旋 vs 睡眠:选择决策树

下表总结了两种实现的核心差异,帮助开发者在实际场景中做出正确选择:

特性 自旋读写锁(rwlock_t) 睡眠读写锁(rwsem)
等待行为 CPU 忙等(自旋) 线程睡眠,让出 CPU
临界区时长 极短(微秒级) 较长(毫秒级或更长)
上下文要求 可在中断上下文使用 只能在进程上下文使用
CPU 开销 等待时占用 CPU 等待时释放 CPU
公平性 读者优先(默认) 写者优先(默认)
适用场景 快速路径、频繁访问的短临界区 文件系统、块设备等可能阻塞的路径

下图展示了自旋与睡眠两种等待模式在锁竞争时的行为差异:

自旋等待 vs 睡眠等待 行为对比 自旋等待(rwlock_t) 线程 A 持有锁(临界区) 线程 B 自旋检查锁状态(占用 CPU) 线程 B 继续自旋(占用 CPU) 线程 A 释放锁,线程 B 获取锁 CPU 被无效占用,但切换延迟低 睡眠等待(rwsem) 线程 A 持有锁(临界区) 线程 B 睡眠,让出 CPU 其他线程使用 CPU 执行任务 线程 A 释放锁,唤醒线程 B CPU 利用率高,但切换开销大

2.4 读写锁的局限性

尽管读写锁在“读多写少”场景下表现出色,但它并非万能。在实际使用中,开发者需要警惕以下问题:

  • 读者优先导致的写者饥饿:在高并发读场景下,写者可能长时间无法获得锁。
  • 缓存行 bouncing:多个读者频繁修改锁的计数器,导致缓存一致性协议产生大量开销。
  • 不可递归:标准的读写锁不支持递归获取,同一线程重复获取读锁可能导致死锁。
  • 优先级反转:低优先级读者持有锁时,高优先级写者可能被阻塞。