LWN:Rust 另一些对 kernel 有用的特性!

共 5690字,需浏览 12分钟

 ·

2021-11-29 20:00

关注了就能看到更多这么棒的文章哦~

More Rust concepts for the kernel

By Jonathan Corbet
September 20, 2021
Kangrejos
DeepL assisted translation
https://lwn.net/Articles/869428/

Kangrejos(Rust for Linux)会议的第一天介绍了这个项目以及它所要实现的目标;第二天介绍了一些 Rust 的核心概念以及跟内核开发有什么关系。第三天,也是最后一天的时候,Wedson Almeida Filho 深入探讨了如何让 Rust 在 Linux 内核中用起来,他介绍了目前为止已经学到的一些经验,并与一些内核开发人员讨论了下一步的工作。

Almeida 首先指出,他不是 Rust 语言的开发者,也不觉得这种语言是完美的。但他确实相信,Rust 可以解决内核中的一些问题。他是安卓平台安全团队的工程师,一直在寻找改善该平台的方法,特别是减少攻击面(reduce attack surface)。他认为 Rust 可以做到这一点,并且它还有助于提高正确性,提供一个超出 C 语言能提供的 expressive type system(类型系统)。

Ownership

他继续介绍了 Rust 中的一个重要概念,那就是数据所有权(data ownership)。Rust 程序中的每个数值都仅有一个所有者。这个所有权会随着程序的执行而转交给别人,但绝不会共享。当一个对象的所有者不再存在时,该对象就会被释放掉(free)。所有权是具有独占性的,但是当然还是要有办法可以将数据进行搬移和转交的。在 Rust 中,这是用引用(reference)来实现的。mutable reference (可变引用)允许其持有者来修改数据,注意 mutable reference 是独占的,不可以存在对该对象的其他引用。而 shared reference (共享引用)则是非排他性的(non-exclusive),并且是只读的。

但是上述规则有几种例外情况。Almeida 简要提到了内部可变性(interior mutability)的想法,但并没有深入探讨细节。Rust 也支持原始指针(raw pointer),但是只有在 unsafe 代码中才支持。

Rust 的所有权规则带来的一个结果是,在用 safe Rust 编写的代码中不会出现 data race。因为要产生 data race 的话,就必须要有至少两个 CPU 在以未同步好的方式(unsynchronized manner)访问共享数据,并且至少有一个在进行写入。由于 mutable reference 是独占式的,使得这种情况根本不可能出现。

这些规则也使得编译器可以对那些编译器无法直接看到的代码进行优化。例如下面这样的代码:

*x = 32
some_function()
return *x

编译器可以完全放心地只返回 32。变量 x 不会有别名,所以 some_function()不可能会对它的值进行改动。

那么,这个所有权规则在内核代码中该如何使用呢?Almeida 提出了一个例子,就是在许多内核结构中都有的 private_data 字段。这个字段一般是由子系统来将自己的特有数据填入到由系统中更高一层代码所管理的结构中。一般情况此字段是一个 void * 指针,当其被真正使用起来时会被转换为相应的类型。根据 Rust 标准,这不是安全的用法(not safe usage)。下面以 struct file (在内核中用其来表示一个打开的文件)为例来解释。

在 Rust 代码中,开发者会写一个 open() 函数,该函数会创建某种内部状态,通常会通过 private_data 来存放这些数据。这些内部状态信息将被返回给调用者,并且这个状态对象的所有权也被返回回去。如果用户空间在打开的文件上调用 ioctl(),那么 ioctl 处理程序将得到一个对此状态对象的共享引用(shared reference)。这个引用必须是共享的,因为这些调用可能是并发出现的。相反,当文件被关闭时,release() 函数就可以获得这个状态数据的所有权,然后就可以释放掉。所有这些都可以使用 Rust 的 type 规则来提供类型安全以及并发安全(type and concurrency safety)。

Device IDs, locks, and more

