news 2026/9/4 6:48:31

第 3 章 rwlock 与 qrwlock:读者写者的自旋权衡

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
第 3 章 rwlock 与 qrwlock:读者写者的自旋权衡

第 2 章的spinlock把所有争抢者一视同仁:无论只是想读一眼、还是要动手改,都得排队独占。可现实里有一类极常见的场景——读多写少:一份数据被无数次读取,偶尔才更新一回。若读操作彼此之间本就无冲突,却仍被自旋锁串成单行道,多核的并行度就被白白浪费了。本章的主角qrwlock(queued read/write lock)正是为此而生:它允许多个读者同时进入临界区共享数据,只在写者出现时才要求独占。它和spinlock一样不睡眠、可用于中断上下文,区别只在于把“互斥”细化成了“读共享、写独占”。本章的主线,是看内核如何用一个 32 位原子字同时编码读者计数与写者状态,又如何用一个“等待标志”解决读写锁与生俱来的写者饥饿难题。

说明:以 x86-64 为主线,现代内核的rwlock_t在架构层统一落到qrwlock。源码集中在kernel/locking/qrwlock.cinclude/asm-generic/qrwlock.hinclude/asm-generic/qrwlock_types.h

3.1 rwlock 守护什么:读多写少的共享临界区

读写锁的语义可以用两条规则概括:

  • 读者与读者相容。任意多个读者可以同时持锁进入临界区,因为纯读不改变数据,彼此之间没有冲突。
  • 写者与所有人互斥。写者持锁时,既不允许别的写者进入,也不允许任何读者进入;反过来,只要还有读者在临界区,写者就必须等待。

这与第 2 章的spinlock形成鲜明对比:spinlock是“任何人进入都独占”,qrwlock则把这份独占只保留给写者,把读者之间的并行释放了出来。它适用的典型场景是那些读远多于写的共享结构——路由表、配置项、注册表这类“查得多、改得少”的数据。

需要强调的是,qrwlock依然是自旋锁家族的一员:争锁失败时不睡眠而是自旋,因此同样受第 2 章那两条硬约束的束缚——临界区必须短、持锁期间不能睡眠。它不是rwsem(第 6 章那把可睡眠的读写信号量),二者的分野和spinlockmutex的分野如出一辙:一个自旋、一个睡眠。

3.2 从read_lockqueued_read_lock:架构层的映射

spinlock一样,日常写的read_lock()/write_lock()也是逐层向下委托。在支持qrwlock的架构上,include/asm-generic/qrwlock.h末尾把架构接口直接映射到 queued 版本:

#definearch_read_lock(l)queued_read_lock(l)#definearch_write_lock(l)queued_write_lock(l)#definearch_read_trylock(l)queued_read_trylock(l)#definearch_write_trylock(l)queued_write_trylock(l)#definearch_read_unlock(l)queued_read_unlock(l)#definearch_write_unlock(l)queued_write_unlock(l)

上层的read_lock同样会先preempt_disable、经 lockdep 埋点,再落到arch_read_lock——这套分层与第 2 章 2.2 节完全一致,不再赘述。本章只聚焦最底层的queued_read_lock/queued_write_lock如何实现“读共享、写独占”。

3.3 经典读写锁的病:写者饥饿

在看qrwlock的实现之前,先要理解它到底解决了什么问题。最朴素的读写锁实现里,读者只要看到“当前没有写者”就直接把读者计数加一进入临界区。这在读者稀疏时没问题,可一旦读者络绎不绝——前一个读者还没走、后一个读者又来了,读者计数永远不归零——那么等在一旁的写者就永远等不到那个“无人读”的瞬间,被活活饿死。这就是经典读写锁的通病:读者优先会导致写者饥饿

qrwlock的核心设计目标,就是在保留“读者并行”的同时消除写者饥饿。它的办法是给写者一个抢占预约的能力:写者一旦开始等待,就立刻竖起一面旗,逼退所有还想走快路径的新读者,让在场的读者逐渐排空,从而保证写者能在有限时间内拿到锁。这面旗,就是下一节要讲的_QW_WAITING

