LWN:RCU近期的改动!
关注了就能看到更多这么棒的文章哦~
Recent RCU changes
By Jake Edge
May 10, 2022
LSFMM
DeepL assisted translation
https://lwn.net/Articles/894379/
2022 年 Linux 存储、文件系统、内存管理和 BPF 峰会(LSFMM)的文件系统和内存管理峰会上,Paul McKenney 介绍了过去几年中对 read-copy-update(RCU)子系统进行了哪些修改。他首先快速概述了什么是 RCU,以及它存在的原因。但他并没有真正深入探讨,因为他认为许多话题中每一个都可能需要 90 分钟才能深入了解,但他确实提供了一些关于最近在 RCU 方面所做工作的描述。
RCU 仍在积极开发中,McKenney 说,这个信息让他在几年前的一次会议上与一位学术界人士交谈时让对方大吃一惊。他没好意思告诉他,locking (加锁)和 atomic(原子操作)也在积极开发中,他笑着说。"但这就是我们现在的情况"。
RCU review
[Paul McKenney]
最主要的问题是,在 Linux 这样的操作系统中达成共识是成本很高的。虽然 50 年前的自己也会对这一点感到震惊,但事实证明,光速太慢,原子太大;这些东西在如今的并发软件(concurrent software)中造成了大量问题。他说,RCU 处理这个麻烦的方式是在时间(time)和空间(space)上都进行同步。核心的 RCU API 被分成两组时间和空间的调用(temporal and spatial calls)。RCU 是一种允许数据结构的 reader 在 updater (更新者)做出改变时可以继续使用的方法;RCU 最常被用于各种类型的 linked list 链表。
他展示了一张有四个象限的幻灯片,描述了 RCU 在时间方面的工作方式。其基本思想是,reader 在读取数据之前调用 rcu_read_lock(),在完成之后调用 rcu_read_unlock()。这允许 updater 删除加锁了的数据,只要它在实际释放数据之前调用 synchronize_rcu()(或能接收并处理它用 call _rcu() 设置的 callback 函数)。旧的数据可能仍在被 reader 使用中,但 RCU 保证在它从 synchronize_rcu() 返回之前,所有的 reader 都已经完成了 unlock 解锁操作。四个象限中显示了 reader 的 lock/unlock 以及 updater 删除数据、synchronize_rcu() 并且释放旧数据的可能顺序。RCU 阻止了一些可能会导致严重错误行为的顺序,也就是在 lock 之后进行删除,在 unlock 之前释放旧内存。他说,如果这种情况真的发生,那就是 RCU 的一个错误。
他还研究了全局达成协议的开销,将使用 reader-writer lock 的情况与使用 RCU 的情况进行对比。当一个 updater 想使用 reader-writer lock 来修改数据时,在该 lock 传播到每个 reader 之前都会耗费一段时间,然后才可以进行 update 动作,但之后还会耗费一些时间来等待该更新能传达给所有的 reader。在上面说的这两段时间里,reader 都会被卡住,直到能再次读取才行。使用 RCU 的话,相应的时间段就不会再被浪费了,reader 可以继续进行读取,只是可能拿到的是旧版本的数据,而 updater 只需要等到 grace period 结束,就可以确保所有 reader 后续的 read 操作都能看到新的数据。
他展示了一些图表来解释为什么 RCU 的复杂性是可以接受的。RCU 比 reader-writer lock 表现得更好,而且随着线程数量的增加相应的扩展性也会好很多。此外他说,关键区(critical section)越短,CPU 越多,RCU 的效果就越好。RCU 还可以防止一些死锁问题。
关于 RCU 的更多信息可以在 LWN 内核索引的 RCU 专门条目中找到。《The RCU API, 2019 edition》就是一个很好的启蒙材料。
Changes
他的 "RCU 快速回顾" 到此结束。他列出了一份他想谈论的 RCU 的八个改动。他猜测哪些是对听众最重要的,并对它们进行更深入的讲解。首先从整合各种 flavor 开始。
早在 2018 年的某一天,他收到了一封来自 Linus Torvalds 的电子邮件,抄送给 security@kernel.org,其中描述了 RCU 的某个使用场景中的一个漏洞。reader 使用 *_sched() 这种 API 来进行加锁,但 updater 使用了 synchronize_rcu()(本来应该用 synchronize_sched())。在 Linux 4.19 和更早的版本中,需要将这两种方式配对使用。由此产生的 bug 是会出现 use-after-free 的问题,从而导致了这个安全漏洞。
Torvalds 问是否有什么办法处理这个问题。最后决定的方案是让 synchronize_rcu() 对所有种类的 API 都有效。McKenney 说,这花了 "我一年的时间来实现它",但很好地解决了这个问题,除了一个小缺点:如果需要向 4.19 或更早的版本来 backport 一些代码就很麻烦。Amir Goldstein 问道,"这是否意味着我们现在拥有诱饵和陷阱了?" McKenney 同意这一点,但 synchronize_rcu_mult()有一张 "免罪金牌":它可以在那些早期内核中被调用,并可以在当前各种类型的 call _rcu() 中使用。它会对所有调用依次进行 chain 式调用,并在返回前等待所有调用完成,这模拟了较新版本中的 synchronize_rcu(),但要付出一些额外的延迟。
在 5.4 中,Joel Fernandez 为 list_for_each_entry_rcu()(以及 hlist_* 这类变种)增加了 lockdep 支持。这些调用可以接受一个可选的 lockdep 表达式,这就消除了 API 中对这些调用的更多变种的需求。
Uladzislau Rezki 在 5.9 中添加了一个单参数版本的 kvfree_rcu()(kfree_rcu()是它的另一个名字)。以前,kvfree_rcu()需要两个参数:要释放的对象,以及该对象中包含 rcu_head 结构的字段名。现在,将 rcu_head 添加到对象中是个可选动作了,在这种情况下,不需要在参数中提供字段的名称了。如果对象的结构很小,rcu_head 增加的额外开销占比就比较大了。两个参数的版本仍然是支持的,并且一如既往地从不 sleep,但新版本可以在系统没有内存时 sleep。这是一种权衡:你可以使用更小的结构,但它可以 sleep,他说。
RCU 有一些新的变体,用于某些专门的使用场景。他没有说得太详细,但希望人们知道一下 RCU Tasks Rude 和 RCU Tasks Trace,因为它们可能会出现在 traceback 里面。它们主要用于对 trampolines 进行 tracing,他建议那些认为应该使用它们的人在使用这些 API 前与他或其他有经验的用户联系一下。RCU Tasks 从 3.18 开始就存在了,但 Rude 和 Trace 是在 Neeraj Upadhyay 的帮助下加入到 5.8 中的。
可以轮询(polling) 等待 grace period 结束,而不是调用 synchronize_rcu() 并进行等待,这个功能可以追溯到 3.14。轮询是先获得一个 cookie,然后最终将 cookie 传递给 cond_synchronize_rcu()。这个方法是可行的,但不能用于不允许 sleep 的情况。此外,获取 cookie 并不意味着 grace period 已经实际开始了,这在某些使用场景中可能会有问题。在 5.12 中,API 里又加入了一些函数,start_poll_synchronize_rcu()和 poll_state_synchronize_rcu(),以及用于 sleepable RCU 的*_srcu()变体,以便来支持这些使用场景。然而,在使用它们时都有一些需要注意的地方。
在 realtime 和 HPC 社区最感兴趣的功能就是 run-time callback offloading (以及 deoffloading)的支持,这是由 Frédéric Weisbecker 在 5.12 中加入的。通常情况下,RCU callback 都是在它们正在排队的 CPU 上执行的,但这可能会干扰到 CPU 上运行的其他 task。所以有一种方法可以将这些 callback 来 offload 到内核线程(kthread)中,然后将这些 kthread 分配到其他地方执行。
传统上是在启动时通过选择哪些 CPU 将被用于 callback 来完成指定执行的任务的,而且不重启就不能改变。Weisbecker 添加了一个基础设施,允许在运行时来改变这些指定关系;一个 CPU 在启动时被标记为 "可能被 offload",然后它可以在任何什么时候切换为 offload 模式或切换回来。目前,有一个内核内部函数来做这件事,但 McKenney 认为后续还是会将其跟用户空间的接口关联起来。
另一个功能是为 sleepable RCU(SRCU)代码提供 "内存减肥(memory diet)"。以前它会根据 NR_CPUS 来分配一个数组,这是内核可以处理的最大 CPU 数量。这个数字有时被发行版提供商设置为 4096,尽管绝大多数运行系统中的 CPU 数量都远没这么多。因此,现在不是在构建时来分配 array 了,而是在运行时来根据实际存在的 CPU 数量进行分配。这将在 5.19 版本中实现。
5.19 版的另一个功能是由 Rezki 贡献的实时加速宽限期(realtime expedited grace period)。McKenney 简要介绍了 RCU CPU-stall timeout 的历史。在 1990 年代,Dynix/PTX 使用的是 1.5s;在 2000 年代,Linux 使用 60s,这让他有些失望。在 2010 年代,Linux 下降到 21s;现在提出了一个 patch 来改为 20ms。在安卓系统上,这个 expedited grace period 的超时是 20ms,而在其他系统上将保持 21s。
为了使其发挥作用,还需要 Kalesh Singh 的一些额外 patch。通常情况下,expedited grace period 是由 workqueue 来驱动的,并且像普通的用户空间进程一样,用 SCHED_OTHER 这个 schedule class 来运行。这些 patch 将会在 SCHED_FIFO 调度类(schedule class)中增加一种新的 expedited kthread,他说这就像是 "强效药"。它仅限于少于 32 个 CPU 的系统,没有 realtime,并且启用了优先级提升(priority boosting)功能。他说,测试结果令人印象深刻,latency 降低了三个数量级,大约降低到 2ms。这就像是一种在快速路径(fast path)上有 expedited grace period 的实时系统;"如果你去年告诉我会有这种程度的优化,我一定会当面嘲笑你。"
Future
他说,他今年已经 100 岁了,也可能是 40 岁,但以十进制的话当然是 64 岁。他预计会继续这些工作,也指出他的父亲和祖父们一直工作到 90 岁左右,但 "大自然母亲的退休计划" 事实上都是我们所有人的归宿,所以需要做好准备。他列举了未来可能会致力于的一些事情,但指出,那些他看不到的事情会使这个情况变得复杂。需要有对 RCU 有很好理解的人,在这些事情出现时进行处理。
他回顾了截止 2017 年 4 月的两年时间里对 RCU 的 commit 列表(所以这是五年前的情况)。其中有 46 个贡献者,其中大多数人贡献了一个 patch,而 McKenney 贡献了绝大多数的 patch(288 个 patch, 占比 74%)。从 2022 年 4 月开始看前两年的情况的话,显示有 79 个贡献者,McKenney 所提交的 patch 比例下降到 63%(503 个补丁)。patch 总数量增加的一个原因是,自从他在 Facebook 开始工作以来,他集中精力为 RCU 增加更多的分布式测试。
总的来说,这个趋势是好的。最近有一些开发者也深入到 RCU 内部做了大量工作,这很好。然而仍有很多工作要做,他说。他多年来注意到的一件事是,一旦有一个开发者表明他们可以在 RCU 上工作,那么有一些公司就会付他们很多钱来让他们去做其他事情。这是好事,因为具有一些 RCU 知识的人分布在社区各处。最近,他注意到不少开发人员坚守在 RCU 这个领域,这就更好了。
他说,RCU 知识和理解需要在整个社区内更好地进行传播。他推荐了两个他所做的演讲,作为这方面的起点(https://www.linuxfoundation.org/webinars/unraveling-rcu-usage-mysteries/ 和 https://linuxfoundation.org/webinars/unraveling-rcu-usage-mysteries-additional-use-cases/ ),但还需要更多。还有一个更普遍的问题是,如何为一个特定的问题选择正确的同步工具,毕竟 RCU 并不总是正确的选择,这也是另一个需要在内核社区内更好地理解和宣传的领域。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~