Linux内核机制—rwsem-3-5.4上实现

基于 Linux-5.4

一、简介

下面基于你实际的 msm-5.4 分支讲解,而不是只描述上游抽象模型。核心代码位于:

msm-5.4/include/linux/rwsem.h
msm-5.4/kernel/locking/rwsem.c

 

1. rwsem 解决什么问题

rw_semaphore 是允许睡眠的读写锁:

(1) 多个读者可以同时持锁。
(2) 写者独占。
(3) 获取失败时任务可以进入调度器睡眠。
(4) 适合临界区较长、可能阻塞的场景。
(5) 不能用于中断、原子上下文或持有自旋锁时。

它与 rwlock_t 的主要区别是:

-----------------------------------------------------------------------------
项目            rwsem                            rwlock
-----------------------------------------------------------------------------
等待方式         可自旋后睡眠                      一直自旋
临界区           可较长、可调度                    必须非常短
使用上下文       进程上下文                        可用于原子/中断上下文
典型用途         mmap_sem、文件系统元数据           极短共享状态
-----------------------------------------------------------------------------

Linux 5.4 中的 mm->mmap_sem 就是一个 struct rw_semaphore。


2. 核心数据结构

struct rw_semaphore { //include/linux/rwsem.h
    atomic_long_t count;
    atomic_long_t owner;
#ifdef CONFIG_RWSEM_SPIN_ON_OWNER
    struct optimistic_spin_queue osq;
#endif
    raw_spinlock_t wait_lock;
    struct list_head wait_list;
    /* debug / lockdep / vendor fields */
};

成员介绍:

(1) count

rwsem 正确性的核心状态,编码:当前读者数量、是否有写者、是否有等待者、是否进入 handoff 模式。获取锁的快路径几乎只访问 count。


(2) owner

用于记录锁持有者和乐观自旋相关标志:

写锁:通常是准确的写者 task_struct 指针,低位可能带有标志位。
读锁:只保存最近(最后)一个读者的 task_struct 指针,低位可能带有标志位,可能已经过期。

它主要用于判断锁持有者是否正在 CPU 上运行。不用于决定锁的正确性,真正状态仍以 count 成员为准。

owner 低位还编码:

/* 这些是 owner 中的标志 */
#define RWSEM_READER_OWNED        (1UL << 0) //bit0
#define RWSEM_RD_NONSPINNABLE    (1UL << 1) //bit1
#define RWSEM_WR_NONSPINNABLE    (1UL << 2) //bit2
#define RWSEM_NONSPINNABLE    (RWSEM_RD_NONSPINNABLE | RWSEM_WR_NONSPINNABLE) //bit1-2
#define RWSEM_OWNER_FLAGS_MASK    (RWSEM_READER_OWNED | RWSEM_NONSPINNABLE) //bit0-2

owner 字段的注意事项:writer owner 比较可靠,但 reader owner 只能当启发式信息:多个 reader 同时持锁,owner 只存最近一个 reader,该 reader 可能已经 up_read()####,owner 指针仍可能暂时保留。
因此源码明确说明:reader owner 可能不是当前真实持锁者,只能“take it with a grain of salt”。owner主要回答:“当前值得自旋等待吗?”它不能回答:“锁现在是否真正空闲?”后者必须看 count。


(3) osq

Optimistic Spin Queue,类似 MCS 队列,用于串行化乐观自旋者,避免大量任务同时读取和争抢 owner cacheline。


(4) wait_lock

保护:wait_list、waiter 加入/删除、reader 批量授权、writer handoff 状态转换。这是一个 raw_spinlock_t,临界区必须很短。


(5) wait_list

真正睡眠等待者的 FIFO 链表,节点上挂的结构为:

struct rwsem_waiter {
    struct list_head list;
    struct task_struct *task;
    enum rwsem_waiter_type type;
    unsigned long timeout;
    unsigned long last_rowner;
};


3. sem->count 位布局

这个5.4 内核 ARM64 平台布局如下:

         63         62                                8        7--3           2         1        0
+----------------+--------------------------------------+----------------+---------+---------+--------+
| READFAIL guard |           reader count               |      保留      | HANDOFF | WAITERS | WRITER |
+----------------+--------------------------------------+----------------+---------+---------+--------+

bit0 表示有活跃的写者,即有写者正持锁在临界区内。bit62-bit8 不为 0,表示有活跃的读者,即有读者正在临界区内执行,值表示正在临界区执行的读者的个数。在 sem->wait_list 中排队的非活跃的读者和写者不体现在这些bit位中。

位定义:

#define RWSEM_WRITER_LOCKED  (1UL << 0) //bit0
#define RWSEM_FLAG_WAITERS   (1UL << 1) //bit1
#define RWSEM_FLAG_HANDOFF   (1UL << 2) //bit2
#define RWSEM_FLAG_READFAIL     (1UL << (BITS_PER_LONG - 1)) //(1<<(64-1))=bit63

#define RWSEM_READER_SHIFT   8
#define RWSEM_READER_BIAS    (1UL << 8)   //0x100
#define RWSEM_READER_MASK    (~(RWSEM_READER_BIAS - 1)) //低 8 位为 0,其它位全为 1

#define RWSEM_WRITER_MASK    RWSEM_WRITER_LOCKED //只是 bit0
#define RWSEM_LOCK_MASK        (RWSEM_WRITER_MASK|RWSEM_READER_MASK) //除去 bit1-7
#define RWSEM_READ_FAILED_MASK    (RWSEM_WRITER_MASK|RWSEM_FLAG_WAITERS|RWSEM_FLAG_HANDOFF|RWSEM_FLAG_READFAIL) //bit0 + bit1 + bit2 + bit63

因此:

0x000: 完全空闲
0x100: 1个读者
0x200: 2个读者
0x001: 1个写者
0x003: 写者持锁 + 存在等待者
0x202: 2个读者 + 存在等待者
0x006: 无活跃持锁者 + WAITERS + HANDOFF

最高位 READFAIL 是防止 reader count 溢出的保护位,正常运行中几乎不会触发。


4.接口函数

//include/linux/rwsem.h

/* 持锁失败进入 TASK_UNINTERRUPTIBLE, 因此无需担心信号唤醒,无需判断返回值 */
void down_read(struct rw_semaphore *sem);
/* 持锁失败进入 TASK_INTERRUPTIBLE, 能被信号唤醒,强制要求判断返回值 */
int __must_check down_read_killable(struct rw_semaphore *sem);
int down_read_trylock(struct rw_semaphore *sem);

void down_write(struct rw_semaphore *sem);
int __must_check down_write_killable(struct rw_semaphore *sem);
int down_write_trylock(struct rw_semaphore *sem);

void up_read(struct rw_semaphore *sem);

void up_write(struct rw_semaphore *sem);

void downgrade_write(struct rw_semaphore *sem);


5. 使用上的关键限制

(1) rwsem 不可递归获取。
(2) 不能在中断或原子上下文睡眠。
(3) 不支持 read→write 原子升级。
(4) down_*_killable() 可能返回 -EINTR。
(5) trylock() 失败不会排队。
(6) rwsem_is_contended() 只是基于 wait_list 的启发式判断。
(7) reader owner 不是可靠所有权证明。
(8) 不应把 rwsem 当 completion 使用####,非 owner 释放API应尽量避免。
(9) 长时间持读锁也可能严重阻塞 writer。
(10) writer handoff只保证下一次竞争机会,不代表waker已经把锁直接交给writer。


二、锁获取流程

1. 读锁获取流程

1.1 down_read 快路径

入口最终调用 __down_read()

inline void __down_read(struct rw_semaphore *sem) //rwsem.c
{
    if (!rwsem_read_trylock(sem)) {
        rwsem_down_read_slowpath(sem, TASK_UNINTERRUPTIBLE);
    } else {
        rwsem_set_reader_owned(sem);
    }
}

rwsem_read_trylock() 实现:

/* 成功持锁返回 true,否则返回 false */
static inline bool rwsem_read_trylock(struct rw_semaphore *sem) //rwsem.c
{
    /* 原子地执行 sem->count += RWSEM_READER_BIAS(bit8,即 0x100), 返回更新后的 sem->count 的值 */
    long cnt = atomic_long_add_return_acquire(RWSEM_READER_BIAS, &sem->count);
    /* 非异常几乎不会出现小于0的情况 */
    if (WARN_ON_ONCE(cnt < 0))
        rwsem_set_nonspinnable(sem);
    /* 没有剔除保留位, 只要有活跃的写者、有 waiters、有 handoff 标志位,读者数溢出,都返回 false */
    return !(cnt & RWSEM_READ_FAILED_MASK);
}

返回前检查是否存在:写者、等待者、handoff、read-fail 保护位。如果都没有,才表示读锁获取成功。

a. 若 sem->count 上已经设置了 handoff,这里也失败,等效于关闭了读者在快速路径中偷锁。

b. 有 waiters 这里也失败,也就是说即使当前只有活跃的读者,但是来了个写者休眠等待了,后续的读者也都会失败,然后进入休眠等待状态。

注意:当快路径失败时,加上的 READER_BIAS 还在,进入慢路径后再撤销或重新调整。这种设计减少了一次 CAS 循环。

