Linux内核的魔法-RCU解密

        现在的CPU核心是越来越多了,在一个多核CPU的世界里,会遇到一个令人特别头疼的问题,怎么让一份数据在被无数个线程同时疯狂读取的时候,还能被我们安全的去修改它。今天,就来聊一个在Linux内核中解决这个问题的黑魔法,它叫做RCU,全称是Read,Copy.Update,也就是读,复制,更新。这是一种非常非常聪明的同步技术。尤其是对那种对读取速度要求高到变态的场景下,它简直能把传统的读写锁给甩出好几条街。

        RCU代表读,复制,更新,听着名字,感觉是不是特别直白,好像三言两语就把自己的工作交代的一清二楚了,但问题是,如果事情真的就这么简单,它又凭什么被叫做LINUX内核里的魔法呢。所以,这个名字会不会是恰恰在迷惑我们,把它真正的秘密给藏起来了。那我们就先顺着这个名字往下挖一挖。看看到底是怎么回事。

        如果只是从字面上去理解,RCU就好像是在说一个很普通的数据发布流程,这个流程本身是相当经典了:

  1. 第一步,读数据。
  2. 第二步,不在原地改,把它复制一份出来,在副本上改。
  3. 第三步,也是最关键的一步,用一个原子操作,把那个指向数据的指针“咔”一下瞬间指向新的,改好的副本。

这么以来,所有的读者,要么看到的是完整的旧数据,要么看到的是完整的新数据,绝对不可能看到改到一半,乱七八糟的,不完整的数据。下面是一个形象的比喻:

        读写锁像图书馆的“阅览室规则”:所有人借书要登记,写书时清场。RCU 则像“活页本”:你要改一页时,复印那一页改好再放回去,等所有人读完旧页才扔掉旧页。读者永远不用排队。

这个方法非常棒,但是它并不新鲜,很多地方都在用,所以这不是RCU真正的魔法所在,虽然这三步操作是它的前提,但真正让RCU独一无二,与众不同的,其实和读取,复制或者更新关系不大,而是在这三步操作完成之后,接下来要做的那件事。真正的魔法要来了。

延迟销毁

RCU最核心,最天才的创新,是它把更新这件事,给巧妙的拆成了两步,移除和回收,这是一种延迟销毁的思想,就好比处理垃圾,你不是把旧东西扔进焚化炉,而是先把它从大家眼前拿走,藏起来,放到一个待处理的区域。

在移出阶段,更新者的任务很简单,就是把指向旧数据的指针给断开,让新来的读者找不到它就行了。然后,在未来的某个时间点,一个独立的回收阶段才会去真正释放这块旧数据的内存,但这里就引出了RCU最核心的那个难题,它到底怎么知道,什么时候去回收那块旧数据才是安全的呢?万一当时还有读者正在用它怎么办,要知道RCU为了追求极致的读取速度,它的读者在读取数据的时候是完全“隐身”的,它们不会加锁,不会登记,读完之后也不会通知任何人,所以,在这种情况下,怎么知道所有的读者都已经走光了?这才是RCU真正要解决的那个终极难题。

RCU如何感知读者离开临界区

        为了解决这个大难题,RCU就引入了两个非常核心,而且特别绝妙的概念,一个叫做宽限期,英文是Grace Period,另一个是静止状态,Quiescent state,就是这套机制,让RCU能够在完全不打扰读者的情况下,优雅的感知到它们什么时候都已经离开了。

        先说宽限期,你可以把它想象成一个安全等待窗口,当更新者把旧数据移除之后,它不会马上动手回收内存,而是会说,好了,现在开始,启动一个宽限期,RCU的整个机制会向我们保证,只要这个宽限期一结束,那么所有在宽限期开始前可能正在访问旧数据的读者,就一定一定都已经读完走人了。

        那RCU是怎么做到这么神奇的保证的呢?答案就是静止状态,这个概念不复杂,说白了,只要一个CPU它当前没有正在执行被rcu_read_lock/rcu_read_unlock这两个标记包起来的代码,也就是没有在RCU的临界区内,我们就可以说,这个CPU正在处于一个静止状态。

        和一般的定义方式不同,静止状态采用了负定义的方式,它的定义没有说明它是什么,而是说了它不是什么,利用了排除定义的方式,这种定义方法兼顾了完备性和扩展性,如果 CPU 不在 rcu_read_lock() 和 rcu_read_unlock() 之间,那它此刻一定不持有任何 RCU 保护的对象的引用,这个 CPU 不会阻止任何旧数据的回收。