3.4 一个原子字装下读写状态:qrwlock的编码

qrwlock的全部状态压缩在一个 32 位原子字里。看它的类型定义(include/asm-generic/qrwlock_types.h):

typedefstructqrwlock{union{atomic_tcnts;struct{u8 wlocked;/* Locked for write? */u8 __lstate[3];};};arch_spinlock_twait_lock;}arch_rwlock_t;

和第 2 章的qspinlock同一套路:一个union让同一份数据既能作为整体cnts做原子读-改-写,又能按字节单独访问其中的wlocked。这个 32 位的cnts被切成两段(include/asm-generic/qrwlock.h):

#define_QW_WAITING0x100/* A writer is waiting */#define_QW_LOCKED0x0ff/* A writer holds the lock */#define_QW_WMASK0x1ff/* Writer mask */#define_QR_SHIFT9/* Reader count shift */#define_QR_BIAS(1U<<_QR_SHIFT)

把这几个常量翻译成位布局,就一目了然了:

位段含义取值
bit 0–7(低字节wlocked写者持锁标志_QW_LOCKED=0x0ff
bit 8写者等待标志_QW_WAITING=0x100
bit 9–31读者计数每个读者贡献一个_QR_BIAS=0x200

其中_QW_WMASK0x1ff)正好覆盖 bit 0–8,也就是“写者相关”的全部位——只要这九位里有任何一位非零,就代表有写者正持锁或正在等待。读者计数从 bit 9 起,每来一个读者就给cnts加一个_QR_BIAS,因此最多可容纳2 23 2^{23}223个并发读者。

这个编码的精妙之处,是把“写者独占的低字节”单列成了wlocked:写者解锁时无需对整个cnts做原子读-改-写,只要往这个字节写一个 0 就行——这一点在 3.8 节会看到。

3.5 快路径:读者加计数,写者抢低位

无争用时,读写两条路径都只需一次原子操作。

读者快路径queued_read_lock):

staticinlinevoidqueued_read_lock(structqrwlock*lock){intcnts;cnts=atomic_add_return_acquire(_QR_BIAS,&lock->cnts);if(likely(!(cnts&_QW_WMASK)))return;/* The slowpath will decrement the reader count, if necessary. */queued_read_lock_slowpath(lock);}

读者先无条件地给cnts加一个_QR_BIAS(读者计数加一),拿到加完后的新值。若cnts & _QW_WMASK为零——说明既没有写者持锁、也没有写者在等——读者就成功了,直接返回进入临界区。注意这里用的是atomic_add_return_acquire:acquire 语义(第 1 章 1.6 节)保证临界区里的读取不会被重排到加计数之前,读者看到的一定是最新数据。

只有当_QW_WMASK那九位里有非零位(有写者持锁或等待)时,读者才走进慢路径——并且它已经先把计数加上去了,慢路径会视情况把这一笔减回来。

写者快路径queued_write_lock):

staticinlinevoidqueued_write_lock(structqrwlock*lock){intcnts=0;/* Optimize for the unfair lock case where the fair flag is 0. */if(likely(atomic_try_cmpxchg_acquire(&lock->cnts,&cnts,_QW_LOCKED)))return;queued_write_lock_slowpath(lock);}

写者要的是独占,所以它用第 1 章 1.4 节的 CAS:期望cnts当前为 0(既无读者也无写者),若成立就一步把它换成_QW_LOCKED(低字节置为0xff)。这个 CAS 只有在锁完全空闲时才成功——只要有任何读者在场、或有别的写者,cnts就非零,CAS 失败,写者转入慢路径。

3.6 写者优先:_QW_WAITING如何逼退新读者

写者慢路径是理解qrwlock如何消除写者饥饿的关键(kernel/locking/qrwlock.c):