即只有 sem->count 锁字中只有活跃读者,读持锁才能走快速路径,锁字中有waiters、handoff、活跃写者,都失败。


1.2 down_read_trylock

down_read_trylock() 不排队、不睡眠,使用 cmpxchg 循环:

/* 即尝执行 sem->count += RWSEM_READER_BIAS,若存在 写者、等待者、handoff、read-fail 保护位,则失败返回  */
static inline int __down_read_trylock(struct rw_semaphore *sem)
{
    /* Optimize for the case when the rwsem is not locked at all. */
    long tmp = RWSEM_UNLOCKED_VALUE; //0L

    do {
        /*
         * 如果 sem->count 等于 tmp,则原子地将 sem->count = tmp + RWSEM_READER_BIAS。
         * 否则 sem->count 的值不变,原子地执行 tmp = sem->count。
         * 若 sem->count 的值成功更新了返回 true, 否则返回 false.
         */
        if (atomic_long_try_cmpxchg_acquire(&sem->count, &tmp, tmp + RWSEM_READER_BIAS)) {
            rwsem_set_reader_owned(sem);
            return 1;
        }
    } while (!(tmp & RWSEM_READ_FAILED_MASK));

    return 0;
}

只有没有失败标志时才成功,失败立即返回 0。


1.3 读慢路径

rwsem_down_read_slowpath()

主要步骤:

(1) 保存当前 reader owner 信息;
(2) 撤回快路径预加的 READER_BIAS;
(3) 尝试乐观自旋;
(4) 仍失败则构造 reader waiter;
(5) 在 wait_lock 保护下加入 wait_list;
(6) 设置 WAITERS 标志;
(7) 必要时触发队首唤醒;
(8) 设置 TASK_UNINTERRUPTIBLE 或 TASK_KILLABLE;
(9) schedule() 睡眠;
(10) 被授予读锁后返回;

reader 等待的条件不是普通的“被唤醒就重试”,而是:

if (!smp_load_acquire(&waiter.task))
    break;

//waker 会执行:

smp_store_release(&waiter->task, NULL);

这对 release/acquire 确保被唤醒的 reader 已经正式获得读锁计数。


2. 写锁获取流程

2.1 down_write 快路径

写锁只能从完全空闲状态获得:

static inline void __down_write(struct rw_semaphore *sem) //rwsem.c
{
    long tmp = RWSEM_UNLOCKED_VALUE; //0L

    /* 写锁的快速路径只能从完全空闲状态(0L)下获取,这里没循环,只比较一次: if(sem->count==0) sem->count=1; */
    if (unlikely(!atomic_long_try_cmpxchg_acquire(&sem->count, &tmp, RWSEM_WRITER_LOCKED))) //1UL
        rwsem_down_write_slowpath(sem, TASK_UNINTERRUPTIBLE);
    else
        rwsem_set_owner(sem);
}

只要存在以下任意状态就失败:

(1) 一个或多个 reader。
(2) 已有 writer。
(3) waiters/handoff 等状态。sem->count 上有 handoff 标志也不行,防止了写者在快速路径上偷锁。

即只有锁是完全空闲(sem->count 锁字为0,即没有活跃持锁者、没有waiter、没有handoff、)状态下,才能走快速路径。


2.2 写慢路径

流程:

(1) 先尝试 optimistic spinning;
(2) 失败后创建 writer waiter;
(3) 加入 wait_list 尾部;
(4) 判断自己是否为第一个 writer;
(5) 在 wait_lock 下尝试 rwsem_try_write_lock();
(6) 失败则释放 wait_lock 并 schedule();
(7) 被唤醒后重新获取 wait_lock 再尝试;
(8) 成功后删除 waiter、设置 owner 并返回;

writer 有三种等待状态:

WRITER_NOT_FIRST
WRITER_FIRST
WRITER_HANDOFF

只有队首 writer 才有资格进入 handoff。


三、解锁流程

1. up_read

inline void __up_read(struct rw_semaphore *sem)
{
    long tmp;
    /* 原子地执行 sem->count -= RWSEM_READER_BIAS 并返回更新后的 sem->count */
    tmp = atomic_long_add_return_release(-RWSEM_READER_BIAS, &sem->count);
    DEBUG_RWSEMS_WARN_ON(tmp < 0, sem);
    /* 若当前没有活跃的读者和写者,且有 waiter,才执行唤醒动作 */
    if (unlikely((tmp & (RWSEM_LOCK_MASK|RWSEM_FLAG_WAITERS)) == RWSEM_FLAG_WAITERS)) {
        /* 清除 sem->owner 中的标志位 */
        clear_wr_nonspinnable(sem);
        rwsem_wake(sem, tmp);
    }
}

