关注了就能看到更多这么棒的文章哦~
Using LKMM atomics in Rust
By Daroc Alden
October 16, 2024
Kangrejos 2024
Gemini-1.5-flash translation
https://lwn.net/Articles/993785/
Rust 和 C 语言一样,拥有自己的内存模型(memory model),定义了多个线程并发访问相同数据时的行为方式。 然而,Linux 内核有着它自己的想法。Linux 内核内存模型(LKMM)与标准 C 内存模型和 Rust 模型都略有不同。在 Kangrejos 大会上,冯博群做了一个关于协调 Rust 和内核所使用的内存模型的必要性的演讲,包括一些可能的途径。虽然没有达成共识,但这仍然是一个积极讨论的领域。
冯博群解释说,问题在于 LKMM 做出了一些 Rust 内存模型没有做出的保证。由于编译器不知道这些保证,它(可能)会在进行优化时破坏这些保证。 唯一可行的解救位置是 C 和 Rust 代码之间的 ABI(应用程序二进制接口),它应该有一些双方都意识到的保证。 然而,在实践中,许多架构并没有在其 ABI 中指定任何关于原子操作或线程间交互的保证。 冯博群说,拥有一个列出相关保证的 ABI 并不能完全解决问题——跨语言链接时优化(LTO,link-time-optimization)仍然可能导致问题。
他表示,我们需要让这些内存模型承认彼此的存在。看起来纯粹的 Rust 代码似乎不需要关心 LKMM,但即使如此,也不完全正确。LKMM 保证,如果一个线程向一个变量写入数据,然后唤醒另一个线程,那么第二个线程将会看到该写入操作。而 Rust 并不知道这一点,因此理论上编译器可以重新排序从而在调用唤醒另一个线程的函数之后该写入操作。所以任何 Rust 代码都会受到 LKMM 所需内容的影响。
Andreas Hindborg 询问,是否真的不存在完全用 Rust 编写的内核驱动程序可以不使用 LKMM 的情况。 冯博群举了一个例子,说明即使是一个简单的多线程原子计数器最终也会涉及到 LKMM。 Paul McKenney 总结了这个问题:现有的很多边界,有人可能会建议进行区分,并说一种内存模型适用于边界的一侧,而另一种模型适用于另一侧,例如函数调用,但我们不会这样做。 McKenney 说,执行排序必须是一个全局性的特征,否则工具编写者将会遇到麻烦。
Benno Lossin 质疑,如果 Rust-for-Linux 项目无论如何都需要与 LKMM 匹配的版本,那么为什么 Hindborg 想要将 Rust 的内存模型(特别是它提供的原子操作)用于代码的隔离部分。 Hindborg 说,与任何将来尝试为 Rust 生成兼容 LKMM 的原子 API 不同,现有的 Rust 原子 API 现在就可以使用,他不想被拖慢速度。 McKenney 建议采用分阶段的方法——最终目标是实现完全兼容,但就目前而言,策略性地放置完整的内存屏障就足够了,即使它们有更高的开销。
Alice Ryhl 建议添加新的类型,这些类型旨在最终匹配 LKMM 的语义,在内部使用 Rust 原子 API 来实现它们,然后在以后重新设计它们。 Lossin 不同意这种做法,他说 API 设计才是难点,现在使用 Rust 原子 API 更有意义,等 API 真的存在了再修复它。 Gary Guo 建议采用一种完全不同的方法:检查编译后的机器代码是否遵守 LKMM,而不管源语言是什么。他说,如果我们可以在 Rust 中使用 LKMM 原子 API,我们就应该直接使用它们。
冯博群的演讲得出了同样的结论:Rust-for-Linux 项目应该在 Rust 中实现与 LKMM 兼容的原子 API 和其他相关的抽象,并且只使用这些 API。为了向那些可能不太熟悉 LKMM 的人解释这意味着什么,冯博群重点介绍了一些具体的差异。 首先,所有原子变量都被自动假定为易失的(volatile)——因此编译器不能为它们发明无关的加载或冗余的存储。 其次,可以使用不同的原子排序,包括“完全有序(fully ordered)”,它充当任何其他原子操作的完整内存屏障。 失败的比较并交换操作被视为松散内存操作(relaxed memory operations)(而不是有两个不同的版本,其中一个总是松散的,而另一个不是)。 最后,LKMM 增加了地址、数据和控制依赖关系,这些依赖关系会影响排序。 其中一些依赖关系特别微妙——例如,一个条件读取原子变量的 if 语句只对后续的原子写入操作进行排序,而不对后续的原子读取操作进行排序。
人们很容易认为,由于 Rust 代码和 C 代码一样编译成相同的 LLVM 中间表示,所以编译器应该能够以相同的方式遵守 LKMM 的规则。 不幸的是,C 编译器实际上已经给试图遵循 LKMM 的 C 代码带来了问题。 冯博群举了一个代码试图利用他提到的控制依赖关系的例子。 想象一个 if 语句,它从一个原子变量中读取数据,然后在 if 语句的两个分支中都写入一个不同的变量,然后再继续做两件不同的事情。 编译器可以并且确实会将相同的写入操作提升到 if 语句之外——这对于普通代码来说不会造成问题,但它会改变原子操作的顺序,并可能破坏程序员所依赖的保证。 在内核中,这就是 volatile_if()
和 ctrl_dep()
宏的原因,它们生成适当的编译器屏障以防止这种情况发生。
Guo 询问 Rust black_box()
函数是否可以达到类似的目的,冯博群对此表示同意。 McKenney 怀疑它是否有助于控制依赖关系——Guo 的一个快速测试证实了这一点。 但是还有其他基于 Rust 宏的潜在解决方案。
无论如何,解决方案肯定需要更多地关注如何在 Rust 代码中使用原子 API。冯博群总结道,虽然使用更简单的实现很诱人,但这是内核——因此没有真正的方法可以避免关心性能和架构细节。 然而,还是有希望创建一个 Rust-for-Linux 项目可以实现的通用 API 的。 Rust 可能很快就会以 Atomic
类型的形式拥有泛型原子 API,它统一了所有现有的原子 API。 从理论上讲,内核开发人员可以实现相同的 API,但基于 LKMM 原子 API。
随着会议接近尾声,Ryhl 说她不关心他们最终是否实现了任何特定的 API——她只是认为他们应该专注于先做一些简单的事情。 在与会者就可能是什么达成一致之前,会议就结束了,但无论如何,冯博群想要解决的内存模型一致性问题肯定正在被积极考虑。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
欢迎分享、转载及基于现有协议再创作~
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~