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) 收藏 举报
浙公网安备 33010602011771号