使用 release 语义执行 count -= RWSEM_READER_BIAS; 当结果满足:无活跃 reader/writer 并且 WAITERS 存在,最后一个 reader 调用 rwsem_wake()。

示例:

0x202  两个 reader + WAITERS
0x102  一个 reader + WAITERS
0x002  零 reader + WAITERS → wake 队首等待者


2. up_write

static inline void __up_write(struct rw_semaphore *sem)
{
    long tmp;

    rwsem_clear_owner(sem); //sem->owner=0;
    /* 先返回 sem->count 的值,然后再执行 sem->count-= RWSEM_WRITER_LOCKED */
    tmp = atomic_long_fetch_add_release(-RWSEM_WRITER_LOCKED, &sem->count);
    if (unlikely(tmp & RWSEM_FLAG_WAITERS))
        rwsem_wake(sem, tmp);
}

清除 owner, count -= RWSEM_WRITER_LOCKED, 如果 WAITERS 存在,调用 rwsem_wake(), 同样使用 release 语义。


3. downgrade_write

static inline void __downgrade_write(struct rw_semaphore *sem)
{
    long tmp;
    /* 原子地释放写锁并获取读锁,返回修改前的 sem->count 值 */
    tmp = atomic_long_fetch_add_release(-RWSEM_WRITER_LOCKED+RWSEM_READER_BIAS, &sem->count);
    rwsem_set_reader_owned(sem); //sem->owner=current
    /* 如果有等待者,则执行唤醒动作 */
    if (tmp & RWSEM_FLAG_WAITERS)
        rwsem_downgrade_wake(sem);
}

原子转换:WRITER_LOCKED → READER_BIAS, 然后设置 reader owner,并批量唤醒排队 reader。Linux 支持 write→read 降级,但不支持 read→write 原地升级。升级必须:up_read() --> down_write(),期间状态可能变化,调用者必须重新校验条件。


四、乐观自旋

乐观自旋实现在 rwsem_optimistic_spin() 中。目的:如果锁持有者正在 CPU 上运行、很可能马上释放,与其执行:入队 → schedule → 唤醒 → 上CPU,不如短时间原地等待。

1. 允许自旋的基本条件

rwsem_can_spin_on_owner() 会检查:

(1) 当前任务没有 need_resched()。
(2) owner 没有被标记为 non-spinnable。
(3) 写锁 owner 仍在 CPU 上运行。
(4) owner 所在 vCPU 没有被抢占。
(5) RT 任务不会形成同 CPU 活锁。//TODO: 什么意思?

核心判断:

return owner->on_cpu && !vcpu_is_preempted(task_cpu(owner));


2. OSQ 的作用

所有 spinner 先进入 sem->osq:

spinner A → 当前活跃 spinner
spinner B → OSQ 等待
spinner C → OSQ 等待

避免 A、B、C 同时疯狂读写 count 和 owner,降低 cacheline bouncing。


3. writer 对 reader phase 的自旋上限

当锁由 reader 持有时,writer 不能无限自旋。

你的分支使用:阈值 = 10 + reader数量 / 2 微秒,最大 25 微秒。

reader 越多,预计清空 reader phase 所需时间越长,但最多只等待 25 微秒。超时后设置 RWSEM_WR_NONSPINNABLE,后续 writer 直接睡眠,不重复浪费 CPU。


4. 自旋不是公平保证

在 handoff 建立前,新的 optimistic spinner 仍可能“偷锁”。这也是为什么 Linux 5.4 需要 WAITERS 超时后的 handoff 协议。


五、HANDOFF 与防止 writer 饥饿

0. 简介

RWSEM_FLAG_HANDOFF 不是“已经把锁所有权交给writer”,而是:禁止新到来的任务继续抢锁,把下一次可用机会保留给队首等待者。

最小等待时间:

#define RWSEM_WAIT_TIMEOUT DIV_ROUND_UP(HZ, 250) //250分之一秒,通常是 1 个 jiffie 约 4ms。

队首 writer 满足以下任一条件后请求 handoff:

(1) 等待超过 RWSEM_WAIT_TIMEOUT。
(2) 当前 writer 是 RT 任务。

此时:

count |= RWSEM_FLAG_HANDOFF

后续 writer 或 reader 的非排队抢锁都会失败。真正的锁获取仍由队首 writer 在自己的上下文中执行(I:只有写者在自己上下文持锁):

rwsem_try_write_lock()

它在持有 wait_lock 时:清除 HANDOFF、设置 WRITER_LOCKED、必要时清除 WAITERS、设置 owner=current。