另一个例子是整个驱动程序子系统中的 device-ID table。这些表中每个数组元素都包含了一个 ID 以及一个无类型的可选参数,并且必须是用 null-terminated。Almeida 用 Rust 编写了一个 PL061(GPIO)驱动,来实现了内核中的相应 C 驱动的功能,并创建了一个新的 device-ID table 抽象来配合使用。开发者必须要确定这里可选参数的类型,并且提供来的所有数据都必须是这个类型的。也没有必要再将 list 用 null-terminate 了,这就消除了驱动程序作者经常犯的一个错误。因此,整个机制就是类型安全的(type-safe),并且也更容易使用。

锁(locking)是内核中很多复杂问题和混乱的来源之一。在 C 语言代码中,锁通常被声明为某个结构中的一个字段,而且往往不清楚某个特定的锁到底是在保护什么数据。而在 Rust 中,数据是直接跟保护它的锁关联起来的,这样一来,如果不先获得锁就没法编写访问该数据的代码。至少得满足这个条件之后编译器才会帮开发者完成编译。所有的检查工作都可以在编译时完成。

Almeida 简要地提到了代号 CVE-2021-26708 的漏洞,这是在 mainline 内核中一个可以被恶意攻击利用的 race condition,就是在获得保护数据的锁之前访问数据而造成的问题。他认为 Rust 就可以防止这个漏洞的发生。锁在定义的时候就会确保在没有获得该锁的情况下不能触及相关数据。但是,如果开发者没有意识到首先需要一个锁,会发生什么?在这种情况下,Rust 的所有权规则会起到效果,因为试图修改未受保护的数据会因为使用了错误的引用类型而失败。

Laurent Pinchart 跳出来说,他喜欢这种把数据和保护它的锁捆绑在一起的想法。但他比较担心开发者明知道不需要锁的那些情况,比如初始化部分的代码就属于这样的情况。看起来如果编译器知道对有关对象只能有一个引用,那么锁就没有必要了,例如,当对象刚刚被创建时就会是这种情况。Almeida 说,如果所有其他办法都没法用的话,开发者总是可以使用 "unsafe escape hatch" (意思是写 unsafe 代码来绕过编译器的限制)。

另一个需要注意的抽象概念是文件描述符(file descriptors),这些描述符在创建时首先需要获得对底层相应的文件结构的引用,然后才能分配描述符编号。如果分配失败的话,代码必须要记着放弃(drop)对文件的引用。Rust 的生命周期管理功能可以让错误处理自动就完成,减少了很多八股文一样的代码。内核代码中经常出现的那种 "goto out; " 的写法在 Rust 中是没有必要的。

Almeida 指出 CVE-2019-15971 就是由于未能增加文件的引用计数而产生的 use-after-free 漏洞。在 Rust 中如果犯了这种错误的话,一定会收到编译器的友好提示信息的。

继续谈论到错误路径(error path),Almeida 重复强调说,他认为在内核代码中看到的大多数复杂和容易出错的错误处理,在使用 Rust 时都可以消失了。在大多数情况下,对象会在超出 scope (生效范围)的时候就会直接被清理掉,根本不需要显式去进行处理。对于开发者需要更多的控制权的那些情况,可以使用 scopeguard 对象。这个对象在初始化时会使用相应的 error-handling 信息,如果一切顺利的话,不需要调用这些 error-handling 代码,那么它的 dismiss() 方法会被调用到。否则,如果该对象超出了生效范围之后的时候会直接执行相应的 error handling 错误处理。

此外会议上还讨论了其他一些内核抽象概念,包括 task 结构、红黑树,以及对 memory-mapped I/O 区域的访问。不过,Almeida 想讲的观点在此时应该已经很清楚了。Rust 语言能够处理内核层面的编程工作,这比用 C 语言完成同样的任务要安全得多。

Discussion

Julia Lawall 首先询问了在 Rust 中有哪些缺点?有什么突出的缺点?Almeida 回答说,在 Rust 中,所有的对象在内存中都是可移动的(movable),这对那些会自己引用自己(self-referential)的数据结构来说可能是个问题。解决办法是钉住(pinning),但这就需要编写 unsafe 的代码了。他说,现有的 Rust 驱动中的很多 unsafe 代码都源于这个问题。另一个问题是 Rust 要求所有数据都要被初始化,这对 mutex 来说尤其是一个问题。要解决这个问题的话主要是要找到并使用正确的抽象概念。