静止状态 = { CPU的所有可能状态 } - { RCU读临界区内 }

        其实在LINUX内核运行过程中,这种静止状态到处都是,非常自然。比如说,当一个CPU发生了上下文切换,那它肯定退出了当前的RCU读取区域了,或者CPU没事儿干,进入了idle循环,甚至从内核态切换到用户态去执行程序,这些时刻,都百分百是静止状态。RCU就是靠检测这些内核时刻都在发生的事件来工作的。只要一个 CPU 没有戴着 RCU 的“安全帽”(不在 rcu_read_lock/unlock 区域内),它就可以随时报告“我已静止”。哪怕它只是从内核返回用户态的那个瞬间,也算一次“QS”。LINUX系统中报告QS状态事件的上下文总结如下面文章所述:

https://blog.csdn.net/tugouxp/article/details/160312032?sharetype=blogdetail&sharerId=160312032&sharerefer=PC&sharesource=tugouxp&spm=1011.2480.3001.8118

        我们把整个流程串起来走一遍,看看RCU到底是怎么运作的。第一步,更新者完成了指针的切换,从这一刻起,旧数据对于新来的读者来说,已经找不到了,我们就管它叫僵尸数据,紧接着,一个宽限期就正式拉开了帷幕,注意,任何在指针切换之前已经开始读取的哪些老读者,它们手里还攥着指向那个僵尸数据的引用,所以,它们可以继续慢悠悠的完成自己的工作,不受任何影响。在这个宽限期里面,RCU就只做一件事,等,等什么呢,它会静静的观察,直到它确认,系统里的每一个CPU都至少发生过一次静止状态。这就像是在确认,一栋大楼里的每个办公室灯都已经关过了,人肯定回家了,一旦RCU确认所有的CPU都已经打卡下班了,那么宽限期就正式结束了。在这一刻,RCU就能百分百确定,绝对没有任何人还在用那个旧的僵尸数据了。宽限期不是“主动等时间”,而是“被动等事件”, 等所有CPU都进入宽限期这一事件。最后,最关键的一点来了,当事件到达,现在释放内存,是绝对安全的。

这个等不是傻等,而是事先加入了唤醒策略的:

  • 写者调用 synchronize_rcu() 时,把自己加入等待队列然后睡眠

  • 内核在每次时钟中断、进程切换、退出用户态等时机,检查当前 CPU 是否经历了 QS

  • 当所有 CPU 都上报过 QS 后,唤醒等待的写者

所以宽限期的长短,取决于最慢的那个 CPU 什么时候发生一次调度/中断。如果一个 CPU 在死循环且禁止抢占,则它就不会报告静止状态, 宽限期会一直被这个慢读者拖住,宽限期就永远不会结束,这也是为什么 RCU 读临界区必须短暂且不能睡眠。

RCU 的核心就是:用一个“等所有人都离开”的宽限期,换取“读者永远不用等”的极致性能。

这个设计特别巧妙,但是作为LINUX内核开发者该怎么用它呢?我们看以下RCU的核心API,就这么几个,分工特别明确:

  1. rcu_read_lock()/rcu_read_unlock():标记读侧临界区的开始/结束。
  2. rcu_dereference():安全地获取一个用于读取的指针。
  3. rcu_assign_pointer():发布一个指针的新版本。
  4. synchronize_rcu():启动,阻塞并等待一个宽限期的结束。
  5. call_rcu/kfree_rcu:安全地延迟执行回调释放内存,直到所有可能正在访问该内存的读侧临界区完成。

用RCU和用熟悉的锁有什么区别呢?下图作了一个对比,这样就一目了然,左边是传统的互斥锁,你看它在释放锁之后,立刻就回收内存,非常直接,再看右边的RCU版本,他再移除数据和内存回收之间,调用synchronize_rcu默默的启动插入了一个完整的宽限期,启动并等待这个宽限期走完,实现了RCU的魔法。

在实现中,RCU 采用分层组合树(Combining Tree)架构,将 per-CPU 状态通过 rcu_node 树逐级汇聚,实现 O(log N) 的宽限期检测可扩展性。RCU 子系统由四个核心数据结构构成:rcu_head(回调头)、rcu_data(per-CPU 状态)、rcu_node(组合树节点)、rcu_state(全局状态)。rcu_node 组合树是 RCU 可扩展性的核心。叶节点覆盖最多 16 个 CPU (RCU_FANOUT_LEAF),非叶节点扇出为 64 (RCU_FANOUT)。QS 报告从叶向根逐级传播,所有CPU都完成静止状态后,根节点设置当前的宽限期结束,就可以安全调用当前宽限期开始前的释放回调清理内存了。

RCU的应用场景

说到这里,你可能会觉得,RCU简直是神奇,是不是以后就可以把锁全都仍了,改用RCU了。还真不是,RCU有适合它自己的应用场景。下面这句话完美的概括了RCU的理想国,什么时候用RCU最爽呢,就是当你绝大部分时间都是在读数据,更新操作非常非常少,并且你的读者能够容忍自己偶尔读到那么一点点旧数据的时候,这个时候,RCU就能发挥出它最大的威力。

"RCU的天堂是:更新几乎从不发生,而且你不在乎数据是否稍微有点儿不一致。"