因此要区分:writer 被 wake_q 唤醒 ≠ writer 已经获得 rwsem。(I: reader 持锁后被唤醒,只要被唤醒就是已持锁状态,被信号异常唤醒除外)

这是理解 rwsem_mark_wake() 的关键。


1. 为什么需要 HANDOFF —— 解决"写者饥饿" 

rwsem 允许**乐观自旋(optimistic spinning)和读者/写者抢锁(lock stealing)**来提升吞吐。但这带来一个副作用:

一个写者在等待队列里排第一,理应轮到它;但每次锁刚被释放,新来的读者/写者可能在它被唤醒之前就把锁抢走(steal);如果这种抢锁持续发生,队首那个 waiter 就会一直拿不到锁 → 饥饿。

HANDOFF(直译"交接")就是为打破这种饥饿而设计的:当队首 waiter 等太久了,就设置一个 HANDOFF 标志,强制"锁必须交接给等待队列里的人,禁止任何新来者插队抢锁",从而保证排队者最终能拿到锁。


2. HANDOFF 的载体:sem->count 里的一个标志位

从代码顶部的位定义看,sem->count 用不同 bit 表达锁状态:

/*
 * There are three places where the lock handoff bit may be set or cleared.
 * 1) rwsem_mark_wake() for readers.
 * 2) rwsem_try_write_lock() for writers.
 * 3) Error path of rwsem_down_write_slowpath().
 */
#define RWSEM_WRITER_LOCKED   (1UL << 0)   // 写者持锁
#define RWSEM_FLAG_WAITERS    (1UL << 1)   // 有等待者
#define RWSEM_FLAG_HANDOFF    (1UL << 2)   // 交接标志

要点:
(1) 只有等待队列的队首才有资格设置 HANDOFF;
(2) 设置/清除都在持有 wait_lock 的情况下进行,所以不会并发冲突;
(3) 一旦 HANDOFF 置位,锁进入"只能交接、不许抢"的状态。


3. 什么时候触发 HANDOFF —— 等待超时

触发条件是"等太久"。超时阈值:

#define RWSEM_WAIT_TIMEOUT   DIV_ROUND_UP(HZ, 250)   //至少 4ms 或 1 个 jiffy

waiter 入队时会记 waiter->timeout = jiffies + RWSEM_WAIT_TIMEOUT。超过这个时间还没拿到锁,就发起 HANDOFF。写者和读者路径分别处理。

 

3.1 写者路径()

在 rwsem_down_write_slowpath() 中,在写者在睡眠循环里判断:

/* 我是队首、且(实时任务 或 已超时) → 进入 HANDOFF 态 */
if ((wstate == WRITER_FIRST) && (rt_task(current) || time_after(jiffies, waiter.timeout))) {
    wstate = WRITER_HANDOFF;
    break;
}

注意两点:

(1) 必须 WRITER_FIRST(队首写者) 才有资格;
(2) RT 实时任务不用等超时,立即 HANDOFF —— 因为实时任务对延迟敏感,不能让它被普通任务反复抢锁饿着。

写者状态机有三态:WRITER_NOT_FIRST(非队首)、WRITER_FIRST(队首)、WRITER_HANDOFF(队首且需要交接)。

给 sem->count 设置 handoff 标志 RWSEM_FLAG_HANDOFF 的动作推迟到 rwsem_try_write_lock() 里做。


3.2 读者路径

在 rwsem_mark_wake() 中,当读者要拿锁但发现被写者占着、且自己等超时了:

if (!(oldcount & RWSEM_FLAG_HANDOFF) && time_after(jiffies, waiter->timeout)) {
    /* 请求设置 HANDOFF, 逼写者交出锁 */
    adjustment -= RWSEM_FLAG_HANDOFF;
    lockevent_inc(rwsem_rlock_handoff);
}


4. HANDOFF 如何真正"设置"和"抢锁禁止"

核心在 rwsem_try_write_lock(), 这是 HANDOFF 逻辑最集中的地方。它用 cmpxchg 循环尝试拿写锁,同时处理 handoff:

