Linux内核Rwsem|读写锁实现原理核心解析
在多线程编程的世界里,锁机制是保证数据一致性的关键。当我们面对"读多写少"的场景时,传统的互斥锁往往成为性能瓶颈。Linux内核如何巧妙地解决这一问题?
一、背景:为什么需要读写锁?
1.1、传统互斥锁的局限性
在并发编程中,互斥锁(mutex)是最基础的同步原语,但它采用“一刀切”的策略:任何时候只允许一个线程访问受保护资源。这种设计在多读少写的场景中存在明显的性能瓶颈。
典型场景示例:
// 配置信息结构体(读多写少)
struct config {
rwlock_t lock;
int max_connections;
int timeout_ms;
char version[32];
};
// 读取线程(高频调用)
void read_config(struct config* cfg) {
mutex_lock(&cfg->lock); // 🔴 所有读者串行化
printf("Timeout: %d\n", cfg->timeout_ms);
mutex_unlock(&cfg->lock);
}
// 写入线程(低频调用)
void update_config(struct config* cfg) {
mutex_lock(&cfg->lock);
cfg->timeout_ms = 5000; // ⚡️ 阻塞所有读者
mutex_unlock(&cfg->lock);
}
1.2 读写锁的核心思想
读写锁(Read-Write Semaphore, Rwsem)引入了一种更精细的并发控制策略:
| 锁类型 | 共享特性 | 适用场景 |
| 读锁 | 共享访问 | N个读者可同时访问 |
| 写锁 | 独占访问 | 仅一个写者,且排斥所有读者 |
这种设计的理论基础是Amdahl定律:通过并行化读操作来提升整体吞吐量。
1.3 Linux的选择:Rwsem vs RCU
Linux内核提供了多种同步机制,Rwsem在其中定位明确:
// Linux同步机制选择指南
if (读操作 >> 写操作) {
if (数据量小 && 实时性要求高) {
使用读写锁(rwsem); // ✅ 本文重点
} else if (容忍旧数据 && 追求极致性能) {
使用RCU; // ⚡️ Read-Copy-Update
}
} else if (读写相当) {
使用互斥锁(mutex); // 🔄 Simple is best
} else if (等待时间极短) {
使用自旋锁(spinlock); // 🌀 CPU忙等待
}
二、数据结构设计:如何抽象读写状态?
Linux内核中,读写锁的核心实现是struct rw_semaphore(简称rwsem),不同内核版本的结构细节略有差异,但核心字段的设计逻辑一致。

2.1 Rwsem核心结构体分析
// include/linux/rwsem.h
struct rw_semaphore {
atomic_long_t count; // 📊 64位状态计数器
#ifdef CONFIG_RWSEM_SPIN_ON_OWNER
struct optimistic_spin_queue osq; /* ✨ 乐观自旋队列 */
#endif
raw_spinlock_t wait_lock; // 🔒 保护等待队列的自旋锁
struct list_head wait_list; // 📋 等待任务链表
};
2.2 count字段的位分解密
count字段采用位域编码,巧妙地将多个状态压缩到一个原子变量中:
/* 64位count字段布局:
* ┌────────────────┬──────────────┬────────────────┬──────────────┐
* │ 写者计数(16) │ 预留位(16) │ 读者计数(24) │ 活动标志(8) │
* └────────────────┴──────────────┴────────────────┴──────────────┘
*/
// 具体定义:
#define RWSEM_WRITER_LOCKED (1UL << 0) // Bit0: 写锁定标志
#define RWSEM_FLAG_WAITERS (1UL << 1) // Bit1: 存在等待者
#define RWSEM_FLAG_HANDOFF (1UL << 2) // Bit2: handoff进行中
#define RWSEM_FLAG_READER_OWNED (1UL << (BITS_PER_LONG - reader_shift))
// 关键宏定义:
#define RWSEM_READER_SHIFT (BITS_PER_LONG - reader_bits)
#define RWSEM_READER_BIAS (1UL << RWSEM_READER_SHIFT)
#define RWSEM_WRITER_MASK (0xFFFFUL << (RWSEM_READER_SHIFT + 24))
这种设计的精妙之处在于:
- 原子性操作:单个atomic_long_t保证状态变更的原子性
- 内存高效:避免为每个状态维护独立变量带来的缓存行争用
2.3 等待队列实现机制
采用双链表结构管理等待任务,每个等待节点包含:
struct rwsem_waiter {
struct list_head list; // 📌 链表节点,用于挂载到等待队列
struct task_struct *task; // 🧵 等待的任务指针
enum rwsem_waiter_type type;// 🔖 等待类型(READER/WRITER)
unsigned long timeout; // ⏱️ 可选的超时时间(如有需要)
};
三、API接口层