void__lockfuncqueued_write_lock_slowpath(structqrwlock*lock){intcnts;/* Put the writer into the wait queue */arch_spin_lock(&lock->wait_lock);/* Try to acquire the lock directly if no reader is present */if(!(cnts=atomic_read(&lock->cnts))&&atomic_try_cmpxchg_acquire(&lock->cnts,&cnts,_QW_LOCKED))gotounlock;/* Set the waiting flag to notify readers that a writer is pending */atomic_or(_QW_WAITING,&lock->cnts);/* When no more readers or writers, set the locked flag */do{cnts=atomic_cond_read_relaxed(&lock->cnts,VAL==_QW_WAITING);}while(!atomic_try_cmpxchg_acquire(&lock->cnts,&cnts,_QW_LOCKED));unlock:arch_spin_unlock(&lock->wait_lock);}

它分三步,每一步都值得细看:

  1. 先抢wait_lockwait_lockqrwlock内嵌的一把普通自旋锁(第 2 章那把公平的qspinlock),它的作用是把所有等待的写者串成 FIFO 队列——同一时刻只有一个写者能越过这道门去竞争真正的读写锁,从根上杜绝了写者之间的乱序与饥饿。

  2. 竖旗_QW_WAITING。若拿到wait_lock后锁并非完全空闲(还有读者在场),写者就执行atomic_or(_QW_WAITING, &lock->cnts),点亮 bit 8。这一位一旦亮起,就改变了新读者快路径的命运:回看 3.5 节,读者快路径判断的是cnts & _QW_WMASK,而_QW_WMASK恰好包含了_QW_WAITING这一位。于是从此刻起,所有新来的读者在快路径上都会看到_QW_WMASK非零,被迫转入慢路径去排队——新读者被这面旗挡在了门外

  3. 等读者排空,再夺锁。在场的老读者陆续read_unlock退出,读者计数逐个递减。写者用atomic_cond_read_relaxed自旋等待,直到cnts恰好等于_QW_WAITING(读者计数归零、只剩自己竖的那面旗),再用 CAS 把它换成_QW_LOCKED,正式持锁。

这三步合起来,就是写者优先:写者一旦开始等待,就冻结了新读者的准入,保证在场读者有限时间内排空,写者不会被源源不断的新读者饿死。这正是对 3.3 节那个通病的解药。

一处对称的细节:_QW_WAITING之所以选在 bit 8、而读者计数从 bit 9 起,就是为了让“只剩等待旗”这个状态恰好等于_QW_WAITING这个干净的数值,第 3 步的等待条件VAL == _QW_WAITING才能写得如此简洁。编码的位布局是为算法服务的。

把 3.4 至 3.6 合起来看,cnts这个 32 位字就在几个状态间迁移,写者优先的“排空”机制一图可见:

cnts = 0

读者 add _QR_BIAS

更多读者加入 / 部分读者退出

写者 CAS 0 → _QW_LOCKED (快路径)

写者来了,atomic_or _QW_WAITING
此后新读者被 _QW_WMASK 挡进慢路径

在场读者陆续 read_unlock,计数递减

cnts == _QW_WAITING(读者排空)
CAS → _QW_LOCKED

write_unlock,wlocked 写 0

最后一个读者 read_unlock

空闲

读者共享

写者独占

写者等待

写者优先的关键:
祗旗后冻结新读者准入,
保证在场读者有限时间排空

3.7 读者慢路径与中断特例

再看读者一侧的慢路径(kernel/locking/qrwlock.c):

void__lockfuncqueued_read_lock_slowpath(structqrwlock*lock){if(unlikely(in_interrupt())){atomic_cond_read_acquire(&lock->cnts,!(VAL&_QW_LOCKED));return;}atomic_sub(_QR_BIAS,&lock->cnts);arch_spin_lock(&lock->wait_lock);atomic_add(_QR_BIAS,&lock->cnts);atomic_cond_read_acquire(&lock->cnts,!(VAL&_QW_LOCKED));arch_spin_unlock(&lock->wait_lock);}