static inline bool rwsem_try_write_lock(struct rw_semaphore *sem, enum writer_wait_state wstate)
{
    ...
    do {
        bool has_handoff = !!(count & RWSEM_FLAG_HANDOFF);

        /* (1) 已有 handoff, 但我不是队首 → 不许抢, 直接失败 */
        if (has_handoff && wstate == WRITER_NOT_FIRST)
            return false;

        /* 锁还被占着(有活跃的读者或写者) */
        if (count & RWSEM_LOCK_MASK) {
            /* (2) 已置 handoff 或 我还没到 HANDOFF 态 → 不能重复设/不该设 */
            if (has_handoff || (wstate != WRITER_HANDOFF))
                return false;
            /* (3) 我是队首且到了 HANDOFF 态 → 设置 handoff 位 */
            new |= RWSEM_FLAG_HANDOFF;
        } else {
            /* 锁空了, 可以拿,拿写锁 */
            new |= RWSEM_WRITER_LOCKED;
            /* (4) 拿到锁同时清掉 handoff 位 */
            new &= ~RWSEM_FLAG_HANDOFF;
            /* 等待队列中只有一个成员,就是自己这个即将持锁的写者,这里将waiters标志后续也一并去除掉 */
            if (list_is_singular(&sem->wait_list))
                new &= ~RWSEM_FLAG_WAITERS;
        }
        /* 原子地判断如果 sem->count==count 则将 sem->count=new, 否则只执行 count=sem->count */
    } while (!atomic_long_try_cmpxchg_acquire(&sem->count, &count, new));

    /* 如果这次只是"设置了 handoff 位"而没拿到锁, 返回 false(还得继续等) */
    if (new & RWSEM_FLAG_HANDOFF)
        return false;

    /* 走到这里说明真正拿到了锁(且 handoff 已清) */
    rwsem_set_owner(sem);
    return true;
}

把这段逻辑串起来:

(1) 设置阶段:队首写者超时进入 WRITER_HANDOFF 态,发现锁还被占,就在 count 上置 RWSEM_FLAG_HANDOFF 位(第 3 处),然后返回 false 继续等。此后:
a.新来的写者在快路径 rwsem_try_write_lock_unqueued() 中自旋里看到 HANDOFF 位就不许抢(见 while (!(count & (RWSEM_LOCK_MASK|RWSEM_FLAG_HANDOFF))));
b.新来的读者在 rwsem_try_read_lock_unqueued()中看到 HANDOFF 也放弃抢锁去排队。
(2) 交接完成:当前持锁者释放后,锁位清空,队首写者再次 rwsem_try_write_lock 时走"锁空了"分支,拿到写锁并同时清 HANDOFF 位(第 4 处)。交接完成。


5. HANDOFF 位在哪些地方被清除

保证不会"卡死置位",有多条清除路径:

(1) 写者成功拿锁时, rwsem_try_write_lock():new &= ~RWSEM_FLAG_HANDOFF —— 正常交接完成。

(2) 唤醒了读者时, rwsem_mark_wake():

/* 已经让读者拿到锁了, 不必再逼交接 */
if (woken && (atomic_long_read(&sem->count) & RWSEM_FLAG_HANDOFF))
    adjustment -= RWSEM_FLAG_HANDOFF;

(3) 写者放弃等待(被信号打断等)退出时, rwsem_down_write_slowpath() 尾部:

/* 我设的 handoff, 我走了要清掉 */
if (unlikely(wstate == WRITER_HANDOFF))
    atomic_long_add(-RWSEM_FLAG_HANDOFF, &sem->count);

(4) 等待队列空了, rwsem_down_read_slowpath():atomic_long_andnot(RWSEM_FLAG_WAITERS|RWSEM_FLAG_HANDOFF, ...) —— 没人等了,标志一并清。


6. HANDOFF 期间的加速优化

也即 spin on owner。设了 HANDOFF 不代表干等。写者设置 handoff 后会尝试自旋在持锁者身上,等它一放锁就立刻抢到,避免"睡下去又被唤醒"的调度延迟:

/* rwsem_down_write_slowpath --> */
/* 设置 handoff 位、抢锁失败后, 自旋等待 owner 交出锁, 加速交接 */
if (wstate == WRITER_HANDOFF && rwsem_spin_on_owner(sem, RWSEM_NONSPINNABLE) == OWNER_NULL)
    goto trylock_again;

并且在睡眠循环里,只要处于 WRITER_HANDOFF 态就无条件做 trylock:

/* rwsem_down_write_slowpath --> */
/* 如果设置了 handoff,直接去 trylock, 不再睡 */
if (wstate == WRITER_HANDOFF)
    break;


7. 整体流程串一遍(写者视角)

写者进 slowpath 排队 (记 timeout = jiffies + ~4ms)
   │
   ├─ 是队首(WRITER_FIRST) 且 (RT任务 或 已超时)?
   │        │是
   │        ▼
   │   wstate = WRITER_HANDOFF
   │        │
   │        ▼
   │   rwsem_try_write_lock():
   │        ├─ 锁还被占 → 在 count 置 HANDOFF 位, 返回 false
   │        │      → 此后新读者/写者一律不许抢锁, 只能排队
   │        │      → 自旋 spin_on_owner, 等 owner 放锁
   │        └─ 锁空了 → 拿写锁 + 清 HANDOFF 位 → 交接完成, 返回 true
   │
   └─ 若中途被信号打断退出 → 清掉自己设的 HANDOFF 位


