Rust内核代码的内存模型

Rust内核代码的内存模型



这篇文章讨论了Rust在Linux内核中的应用,特别是关于内存模型的问题。文章作者是Johnson Corbett,发表于4月3日。

Rust编程语言与C语言在许多方面存在差异。这些差异让用户喜爱Rust,但当Rust代码集成到以C为主的系统,尤其是操作系统内核这种非典型C程序中时,可能会带来兼容性问题。内存模型就是一个很好的例子。

编程语言对内存的理解方式非常基础和深奥,以至于许多开发者不需要深入了解。但在内核开发中,这种对内存模型一无所知的情况是不允许的。因此,最近关于如何为Rust内核代码选择内存模型的讨论引起了人们的关注。

内存模型将系统的内存视为可以直接供任何CPU访问的简单直接数组。这是一种直观的理解方式,但现实情况远比这个复杂。内存访问速度较慢,所以要投入大量精力来尽量减少内存访问次数。现代系统出于这个目的构建了多级缓存。如果没有这些缓存,性能就会大幅下降,严重影响诸如视频娱乐、网络诈骗和虚拟货币交易等活动。虽然这些活动不都是好事,但都会受到影响。

多级缓存可以提高计算速度,但也带来了一个问题:系统中的每个CPU不再能够看到完全相同的内存内容。如果一个CPU在本地缓存中修改了一些数据,另一个CPU读取这个数据时可能还看不到这些修改。精心安排顺序执行的操作,在系统其他地方的执行顺序可能完全不同。这类似于狭义相对论中的情况,事件的顺序取决于观察者。

毋庸置疑,这种不确定性可能在操作系统内核中造成混乱。CPU具备特殊操作,可以确保给定的内存存储在整个系统中同时可见,但这些操作执行速度较慢,需要谨慎使用。现代CPU提供一系列屏障操作,可以用更低的性能开销来正确排序对数据的访问。关于这些操作的概述,可以参考memory barrier相关资料。

使用这些屏障操作可能有些复杂,并且特定于体系结构,因此人们创建了一些通用接口来尽可能简化这个操作。内存模型将屏障的使用规范与所有简化屏障使用的接口结合起来,描述如何在并发环境下安全访问数据。

David China在2020年说:”我认识的大多数优秀程序员一看到这些东西就吓跑了。正如多年前所说,理解memory barriers这个文档对于成为内核开发者来说是一个非常高的门槛。它不仅仅是memory barrier,几乎也是developer barrier。”

C11标准定义了一些原子类型和原子操作,C++也提供了自己的原子类型。内核社区虽然偶尔讨论过使用C的原子类型,但由于种种原因,这种想法从来没有真正实施过。相反,内核定义了自己的内存模型。这个模型在臭名昭著的memory barriers文件中描述,有时被称为LKMM(Linux Kernel Memory Model)。很少有内核开发者能够详细理解这个模型,许多人都觉得它太过细微难懂。但它支配着内存访问的底层工作方式和原理。

将Rust引入内核的早期担忧之一是Rust语言本身缺乏自己的内存模型。这个缺口已经填补了,Rust的内存模型看起来非常像C++模型。负责维护内核内存模型的Paul E. McKenney认为最好为内核中的Rust代码制定一个正式的模型。他将自己的结论发布到Linux内核邮件列表中,认为Rust代码应该遵守内核的内存模型,并附上了一个初始的补丁集,展示了具体的做法。

使用内核模型的理由很简单:内核开发者更熟悉LKMM,并且当Rust代码与C代码交互时,它必须使用C代码使用的模型。学习一种内存模型已经很困难了,要求开发者学习两种模型才能使用内核,这不会有好的结果。更糟糕的是,当Rust和C代码交互时,每个都依赖各自的内存模型来确保数据的正确排序,这可能会导致更糟的结果。因此,只要内核有自己的内存模型,Rust的代码就必须使用它。