Pinchart 对 Rust 开发者所采取的渐进式方法提出质疑,这个问题在讨论中也多次出现。他说,正确的做法是把内核子系统的维护者关在一个房间里,要求他们学习 Rust,并在过渡期间为这些维护者提供大量的帮助。如果维护者被一个与 Rust 有关的问题所阻碍了,那么他们应该能够立即得到帮助来解决这个问题。否则的话,Rust 开发者将遇到大量的反抗。

Ojeda 质疑是不是真的会碰到反抗。他说,没有人要剥夺使用 C 语言来编写驱动程序的权利。Pinchart 回答说,在未来某个时刻,维护者可能不接受这些驱动程序。Ojeda 说,这可能是五年或十年后的事情了,不会那么快。他认为有必要期望内核开发者们来学习一些 Rust 知识。safe 模式下的工作并不困难。他承认 unsafe 的 Rust 模式确实比较难,毕竟又会出现那些对未定义行为的担忧了,而且文档也没有那么好。

Pinchart 又提出了另一个有很多人提到的担心,那就是内核开发者必须要能够使用整个源码 tree 来进行工作,很少有开发者是从来不去查看自己子系统之外的代码的。这样一来,我们很难在早期就将 Rust 在内核中的影响降到最低,毕竟会有无数开发者可能会读到 Rust 部分的代码。Ojeda 认为现有计划是只在已经了解了 Rust 的维护者相应的子系统中引入 Rust,但 Pinchart 回答说,不应该指望其他开发者就不会受到这些 Rust 代码的影响了。开发人员几乎肯定也是要用 unsafe Rust 来工作的。Mark Brown 补充说,关于 Rust 的引入,可能会有某个标志性的一天,在那一天之后所有维护者们将不得不在某种程度上了解这种语言。

Almeida 问道,在这种过渡开始之前,需要有多少比例的内核开发人员要懂得 Rust?Jonathan Cameron 回答说:"a lot"。我从引进 ReStructured Text 语言文档支持的过程中得到了一些经验,那就是这个过程要比预期的要长,这个过程中遇到了一些激烈的(和持续的)阻力,并且现在仍然没有全部完成。如果没有被分开发社区中相当多的人所接受的话,它就不可能会达到今天的这种成功。Rust 的引入要比这个复杂得多,因此它被接受的时间只会更长。因此是必须要努力让大家广泛能够接受的。Pinchart 说,现在大多数开发者似乎都对 Rust 的价值感兴趣,但其中许多人在担心将其引入内核带来的成本是不是过高。

Ojeda 说,Linux 的 Rust 开发者需要能说服足够多的开发者来相信 Rust 的价值,这样 Linus Torvalds 就会给这个改动给予祝福,否则就可以干脆结束这些讨论了。他期待着即将举行的 Linux Plumbers Conference 和 Kernel Summit 的讨论,能够帮助把更多的开发者带入这个过程。我对他过于依赖 Torvalds 祝福的想法表示了一些担忧,因为这是一个必要条件,但远远不是充分条件。

Greg Kroah-Hartman 说,他喜欢将 Rust 引入内核开发的想法,但也认为这项工作还有很长的路要走。设备驱动(也就是 Rust 开发者初期的目标领域)必须要与内核的许多部分互动,包括 driver model、sysfs 和其他各种子系统,而能够实现这种交互的 Rust 功能还没有出现。围绕着 Rust 的引入,会有社会、政治和技术问题,而目前甚至连技术问题本身都还没有处理好。尽管如此,他还是赞扬了 Rust 开发者到目前为止所取得的进展。

会议时间快要不够了,Kroah-Hartman 说,至少在五年内不可能要求开发者用 Rust 编写。Pinchart 问道,是否有人想过,社区愿意在这次转型中失去多少开发者,实际肯定会有一些开发者因此离去的。Ojeda 最后说,让 Rust 进入内核的过程是对这一点来说最重要的一个因素,他的下一步将是在 Linux Plumbers 大会上进行讨论。

全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。

欢迎分享、转载及基于现有协议再创作~

长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~



浏览 49
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报