8. 总结:

(1) handoff 标志主要是防止 writer 饥饿,但是也防止 reader 饥饿。这么说的原因是工程上使用 rwsem 的主要是"读多写少", 且休眠的读者会先授予锁后再唤醒,比写者先唤醒后再尝试持锁临界区更小。

(2) 偷锁路径:

偷锁是在已有 waiter 存在的情况下,后来者不排队休眠,通过乐观自旋竞争获取锁的行为。只存在于慢速路径的乐观自旋中。持锁的快速路径中只有锁完全空闲才能才走,不属于偷锁。

乐观自旋偷锁(在已经有waiter存在的情况下,后来者持锁)是在慢速路径中,写者持锁是只要当前没有活跃持锁者或 handoff 标志,就尝试偷锁。读者持锁是只要没有 写者持锁或 handoff 标志位,就去偷锁。
详见:

rwsem_down_read_slowpath/rwsem_down_write_slowpath --> rwsem_optimistic_spin --> rwsem_try_write_lock_unqueued/rwsem_try_read_lock_unqueued

(3) handoff 标志可以管快速路径、和乐观自旋偷锁。

 

六、等待者唤醒机制

核心是 rwsem_mark_wake(),调用方在持有 wait_lock 时只把任务加入 wake_q,然后:释放wait_lock → wake_up_q(),这样避免在 wait_lock 内调用调度器唤醒路径。

1. 队首是 writer

只唤醒一个 writer:

wake_q_add(wake_q, waiter->task);

waker 不会替它设置 WRITER_LOCKED。writer 醒来后自己竞争或通过 handoff 获得锁。即 writer 是先唤醒,然后在 writer 自己的进程上下文竞争持锁。


2. 队首是 reader

reader 处理方式完全不同:

(1) waker 先给 reader 增加持锁计数。
(2) 设置 rwsem 为 reader-owned。
(3) 从等待链表收集一批 reader。
(4) 一次性增加所有 reader count, 包括 writer 后面的 reader, 一次性唤醒所有的 reader。
(5) 将它们的 waiter->task 清零。
(6) 最后统一唤醒。

每次最多唤醒:MAX_READERS_WAKEUP = 0x100 即 256 个 reader。代码采用两遍处理:

第一遍:移动 waiter、统计数量、增加 count,
第二遍:清空 waiter->task、加入 wake_q。

原因是 reader 可能还没真正睡下。如果先唤醒再增加 count,它可能立即执行完临界区并 up_read(),造成 reader count 次序错误。


3. 授予锁时机

reader 在被唤醒前,waker 已经增加 reader count,reader 已经被授予锁。writer 在被唤醒时通常还没有获得锁,只是被通知去重试。真正 writer owner 在 rwsem_try_write_lock() 成功后才设置。


4. wake_type

enum rwsem_wake_type {
    RWSEM_WAKE_ANY,          /* Wake whatever's at head of wait list */
    RWSEM_WAKE_READERS,      /* Wake readers only */
    RWSEM_WAKE_READ_OWNED    /* Waker thread holds the read lock */
};

三者的传参调用路径:

    rwsem_down_read_slowpath
        if (wake || (!(count & RWSEM_WRITER_MASK) && (adjustment & RWSEM_FLAG_WAITERS)))
            rwsem_mark_wake(sem, RWSEM_WAKE_ANY, &wake_q); //【】休眠前锁空唤醒一次

    rwsem_down_write_slowpath
        rwsem_mark_wake(sem, (count & RWSEM_READER_MASK) ? RWSEM_WAKE_READERS : RWSEM_WAKE_ANY, &wake_q); //【】写者不是队首,执行一次
    out_nolock:
        rwsem_mark_wake(sem, RWSEM_WAKE_ANY, &wake_q); //【】写者被信号唤醒异常退出

    __up_read
    __up_write
        rwsem_wake
            if (!list_empty(&sem->wait_list))
                rwsem_mark_wake(sem, RWSEM_WAKE_ANY, &wake_q); //【】常规唤醒传的是any

            rwsem_down_read_slowpath //【】读者乐观自旋持到锁后
    __downgrade_write //【】写锁降低为读锁时执行
        rwsem_downgrade_wake
            if (!list_empty(&sem->wait_list))
                rwsem_mark_wake(sem, RWSEM_WAKE_READ_OWNED, &wake_q);

 

5. rwsem_mark_wake 主要逻辑