与互斥锁的区别:读写锁API通过区分读操作和写操作,提供了更细粒度的同步控制。例如,down_read允许多个读者同时进入临界区,而down_write则必须等待所有读者和写者释放锁后才能获取 。
API使用规范与示例
(1)基本使用模式:
struct rw_semaphore sem;
// 读者线程
void reader_thread(void)
{
down_read(&sem); // 🔒 获取读锁
/* 临界区: 只读操作 */
// 📖 多个读者可同时进入
up_read(&sem); // 🔓 释放读锁
}
// 写者线程
void writer_thread(void)
{
down_write(&sem); // ✏️ 获取写锁
/* 临界区: 写入操作 */
// ✍️ 独占访问,无其他读者/写者
up_write(&sem); // 🚪 释放写锁
}
(2)嵌套使用注意事项:
// 读写锁升级的正确方式
down_read(&sem); // 🔒 首先获取读锁
if (need_upgrade) {
up_read(&sem); // 🔓 先释放读锁
down_write(&sem); // ✏️ 再获取写锁
/* 临界区: 写入操作 */
// ✍️ 独占访问保证
up_write(&sem); // 🚪 释放写锁
} else {
/* 临界区: 只读操作 */
// 📖 共享访问
up_read(&sem); // 🔓 释放读锁
}
/* 错误示例 - 会导致死锁: */
// down_read(&sem);
// down_write(&sem); // ❌ 尝试直接升级(可能死锁)
// up_read(&sem);
// up_write(&sem);
四、尝试获取读锁
与 down_read 不同,down_read_trylock 仅尝试获取读锁。若成功,则直接返回 1;若失败,不会阻塞,而是返回 0。其核心逻辑位于 __down_read_trylock 函数中,代码如下:
tmp = RWSEM_UNLOCKED_VALUE;
do {
if (atomic_long_try_cmpxchg_acquire(&sem->count, &tmp, tmp + RWSEM_READER_BIAS)) { // A
rwsem_set_reader_owned(sem);
return 1;
}
} while (!(tmp & RWSEM_READ_FAILED_MASK)); // B
return 0;
- 初始状态:tmp 初始化为 RWSEM_UNLOCKED_VALUE(值为 0)。首次循环针对空锁场景进行优化:若当前 sem->count 等于 0,则将其更新为 RWSEM_READER_BIAS,标记持锁成功,随后设置 owner 并返回 1。
- 快速获取失败后的处理:若快速获取空锁失败(此时 tmp 已更新为当前的 sem->count),需根据当前计数值判断是否可进入临界区。以下情况会导致持读锁失败:
- RWSEM_WRITER_MASK:已有写者持有锁。
- RWSEM_FLAG_WAITERS:存在写者或读者在等待队列中。
- RWSEM_FLAG_HANDOFF:锁正处于转交过程中(即已有预定的 owner)。
- RWSEM_FLAG_READFAIL:临界区内读者过多(通常为异常状态)。
- 重试机制:若判断可进入读临界区(即仅有读者且无写者等待),则重新进入循环。如果在此期间 sem->count 未发生变化,则持锁成功:
- 若其他线程在此期间修改了计数值,则需重新判断是否满足持锁条件并重复上述循环。
- 增加读者计数。
- 设置 owner task。
- 保持 non-spinnable bit不变。
- 退出条件:若判断无法进入临界区,则退出循环并返回失败(0)。
五、获取读锁
Reader获取读锁的核心逻辑集中在__down_read_common函数中,该函数整合了快速路径、慢速路径的完整处理流程,是读锁获取的核心实现:
// 基于Linux 5.x内核
int __down_read_common(struct rw_semaphore *sem, long state, unsigned long timeout)
{
struct rwsem_waiter waiter;
struct wake_q_head wqh;
unsigned int adjustment = -RWSEM_READER_BIAS;
long cnt, orig_cnt;
bool first_reader = false;
// 快速路径:尝试非阻塞获取读锁
orig_cnt = cnt = rwsem_read_trylock(sem);
if (cnt >= 0)
return 0;
// 慢速路径:快速路径失败后的处理逻辑
wake_q_init(&wqh);
for (;;) {
// 防止等待队列任务饿死的判断
if (rwsem_is_reader_owned(sem) && rwsem_has_waiters(sem)) {
goto queue;
}
// 乐观偷锁(optimistic lock stealing)逻辑
if (!rwsem_is_writer_owned(sem) && !rwsem_handoff_pending(sem) &&
rwsem_is_waking(sem) && rwsem_has_waiters(sem)) {
if (rwsem_try_read_lock(sem, cnt)) {
// 偷锁成功,设置owner
rwsem_set_reader_owned(sem);
first_reader = (cnt == RWSEM_READER_BIAS);
goto wake_readers;
}
}
// 偷锁失败,进入阻塞流程
queue:
// 初始化等待节点
waiter.task = current;
waiter.type = RWSEM_WAITER_READ;
waiter.timeout = jiffies + 1; // 手机平台HZ=250,对应4ms超时
init_waitqueue_entry(&waiter.wait, current);
// 等待队列为空的特殊处理
if (list_empty(&sem->wait_list)) {
set_bit(RWSEM_FLAG_WAITERS, &sem->flags);
// 最后一次尝试持锁
if (rwsem_try_read_lock(sem, cnt)) {
clear_bit(RWSEM_FLAG_WAITERS, &sem->flags);
rwsem_set_reader_owned(sem);
return 0;
}
}
// 加入等待队列尾部
rwsem_add_waiter(&waiter, sem, RWSEM_WAITER_READ);
// 修正之前错误增加的counter
cnt = atomic_long_add_return(adjustment, &sem->count);
// 阻塞前的最后唤醒检查
if (rwsem_is_unlocked(sem)) {
clear_bit(RWSEM_FLAG_NONSPINNABLE, &sem->flags);
rwsem_mark_wake(sem, &wqh, RWSEM_WAKE_ANY, 0);
wake_up_q(&wqh);
}
// 阻塞等待
set_current_state(state);
if (waiter.task)
schedule_timeout(timeout);
__set_current_state(TASK_RUNNING);
// 检查唤醒状态
if (!waiter.task) {
// 正常唤醒,持锁成功
return 0;
}
if (signal_pending(current)) {
// 信号中断,持锁失败
rwsem_remove_waiter(&waiter, sem);
return -EINTR;
}
// 超时后设置handoff,禁止乐观自旋
if (time_after(jiffies, waiter.timeout)) {
set_bit(RWSEM_FLAG_HANDOFF, &sem->flags);
}
}
wake_readers:
// 唤醒队列中的多个reader(最多MAX_READERS_WAKEUP=256个)
if (first_reader && !rwsem_is_writer_waiter(sem->wait_list.next)) {
rwsem_mark_wake(sem, &wqh, RWSEM_WAKE_READERS, MAX_READERS_WAKEUP);
wake_up_q(&wqh);
}
return 0;
}
1. 快速路径
快速路径的核心是rwsem_read_trylock函数,通过原子操作快速尝试获取读锁,避免进入复杂的慢速路径:
// 读锁快速路径:
long rwsem_read_trylock(struct rw_semaphore *sem)
{
long tmp, cnt;
tmp = atomic_long_read(&sem->count);
for (;;) {
// 检查是否能获取读锁(无writer持有且无读失败标记)
if (tmp & RWSEM_READ_FAILED_MASK)
return -1;
// 原子增加读锁计数(RWSEM_READER_BIAS=1)
cnt = atomic_long_cmpxchg(&sem->count, tmp, tmp + RWSEM_READER_BIAS);
if (cnt == tmp)
break;
tmp = cnt;
}
// 持锁成功,设置reader owned标记
if (cnt >= 0) {
rwsem_set_reader_owned(sem);
// 读线程过多时禁止乐观自旋
if (cnt > RWSEM_MAX_READERS)
set_bit(RWSEM_FLAG_NONSPINNABLE, &sem->flags);
return cnt;
}
// 持锁失败,回退计数
atomic_long_sub(RWSEM_READER_BIAS, &sem->count);
return -1;
}
核心逻辑解析:
- Reader直接对sem->count加RWSEM_READER_BIAS(通常为1)来增加读临界区线程数;
- 若操作成功,设置RWSEM_READER_OWNED标记并返回;若失败则回退计数,进入慢速路径;
- 当读线程数量超过RWSEM_MAX_READERS时,禁止乐观自旋,避免过度并发导致的性能问题。
2. 慢速路径
快速路径失败后进入慢速路径,其核心是平衡reader吞吐量与waiter饥饿,分为三个关键阶段:
(1)防止waiter饿死的处理
// 防止等待队列任务饿死的判断逻辑
if (rwsem_is_reader_owned(sem) && rwsem_has_waiters(sem)) {
goto queue;
}
核心逻辑解析:
- 若锁已被reader持有且等待队列有任务,直接进入排队流程,不再乐观偷锁;
- 此举是为了平衡:既避免连续reader偷锁导致writer饿死,又能通过后续批量唤醒reader提升吞吐量。
(2)乐观偷锁(Optimistic Lock Stealing)
// 乐观偷锁的核心判断与实现
if (!rwsem_is_writer_owned(sem) && !rwsem_handoff_pending(sem) &&
rwsem_is_waking(sem) && rwsem_has_waiters(sem)) {
if (rwsem_try_read_lock(sem, cnt)) {
rwsem_set_reader_owned(sem);
first_reader = (cnt == RWSEM_READER_BIAS);
goto wake_readers;
}
}
核心逻辑解析:
- 偷锁的条件:无writer持锁、无handoff标记、正在唤醒top waiter且有任务排队;
- 若偷锁成功,若为第一个reader,会唤醒队列中最多MAX_READERS_WAKEUP(256)个reader,提升并发效率;
- 偷锁失败则需回退计数,避免计数错误。
(3)阻塞等待处理
// 阻塞等待的核心代码
// 初始化等待节点
waiter.task = current;
waiter.type = RWSEM_WAITER_READ;
waiter.timeout = jiffies + 1; // 4ms超时(手机平台HZ=250)
// 加入等待队列
if (list_empty(&sem->wait_list)) {
set_bit(RWSEM_FLAG_WAITERS, &sem->flags);
// 最后一次尝试持锁
if (rwsem_try_read_lock(sem, cnt)) {
clear_bit(RWSEM_FLAG_WAITERS, &sem->flags);
return 0;
}
}
rwsem_add_waiter(&waiter, sem, RWSEM_WAITER_READ);
cnt = atomic_long_add_return(adjustment, &sem->count);
// 阻塞前的唤醒检查
if (rwsem_is_unlocked(sem)) {
clear_bit(RWSEM_FLAG_NONSPINNABLE, &sem->flags);
rwsem_mark_wake(sem, &wqh, RWSEM_WAKE_ANY, 0);
wake_up_q(&wqh);
}
// 进入阻塞
set_current_state(state);
if (waiter.task)
schedule_timeout(timeout);
__set_current_state(TASK_RUNNING);
核心逻辑解析:
- 等待节点的timeout为4ms(手机平台),超时后触发handoff机制,禁止乐观自旋;
- 入队前会做最后一次持锁尝试,失败后加入队列尾部,并修正计数;
- 阻塞后被唤醒时,通过waiter.task是否为NULL判断是否正常唤醒,正常则持锁成功,否则处理信号中断。
六、释放读锁
释放读锁的逻辑在__up_read函数中实现:
void __up_read(struct rw_semaphore *sem)
{
long cnt;
cnt = atomic_long_dec_return(&sem->count);
if (cnt >= 0)
return;
rwsem_wake(sem);
}
这里仅减少读计数,不清除owner指针。当等待队列有任务且临界区为空时,系统会唤醒等待队列中的线程。
在唤醒过程中,系统使用rwsem_mark_wake函数,参数为RWSEM_WAKE_ANY:
static void rwsem_mark_wake(struct rw_semaphore *sem, enum rwsem_wake_type wake_type)
{
struct rwsem_waiter *waiter;
waiter = list_first_entry(&sem->wait_list, struct rwsem_waiter, list);
if (waiter->flags & RWSEM_WAITING_FOR_WRITE) {
if (wake_type == RWSEM_WAKE_ANY)
rwsem_wake_writer(sem, waiter);
} else {
if (wake_type == RWSEM_WAKE_READERS || wake_type == RWSEM_WAKE_READ_OWNED)
rwsem_wake_readers(sem, waiter);
}
}
当top waiter是写者时,系统只需唤醒该写者;当top waiter是读者时,系统会唤醒一组读者。
唤醒读者的具体过程如下:
static void rwsem_wake_readers(struct rw_semaphore *sem, struct rwsem_waiter *waiter)
{
struct rwsem_waiter *pos, *tmp;
struct list_head wlist;
int count = 0;
INIT_LIST_HEAD(&wlist);
list_for_each_entry_safe(pos, tmp, &sem->wait_list, list) {
if (pos->flags & RWSEM_WAITING_FOR_WRITE)
break;
count++;
list_move(&pos->list, &wlist);
}
if (count > MAX_READERS_WAKEUP)
count = MAX_READERS_WAKEUP;
sem->count += count * RWSEM_READER_BIAS;
sem->waiters -= count;
list_for_each_entry_safe(pos, tmp, &wlist, list) {
pos->task = NULL;
wake_q_add(&wake_q, pos->task);
}
wake_q_add(&wake_q, NULL);
wake_up_q(&wake_q);
}
系统会先唤醒top reader,然后根据情况唤醒其他读者。唤醒的读者数量最多为256个,以避免饿死写者。
七、尝试获取写锁
与down_write不同,down_write_trylock只尝试获取锁而不阻塞:
static inline int rwsem_write_trylock(struct rw_semaphore *sem)
{
long cnt;
cnt = atomic_long_read(&sem->count);
if (cnt == RWSEM_UNLOCKED_VALUE) {
if (atomic_long_try_cmpxchg_acquire(&sem->count, &cnt, RWSEM_WRITER_LOCKED)) {
sem->owner = current;
return 1;
}
}
return 0;
}
这里,系统检查锁是否为空闲状态(RWSEM_UNLOCKED_VALUE=0),如果是,尝试使用原子操作获取写锁。
八、获取写锁
写者获取锁的主流程在__down_write_common函数中:
static void __down_write_common(struct rw_semaphore *sem, int *cntp)
{
long cnt;
cnt = atomic_long_read(&sem->count);
if (likely(cnt == RWSEM_UNLOCKED_VALUE)) {
if (rwsem_write_trylock(sem))
return;
}
rwsem_down_write_slowpath(sem, cntp);
}
慢速路径中,系统会将自己加入等待队列并尝试唤醒:
static void rwsem_down_write_slowpath(struct rw_semaphore *sem, int *cntp)
{
struct rwsem_waiter waiter;
long cnt;
cnt = atomic_long_read(&sem->count);
if (cnt < 0) {
waiter.task = current;
waiter.flags = RWSEM_WAITING_FOR_WRITE;
waiter.timeout = jiffies + HZ/250;
rwsem_add_waiter(sem, &waiter);
if (rwsem_wake_readers(sem, &waiter))
return;
} else {
if (atomic_long_try_cmpxchg_acquire(&sem->count, &cnt, RWSEM_WRITER_LOCKED)) {
sem->owner = current;
return;
}
}
while (1) {
if (rwsem_try_write_lock(sem)) {
return;
}
if (signal_pending(current))
return -EINTR;
if (rwsem_is_handoff(sem)) {
if (rwsem_spin_on_owner(sem))
continue;
}
set_current_state(TASK_INTERRUPTIBLE);
schedule();
}
}
写者进入阻塞状态后,会尝试通过rwsem_try_write_lock获取锁:
static inline int rwsem_try_write_lock(struct rw_semaphore *sem)
{
long cnt;
cnt = atomic_long_read(&sem->count);
if (cnt == RWSEM_UNLOCKED_VALUE) {
if (atomic_long_try_cmpxchg_acquire(&sem->count, &cnt, RWSEM_WRITER_LOCKED)) {
sem->owner = current;
return 1;
}
}
return 0;
}
九、释放写锁