Kent Overstreet指出了这种方法的一个缺点:内核仍将与其他Rust代码不兼容,并且无法轻松整合它们。他建议也许内核的内存模型可以重建在C或C++原子操作之上,这样支持Rust模型就更容易了。但是,考虑到Linux强烈反对任何类似的更改,这种情况就不太可能发生。

Linus Torvalds的论点之一是基于语言的内存模型对于应用内核来说不够可靠。这不是一个可以快速解决的问题,C++内存模型可能在10年后才会可靠。但是再过10年后,我们就可以放弃对非可靠平台的支持。他说:”我不理解为什么人们认为我们不想自己搞一套。”

Paul E. McKenney补充说,内核的内存模型涵盖了许多用例,例如对同一变量进行混合大小的操作,这些用例对内核很重要,但在Rust模型中没有被解决。他建议也许可以在内核操作之上实现Rust模型的一个子集。

内核项目自己实现内存模型的另一个原因是,无论如何,内核开发者都需要深入熟悉他们所支持的架构。没有大量的体系结构特定代码就无法创建内核,既然这样,让体系结构也定义原子操作等内容只是一个非常小的细节,相对还比较简单。

Linux坚持使用内核内存模型的另一个原因是,他们认为C++模型从根本上设计错误。C++模型的一个关键方面是,暴露给并发访问的数据被赋予一种特殊的原子类型,并且编译器会在访问这个数据时自动插入正确的屏障。这种模型可以确保开发者永远不会忘记使用适当的原子操作,这是一个吸引人的特性。

但这不是内核模型的运作方式。在内核的内存模型中,确定数据访问方式的不是数据的类型,而是访问发生的环境。一个简单的例子是受锁保护的数据:当锁被持有时,这个数据实际上并不是共享的,因为锁持有者具有独占访问权,因此不需要昂贵的原子操作。而是当锁被释放的时候,一个简单的屏障就够了。在其他没有锁的情况下,可能需要原子操作来访问数据。

Linus Torvalds认为内核处理共享数据的方法更加合理。他说:”我个人认为’底层数据必须是原子的’这个观点从根本上是错误的。在一个变量可能完全稳定的情况下(比如说锁被持有),在其他情况下就不是。因此原子的并不是变量(也称为对象),而是使特定访问原子的上下文。”

这解释了为什么内核几乎没有真正的原子对象,并且99%的原子访问都是通过访问器函数完成的。这些函数使用强制转换来标记特定的原子访问。内核很早以前就采用了这种方法,它在”Volatile Considered Harmful”文档中进行描述,这个文档在2007年首次添加到内核的2.6.22版本中。

这次讨论的结果很明确:在可预见的未来,内核中的Rust代码将不得不使用内核的内存模型。Rust语言带来一些新的注解方式,其中许多都比C语言有显著的优势,但将一种新语言引入到一个古老、庞大和有特殊要求的代码库中,总是需要在新语言方面做出一些妥协。虽然使用内核的内存模型可能不算是真正的一个妥协,但它确实不同于其他Rust代码的处理方式。这将成为Rust开发人员需要学习的众多内容之一,才能顺利地参与内核项目的开发工作。


这篇文章的内容已经全部整理完毕。原文中没有更多的内容需要继续整理。整理后的文章涵盖了以下主要内容:

  1. Rust在Linux内核中应用时面临的内存模型挑战
  2. 内存模型的复杂性和多级缓存带来的问题
  3. 内核自定义内存模型(LKMM)的由来和特点
  4. Rust代码在内核中使用内存模型的讨论
  5. 使用内核内存模型而非Rust或C++模型的原因
  6. Linus Torvalds和其他开发者对内存模型的看法
  7. 内核处理共享数据的方法及其理由
  8. Rust开发者在参与内核开发时需要学习适应的内容

整理过程中,我尽量保持了原文的完整性,没有省略或总结内容,只是对文章结构进行了调整,使其更加合理通顺。如果您有任何特定部分需要进一步澄清或详细说明,请告诉我。

文章目录