rwsem_mark_wake(struct rw_semaphore *sem, enum rwsem_wake_type wake_type, struct wake_q_head *wake_q)
{
    lockdep_assert_held(&sem->wait_lock);
    /* 这里的 waiter 是队首的等待者 */
    waiter = rwsem_first_waiter(sem);
    /* 若是领队waiter是写者,则入队后直接返回 */
    if (waiter->type == RWSEM_WAITING_FOR_WRITE) {
        if (wake_type == RWSEM_WAKE_ANY)
            wake_q_add(wake_q, waiter->task);
        return;
    }

    /* 只要不是写锁降级为读锁时对应的唤醒操作 */
    if (wake_type != RWSEM_WAKE_READ_OWNED) {
        struct task_struct *owner;
        adjustment = RWSEM_READER_BIAS;
        /* 先返回旧值,再加上读者活跃持锁偏移 */
        oldcount = atomic_long_fetch_add(adjustment, &sem->count);
        /* 发现锁是被写者持有状态 */
        if (unlikely(oldcount & RWSEM_WRITER_MASK)) {
            /* 领队读者等待超时了,也会指定 handoff 标志 */
            if (!(oldcount & RWSEM_FLAG_HANDOFF) && time_after(jiffies, waiter->timeout)) {
                adjustment -= RWSEM_FLAG_HANDOFF;
            }
            /* 这里给 sem->count 加上 handoff 标志,同时回退前面加的读者偏置 */
            atomic_long_add(-adjustment, &sem->count);
            return;
        }
    }

    /*  ... 收集所有读 waiters,包括写者后面的 waiters,放到待唤醒链表上 ... */

    /* 这里判断了有 handoff 标志才执行减去 */
    if (woken && (atomic_long_read(&sem->count) & RWSEM_FLAG_HANDOFF))
        adjustment -= RWSEM_FLAG_HANDOFF;

    if (adjustment)
        atomic_long_add(adjustment, &sem->count);

    ...
}

也就是读者的 handoff 标志位的设置和清理处理,是完全被 sem->wait_lock 包裹的,waiter 插队动作是被此锁序列化的,因此不影响读者 handoff 位的设置和清理。

 

七、公平性模型

Linux 5.4 rwsem 不是严格 FIFO,而是接近 phase-fair:

1. writer

按等待队列顺序获取。
队首 writer 超时后启用 handoff。
handoff 建立前仍允许短时间 steal,提高吞吐。
handoff 建立后优先保证队首 writer,防止饥饿。

2. reader

如果等待队首是 reader,则进入 reader phase:

批量授予 reader。
最多一次 256 个。
当前实现会跳过队列中的 writer,继续收集 reader。
reader phase 结束后再进入 writer phase。

因此优化目标是在两者之间平衡:严格 FIFO 公平 vs reader 批处理吞吐 vs 短临界区避免睡眠的低延迟。


八、锁竞争示例

1. 示例1

(1) 初始:sem->count = 0
(2) 两个 reader 进入:R1: 0x000 → 0x100, R2: 0x100 → 0x200
(3) writer 到来,排队并设置 WAITERS:sem->count = 0x202, wait_list = [W]
(4) R1退出:0x202 → 0x102
(5) R2退出:0x102 → 0x002, 最后一个 reader 发现:LOCK_MASK == 0 && WAITERS == 1 于是唤醒 W。W 在自己的上下文中调用 rwsem_try_write_lock():0x002 → 0x001, 因为 W 是唯一 waiter,同时清除 () () () (6) WAITERS,然后:sem->owner = W.
(7) W释放:owner = NULL, count: 0x001 → 0x000


九、内存序保证

rwsem 依赖以下关键语义:

1. 获取成功

atomic_long_add_return_acquire()
atomic_long_try_cmpxchg_acquire()

保证临界区内的读写不能越过加锁向前移动。

2. 解锁

atomic_long_add_return_release()
atomic_long_fetch_add_release()

保证临界区修改在其他任务成功获取锁前可见(不能向后移动)。

3. reader 授权

smp_store_release(&waiter->task, NULL);
smp_load_acquire(&waiter.task);

保证 reader 观察到授权标志时,也观察到 waker 完成的 count 和 owner 更新。

4. 等待队列

wait_lock 串行化:waiter 链表操作。handoff 设置/清除。reader 批量授权。writer 队首状态判断。


十、总结

Linux 5.4 rwsem可以概括为:atomic count 快路径 + owner 感知乐观自旋 + OSQ 串行 spinner + wait_list 睡眠慢路径 + reader 批量授权 + writer 超时 handoff + acquire/release 内存序。

 

posted on 2026-09-26 17:01  Hello-World3  阅读(5)  评论(0)    收藏  举报

导航