释放写锁的逻辑与释放读锁类似,但需要清除owner指针:
void __up_write(struct rw_semaphore *sem)
{
long cnt;
cnt = atomic_long_read(&sem->count);
if (cnt == RWSEM_WRITER_LOCKED) {
sem->owner = NULL;
rwsem_wake(sem);
return;
}
rwsem_wake(sem);
往期干货文章|C/C++ 开发专属
做 C/C++ 开发的小伙伴,大概率都会碰到选方向、定学习路线、求职面试、练实操这些头疼的问题,特意整理了一波针对性干货,大家按需自取~
选方向 / 求职迷茫?不知道该往哪走、求职没机会、拿不到 offer,甚至分不清是技术不到位还是简历拖后腿?速看👉为什么很多人劝退学 C++,但大厂核心岗位还是要 C++?
目标大厂 Linux C/C++ 后端岗?想找套科学系统的进阶指南,避开学习弯路?必看👉【大厂标准】Linux C/C++ 后端进阶学习路线
想入局音视频流媒体赛道?想掌握核心学习路径,搭建完整技术体系?看这篇👉音视频流媒体高级开发 - 学习路线
想做桌面 / 嵌入式开发,吃透 C++ Qt 技术?需要一套完整的学习闭环?收藏这篇👉C++ Qt 学习路线一条龙!(桌面开发 & 嵌入式开发)
想深耕底层技术,挑战 Linux 内核开发?需要硬核的学习方法和修炼手册?安排👉Linux 内核学习指南,硬核修炼手册
备战 C/C++ 面试?需要高频八股文题库刷题冲刺,夯实面试基础?刷这篇👉C/C++ 高频八股文面试题 1000 题(三)
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)