欢迎光临
我们一直在努力

Linux 读写信号量 rwsem 源码解析:读者并发读取与写者独占的公平性调度

Linux 读写信号量 rwsem 源码解析:读者并发读取与写者独占的公平性调度

封面信息图

在多核 Linux 系统中,面对“读多写少”的高并发共享数据结构(如进程虚拟地址空间管理结构体 mm_struct->mmap_lock、VFS 目录树缓存等),互斥锁(Mutex)会导致所有只读操作串行化,形成严重的性能瓶颈。

为了支持多个读者并发读取、同时保证写者的独占互斥,Linux 内核提供了读写信号量(Reader/Writer Semaphore,简称 rwsem)。

但传统的读写锁设计极易引发**“写者饥饿(Writer Starvation)”**——如果源源不断的读者持续发起读锁请求,写者可能被无限期阻塞挂起。现代 Linux 内核(Linux 5.x/6.x)在 struct rw_semaphore 中实现了一套极其精妙的基于原子计数与排队队列的公平性调度机制。


一、struct rw_semaphore 核心数据结构与原子计数编码

内核中 include/linux/rwsem.h 的核心结构体定义如下:

struct rw_semaphore {
atomic_long_t count; /* 复合原子状态计数器 */
atomic_long_t owner; /* 写者持有时指向写者 task_struct */
#ifdef CONFIG_RWSEM_SPIN_ON_OWNER
struct optimistic_spin_queue osq; /* MCS 乐观自旋锁队列 */
#endif
raw_spinlock_t wait_lock; /* 保护等待队列 wait_list 的自旋锁 */
struct list_head wait_list; /* 等待唤醒的读者/写者链表 */
#ifdef CONFIG_DEBUG_RWSEMAPHORE
void *magic;
#endif
};

count 字段(atomic_long_t)是整套机制的灵魂,它在 64 位体系下的位域布局如下:

63 8 7 6 5 4 3 2 1 0
+———————————————+—+—+—+—+—+—+—+—+
| Reader Count (当前并发读者计数值) | H | W | Reserved | L | W |
+———————————————+—+—+—+—+—+—+—+—+
│ │ │ │
│ │ │ └── RWSEM_WRITER_LOCKED (0x1)
│ │ └────── RWSEM_WRITER_LOCKED
│ └───────────────────────── RWSEM_FLAG_WAITERS (0x200)
└───────────────────────────── RWSEM_FLAG_HANDOFF (0x400)

  • Bit 0 (RWSEM_WRITER_LOCKED = 1):表示当前有写者持有了该锁。
  • Bit 8 (RWSEM_READER_BIAS = 0x100):读者计数的基本步长,每次 down_read 成功,count 便原子增加 0x100。高位累加即代表读者总数。
  • Bit 9 (RWSEM_FLAG_WAITERS = 0x200):等待队列中有正在排队的任务(读者或写者)。
  • Bit 10 (RWSEM_FLAG_HANDOFF = 0x400):开启顶端写者/读者强制移交,防止饥饿。

二、读锁与写锁路径源码剖析

1. 读者快速加锁路径(down_read)

/* kernel/locking/rwsem.c */
static inline bool __down_read_fastpath(struct rw_semaphore *sem)
{
/* 尝试直接原子累加读者步长 RWSEM_READER_BIAS (0x100) */
if (atomic_long_add_return_acquire(RWSEM_READER_BIAS, &sem->count) > 0)
return true;
return false;
}

如果在加锁前,count 没有写者锁定标志(Bit 0 为 0)且没有设置 WAITERS 标志,原子加法后结果必然大于 0,读者瞬间拿到锁,无需任何额外开销;若返回负值或包含等待标记,说明有写者持锁或有等待者,进入慢速路径 rwsem_down_read_slowpath。

2. 写者快速加锁路径(down_write)

static inline bool __down_write_fastpath(struct rw_semaphore *sem)
{
/* 仅当 count 严格为 0 (既无读者也无写者且无等待者) 时,CAS 设置为 RWSEM_WRITER_LOCKED */
if (atomic_long_cmpxchg_acquire(&sem->count, 0UL, RWSEM_WRITER_LOCKED) == 0UL) {
atomic_long_set(&sem->owner, (long)current);
return true;
}
return false;
}