它分两种情形:

  • 普通上下文:读者先把 3.5 节快路径里预加的那笔_QR_BIASatomic_sub减回去(此刻它还没资格算作在场读者),然后去抢wait_lock——排在写者队列的同一道门后面。轮到自己时再把计数加回来,用atomic_cond_read_acquire自旋等到_QW_LOCKED清零(写者放锁),即进入临界区并释放wait_lock。把读者也纳入wait_lock队列,是为了让读者和写者在这道门前公平排队,而不是读者一味插队。

  • 中断上下文的特例in_interrupt()为真时,读者跳过排队,只用atomic_cond_read_acquire自旋等到_QW_LOCKED清零就直接进入——注意它只看_QW_LOCKED,不管_QW_WAITING。这是一条刻意的例外:中断处理程序可能打断了一个正持读锁的进程上下文,若此时又强制中断里的读者去排在写者后面,而被打断的那个读者又要等中断返回才能read_unlock,就会形成死锁。所以只要写者还只是在等待_QW_WAITING)而未真正持锁(_QW_LOCKED),中断里的读者就允许越过它立即读取——用一处受控的“读者插队”换取避免死锁。

对照 3.6 与 3.7:普通读者、所有写者都要过wait_lock这道公平门,唯独中断上下文的读者享有豁免——这是qrwlock在“写者优先”与“中断安全”之间的精心权衡。

这两条分岔画成流程图,中断读者为何能“插队”就一目了然:

是(中断上下文)

否(进程上下文)

queued_read_lock_slowpath

in_interrupt()?

只自旋等 _QW_LOCKED 清零
不管 _QW_WAITING,允许插队

进入读临界区

atomic_sub 把预加的 _QR_BIAS 减回

arch_spin_lock(wait_lock)
排到写者队列同一道门后

atomic_add 把计数加回

自旋等 _QW_LOCKED 清零

arch_spin_unlock(wait_lock)

3.8 解锁:读者减计数,写者写一个字节

解锁路径极其轻量(include/asm-generic/qrwlock.h):

staticinlinevoidqueued_read_unlock(structqrwlock*lock){(void)atomic_sub_return_release(_QR_BIAS,&lock->cnts);}staticinlinevoidqueued_write_unlock(structqrwlock*lock){smp_store_release(&lock->wlocked,0);}
  • 读者解锁:一次atomic_sub_return_release,把读者计数减掉一个_QR_BIAS。release 语义(第 1 章 1.6 节)保证临界区里的读取都已完成,才让这次递减对他人可见。当最后一个读者退出、计数归零,等待的写者才会在 3.6 节第 3 步看到cnts == _QW_WAITING

  • 写者解锁:这里正是 3.4 节那个编码红利的兑现——写者无需对整个cnts做原子读-改-写,只用smp_store_release(&lock->wlocked, 0)往低字节写一个 0,就清掉了_QW_LOCKED。因为写者独占期间读者计数不可能变动(新读者早被_QW_WAITING挡住了),高位的读者计数与写者互不干扰,单字节写不会破坏它们。在强序的 x86 上,这条 release 写就是一条普通mov,不生成任何屏障指令——又一次印证第 1 章反复强调的“x86 解锁路径极廉价”。

3.9 误用与调试

qrwlock继承了spinlock的全部约束,再叠加读写锁特有的几个坑:

  • 持读写锁期间睡眠或跑长临界区。和第 2 章一样,自旋期间 CPU 空转,临界区里不能调用任何可能睡眠的函数,也不能跑得太久。需要长临界区或阻塞,请改用rwsem(第 6 章)。
  • 读锁递归 + 写者等待导致的自死锁。同一 CPU 在持有读锁的情况下再次请求读锁,若此时恰有写者已竖起_QW_WAITING,第二次读锁会被逼进慢路径去排在写者后面,而写者又在等这个读者退出——本 CPU 等自己,死锁。结论是:qrwlock的读锁不可递归
  • 企图把读锁升级为写锁。持有读锁再去write_lock同一把锁,会等自己手里的读计数归零,必然死锁。读写锁不支持锁升级,需要写就一开始拿写锁。
  • 中断上下文与进程上下文混用同一把锁却没关中断。虽然 3.7 节的中断特例避免了读者一侧的一类死锁,但写锁在中断里出现、而进程上下文持读锁未关中断,仍可能死锁。凡是会在中断里用到的读写锁,进程上下文侧要用read_lock_irqsave/write_lock_irqsave

