4. 读写锁实现机制:基于自旋锁的实现、读者计数、写者等待队列

读写锁(rwlock)是内核中一种细粒度的同步原语,它允许多个读者同时持有锁,但写者必须独占访问。本章深入剖析 Linux 内核中读写锁的经典实现,重点分析其基于自旋锁的底层构建、读者计数的原子管理以及写者等待队列的调度逻辑。

4.1 读写锁的核心数据结构

在 Linux 内核中,读写锁的核心数据结构定义在 <linux/rwlock.h> 中。其本质是一个 arch_spinlock_t 类型的字段,通过编码不同的位状态来区分读者和写者。

字段 类型 说明
lock arch_spinlock_t 底层自旋锁,用于保护读写锁内部状态的原子操作
counter u32 32 位计数器,高位表示写者锁定标志,低位表示读者计数
wait_lock spinlock_t 写者等待队列的自旋锁保护
wait_list list_head 写者等待队列链表头
关键设计点:读写锁通过一个 32 位计数器同时编码读者数量和写者标志,避免了额外的内存开销。写者标志通常使用最高位(bit 31),读者计数使用低 31 位。

4.2 基于自旋锁的底层实现

读写锁的底层操作依赖于自旋锁来保证对 counter 字段的原子修改。以下是一个典型的读写锁获取与释放的伪代码实现:

// 读者获取锁
void read_lock(rwlock_t *lock) {
    unsigned long flags;
    spin_lock_irqsave(&lock->wait_lock, flags);
    // 检查是否有写者持有锁或写者等待
    if (lock->counter & WRITER_MASK) {
        // 有写者,读者需要自旋等待
        do {
            spin_unlock_irqrestore(&lock->wait_lock, flags);
            while (lock->counter & WRITER_MASK)
                cpu_relax();
            spin_lock_irqsave(&lock->wait_lock, flags);
        } while (lock->counter & WRITER_MASK);
    }
    // 增加读者计数
    lock->counter++;
    spin_unlock_irqrestore(&lock->wait_lock, flags);
}

// 写者获取锁
void write_lock(rwlock_t *lock) {
    unsigned long flags;
    spin_lock_irqsave(&lock->wait_lock, flags);
    // 等待所有读者释放,且没有其他写者
    while (lock->counter != 0) {
        // 将当前写者加入等待队列
        list_add_tail(&current->write_entry, &lock->wait_list);
        spin_unlock_irqrestore(&lock->wait_lock, flags);
        schedule(); // 让出 CPU
        spin_lock_irqsave(&lock->wait_lock, flags);
        list_del(&current->write_entry);
    }
    // 设置写者标志
    lock->counter = WRITER_MASK;
    spin_unlock_irqrestore(&lock->wait_lock, flags);
}
性能陷阱:读者在自旋等待期间会持续占用 CPU,如果写者持有锁时间较长,会导致大量读者空转。现代内核引入了 qrwlock(排队读写锁)来缓解这一问题,通过 FIFO 队列避免读者饿死写者。

4.3 读者计数机制

读者计数是读写锁实现的核心。内核使用原子操作来管理计数器的增减,确保并发读者之间的正确性。

  • 计数上限:低 31 位最多支持 2^31 - 1 个并发读者,实际场景中远小于此值。
  • 溢出保护:当读者计数达到最大值时,新的读者获取锁会失败或阻塞,防止计数器回绕。
  • 读者释放:释放锁时原子递减计数器,并检查是否有写者在等待队列中。
读者计数状态转换:
  • 空闲状态:counter == 0
  • 读者持有:counter > 0 且 WRITER_FLAG 未设置
  • 写者持有:counter == WRITER_MASK
  • 写者等待:counter 不为 0,且等待队列非空

4.4 写者等待队列

当写者无法立即获取锁时,它会被加入一个等待队列。这个队列保证了写者之间的公平性,并防止写者被读者无限期饿死。

队列操作 触发条件 行为
入队 写者获取锁时发现 counter != 0 将当前进程的等待节点添加到 wait_list 尾部
出队 写者成功获取锁或放弃等待 从 wait_list 中删除节点
唤醒 最后一个读者释放锁 唤醒等待队列头部的写者
超时 写者等待超过指定时间 从队列中移除并返回错误

4.5 读写锁状态机与流程

下面的 SVG 图展示了读写锁从空闲到读者/写者持有,再到写者等待的完整状态转换流程。

读写锁状态转换与等待队列流程 空闲 (counter=0) 读者持有 (counter>0) 写者持有 (WRITER_MASK) 写者等待队列 (wait_list 非空, 写者进程阻塞) 说明: 1. 读者可并发进入,仅增加 counter 值。 2. 写者必须等待 counter 归零,且入队等待队列。 read_lock() write_lock() read_unlock() write_unlock() 写者尝试获取锁 写者冲突 counter 归零, 唤醒 写者放弃/超时

4.6 读者与写者的公平性权衡

经典的读写锁实现存在一个显著的公平性问题:写者可能被读者饿死。当读者持续持有锁时,写者将无限期等待。现代内核通过以下机制改善公平性:

  • 写者优先策略:当有写者在等待队列中时,新来的读者将被阻塞,直到写者完成操作。
  • 排队读写锁 (qrwlock):使用 FIFO 队列管理所有等待者,读者和写者按到达顺序获取锁。
  • 自适应自旋:写者在自旋等待时,根据读者数量动态调整自旋次数,避免过度空转。
性能对比总结:
场景 经典 rwlock qrwlock
读者密集 高吞吐,低延迟 略低于经典实现
写者密集 写者可能饿死 公平,写者延迟可控
混合负载 公平性差 公平性好
内存开销 低 中等(需要队列节点)

4.7 实现中的关键注意事项

  1. 中断上下文:在中断处理程序中使用读写锁时,必须使用 read_lock_irqsave() / write_lock_irqsave() 变体,以避免死锁。
  2. 递归读者:同一个读者不能递归获取锁,否则会导致计数器错误或死锁。
  3. 写者优先级反转:当低优先级写者等待高优先级读者时,可能发生优先级反转,需结合 RT 调度策略处理。
  4. 内存屏障:读写锁的实现隐式包含了必要的内存屏障,确保读者看到的数据一致性。
常见错误:在读者上下文中调用 write_lock() 会导致死锁,因为写者会自旋等待读者释放,而读者自己就是读者。务必区分读/写路径。