写者要求必须在“绝对空闲”状态下才能命中 Fastpath。


三、公平性调度与防写者饥饿(Anti-Starvation)机制

传统读写锁最致命的缺陷是写者饥饿。Linux 内核是如何做到公平调度的?

时间轴演进:
[读者 A 持锁读取]
│
▼
[写者 B 尝试加锁] ──> 失败,进入等待队列,设置 count |= RWSEM_FLAG_WAITERS
│
▼
[读者 C 到达] ────> 发现 count 包含 WAITERS 标志!
尽管读者 A 仍在读取,读者 C 仍然【被禁止插队】,必须挂入等待队列
│
▼
[读者 A 释放 (up_read)] ──> 唤醒队首写者 B ──> 写者 B 获得独占权执行

核心调度防饥饿逻辑
  • 设置等待标志(RWSEM_FLAG_WAITERS):当写者进入排队后,会将原子变量打上 WAITERS 标记。后续新到达的读者在尝试 __down_read_fastpath 时,会因为该标记而无法进入快速通道,强制进入等待队列排队,从而切断了后续读者的“无限并发抢占”。
  • 强制移交标志(RWSEM_FLAG_HANDOFF):如果等待队列首部的写者已经等待超过了一定时间阈值,内核会在 count 中设置 HANDOFF 标记。此时即便锁被释放,其他任何新到达的线程都绝对无法抢占该锁,锁的所有权会被严格移交给队首等待者。

  • 四、读者批量唤醒(rwsem_wake)

    当写者完成操作调用 up_write 释放锁时,唤醒逻辑展现出极高的并发吞吐设计:

    static void rwsem_wake(struct rw_semaphore *sem)
    {
    struct rwsem_waiter *waiter;

    raw_spin_lock_irqsave(&sem->wait_lock, flags);
    waiter = list_first_entry_or_null(&sem->wait_list, struct rwsem_waiter, list);

    if (waiter->type == RWSEM_WAITING_FOR_WRITE) {
    /* 如果队首是写者,仅唤醒单体写者 */
    wake_up_process(waiter->task);
    } else {
    /* 如果队首是读者,级联连续唤醒所有紧随其后的读者! */
    list_for_each_entry_safe(waiter, next, &sem->wait_list, list) {
    if (waiter->type == RWSEM_WAITING_FOR_WRITE)
    break; // 遇到写者截断,保持公平顺序
    wake_up_process(waiter->task);
    }
    }
    raw_spin_unlock_irqrestore(&sem->wait_lock, flags);
    }

    等待队列拓扑:
    [Head] ──> [读者 1] ──> [读者 2] ──> [读者 3] ──> [写者 1] ──> [读者 4]
    └────── 批量唤醒 ──────┘ │
    写者阻断点 (停止唤醒)


    五、性能特征与工程选型建议

    锁机制适用并发模式读并发能力写者开销是否支持中断上下文
    struct mutex 读写频率均等、独占互斥 无 (单线程) 低 否 (不可在中断中使用)
    rw_semaphore 读远多于写、临界区较长 (可睡眠) 极高 (并发共享) 中等 (需排队等待读者清零) 否 (可睡眠锁)
    rwlock_t (自旋锁版) 读多写少、临界区极短 (纳秒级) 高 (自旋) 高 (自旋消耗 CPU) 是 (中断安全)
    RCU (Read-Copy-Update) 极致只读 (读写比 > 100:1) 零锁开销 较高 (需等待宽限期) 是 (无锁免睡眠)

    在内核模块或驱动开发中,如果共享数据结构需要支持并发读且临界区内可能涉及内存分配(kmalloc)、I/O 交互等可能导致睡眠的操作,rw_semaphore 是兼具高吞吐与公平性的最佳选择。

    赞(0)
    未经允许不得转载:171主机测评 » Linux 读写信号量 rwsem 源码解析:读者并发读取与写者独占的公平性调度
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址