调试手段与第 2 章一致:CONFIG_PROVE_LOCKING(lockdep)能识别读写锁的锁序颠倒、递归误用与中断上下文问题;CONFIG_DEBUG_SPINLOCK会检查未初始化使用、重复解锁等错误。


本章小结

qrwlock守护的是读多写少的短临界区:它在spinlock的“一律独占”之上,把独占权只留给写者,释放了读者之间的并行。它的全部状态压进一个 32 位原子字——低九位(_QW_WMASK)记写者持锁与等待,高位记读者计数;无争用时读者一次atomic_add加计数、写者一次 CAS 抢低位就能拿锁。它最关键的设计是用_QW_WAITING这面旗解决经典读写锁的写者饥饿:写者一旦开始等待就点亮这一位,逼退所有走快路径的新读者,让在场读者排空,从而写者优先、不被饿死;同时又用in_interrupt()的读者豁免避免了中断上下文的死锁。内嵌的wait_lock(一把qspinlock)把等待者串成 FIFO 队列,保证公平。回到第 1 章的公式“锁 = 原子操作 + 屏障 + 等待策略”:qrwlock的原子操作是atomic_add/CAS/atomic_or,屏障是 acquire/release,而它的等待策略最精巧之处,就在于用一位等待旗在读者并行与写者公平之间取得平衡。下一章我们转向osq_lock——一把 MCS 变体的乐观自旋队列,它是mutexrwsem乐观自旋的公共底座。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/4 6:48:10

2026年小程序商城哪个好?商品、订单、支付和会员能力对比

2026年小程序商城哪个好&#xff1f;商品、订单、支付和会员能力对比摘要&#xff1a;2026年搜索小程序商城哪个好&#xff0c;真正要比较的是平台能不能支撑商品、订单、微信支付、会员复购、营销活动、配送自提和售后维护。腾讯微信官网公开信息显示&#xff0c;截至2026年第…

作者头像 李华
网站建设 2026/9/4 6:45:48

Python实战:构建本地化游戏数据查询工具与数据分析平台

简介&#xff1a;这是一份面向Python初学者与明日方舟玩家的轻量级数据工具源码包&#xff0c;解决干员信息分散、练度管理低效、材料规划依赖手动查表等实际问题。资源共270个文件&#xff0c;以253个TOML格式配置文件&#xff08;存储干员/材料基础数据与别名映射&#xff09…

作者头像 李华
网站建设 2026/9/4 6:44:50

读懂cuDNN文件名:Windows下CUDA深度学习环境配置核心指南

简介&#xff1a;本资源是面向Windows平台深度学习开发者的NVIDIA cuDNN 9.1.0.70官方预编译库&#xff0c;专为CUDA Toolkit 12环境优化设计&#xff0c;适用于TensorFlow、PyTorch等主流框架的GPU加速部署与本地训练环境搭建。压缩包共32个文件&#xff0c;包含16个静态/导入…

作者头像 李华
网站建设 2026/9/4 6:44:24

教室级人脸签到系统:FaceNet落地实战与反常识设计

简介&#xff1a;这是一套面向计算机专业本科生与人工智能初学者的高分毕业设计级项目&#xff0c;聚焦课堂场景下的人脸检测与识别签到全流程实现&#xff0c;解决传统人工点名效率低、易代签等实际教学管理痛点。资源包含21个文件&#xff0c;主体为15个Python源码&#xff0…

作者头像 李华
网站建设 2026/9/4 6:44:19

数字版权管理如何重塑游戏所有权:从DRM技术到二手交易限制

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/4 6:44:11

常规行为对齐评估为何难发现LLM模型失配?安全评测盲区解析

如果你只关心“这个模型有没有通过安全测试”&#xff0c;Hacker-Opus 这个标题本身就是一个提醒&#xff1a;答案可能是“通过了”&#xff0c;但这个结论基本没有参考价值。研究题目传达的问题很直接——常规行为对齐评估难以发现模型失配。也就是说&#xff0c;一个模型完全…

作者头像 李华