所以总结一下适用RCU的条件:

  1. 你的功能实现读是占绝对主导地位的。
  2. 你对读的性能要求极高。
  3. 你能接受一点点的数据不一致,读到老数据。
  4. 你对更新的速度没那么敏感。

全局唯一的GP线程

全局唯一的GP线程(rcu_gp_kthread)为RCU带来了多个核心便利,使GP状态转换是天然串行化,不需要对GP状态本身加锁,便利还包括:

  1. 逐node持锁设置qsmask:每个leaf node的qsmask设置在 rnp->lock 保护下原子完成,与该CPU上的QS报告和读者入队互斥

  2. 非PREEMPT_RCU:读者不可抢占,schedule就是QS。qsmask设置后的新读者在其时间片内完成或跨到下一个GP,不阻塞当前GP

  3. GP初始化串行性:rcu_gp_cleanup() 完全完成后才开始 rcu_gp_init(),确保不存在"上一个GP还没完全结束,下一个GP就开始"的竞争

  4. 内存屏障:rcu_seq_start/end() 中的 smp_mb() 和 raw_spin_lock/unlock 的隐式屏障保证操作的全局可见性顺序

RCU 语义易混淆点

当没有旧数据需要延迟回收时,例如添加新元素时,不需要 调用synchronize_rcu() 

或 call_rcu(),下面是LINUX代码中的具体例子:

RCU 的静止状态只针对删除或替换操作,因为这两种操作会产生一个“旧的、可能仍被读者访问”的副本,而添加新元素:

  • 你分配一个新节点,填充数据

  • 通过 rcu_assign_pointer() 把它挂到共享数据结构上(比如链表尾部、哈希桶中)

  • 这个新节点在挂上去之前没有任何读者能看到它

  • 挂上去之后,新读者才会看到它

整个过程没有产生任何需要等待读者离开的内存块。因此不需要等待宽限期,也就不需要 synchronize_rcu 或 call_rcu。总结一下:

总结

所以,回到本文开始的那个问题,RCU最大的秘密,其实不是其名称代表的读,复制,更新的发布流程,那只是个幌子,它背后真正的魔法,是那个设计的及其优雅,效率极高,并且对读者几乎零干扰的延迟回收机制。

数据年度:2000-2024包含的82个统计指标如下:年份省份城市区县区县代码所属地域胡焕庸线行政区域土地面积(平方公里)乡及镇个数(个)乡个数(个)镇个数(个)街道办事处个数(个)村民委员会个数(个)年末总户数(户)乡村户数(户)年末总人口(万人)乡村人口(万人)户籍人口数(万人)年末单位从业人员(人)城镇单位在岗职工人数(人)乡村从业人员数(人)农林牧渔业从业人员数(人)年末第二产业单位从业人员(人)年末第三产业单位从业人员(人)农业机械总动力(万千瓦特)农作物总播种面积(千公顷)机收面积(公顷)固定电话用户(户)移动电话用户数(户)宽带接入用户数(户)地区生产总值(万元)第一产业增加值(万元)第二产业增加值(万元)人均地区生产总值(元/人)第三产业增加值(万元)工业增加值(万元)农业增加值(万元)牧业增加值(万元)城镇单位在岗职工平均工资(元)城镇居民人均可支配收入(元)农村居民人均可支配收入(元)地方财政一般预算收入(万元)各项税收(万元)地方财政一般预算支出(万元)城乡居民储蓄存款余额(万元)年末金融机构各项贷款余额(万元)出口额(美元)实际利用外资金额(美元)农作物总播种面积(千公顷)常用耕地面积(公顷)机收面积(公顷)设施农业占地面积(公顷)农用机械总动力(千万瓦)粮食总产量(吨)棉花产量(吨)油料产量(吨)肉类总产量(吨)农林牧渔业总产值(万元)农业增加值(万元)牧业增加值(万元)规模以上工业企业数(个)规模以上工业总产值(万元)城镇固定资产投资完成额(万元)全社会固定资产投资(万元)社会消费品零售总额(万元)房地产开发投资(亿元)普通小学学校数(个)普通中学学校数(个)普通小学专任教师数(人)普通中学专任教师数(人)普通小学在校生数(人)普通中学在校学生数(人)中等职业教育学校在校学生数(人)医院、卫生院床位数(床)医院和卫生院卫生人员数_卫生技术人员(人)医院和卫生院卫生人员数_执业医师(人)各种社会福利收养性单位数(个)各种社会福利收养性单位床位数(床)全社会用电量(万千瓦时)城乡居民生活用电量(万千瓦时)废气中氮氧化物排放量(吨)废气中烟尘排放量(吨)工业废气中二氧化硫排放量(吨)艺术表演场馆数_剧场、影剧院(个)公共图书馆总藏量(千册)体育场馆机构数(个)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

papaofdoudou

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值