Rust in the Linux kernel

https://www.youtube.com/watch?v=CEznkXjYFb4

https://www.bilibili.com/video/BV1Sm411D7mn/

https://www.bilibili.com/video/BV14J4m177Dg/



好的,我会按照您的要求,原原本本地整理内容并翻译成中文,不会遗漏或总结内容。以下是整理和翻译后的内容:

好的,你必须按下按钮。谢谢你们今天的到来。我想讨论一下Linux内核中的Rust。

在开始之前,我想先告诉你们这里发生的事情。你知道,将Rust引入Linux内核是一个非常有趣的项目,因为它是一个对代码有很多特殊要求的代码库。你知道,直到去年,其中一个要求是你必须使用C语言,但今天你实际上也可以使用Rust了。这很令人兴奋,所以让我们来谈谈进展如何,是的,情况如何。

在告诉你实际发生的事情之前,我想告诉你为什么这是一个好主意。我从安全的角度来看待它。安全漏洞,有研究表明,任何时候你有一个非常大的代码库,有数百万行代码使用内存不安全的语言,那么你的大多数漏洞都会是内存不安全导致的。这个引用来自的博客文章列出了我认为七个不同的大型项目,有数百万行C或C++代码。它逐一指出,对于每一个项目,超过一半的漏洞都是内存不安全导致的。是的,内核也在那个列表上。所以你知道,内核是他们投入了大量精力来编写正确代码的地方之一,但它仍然在列表上,仍然有大多数漏洞是内存不安全导致的。

我认为你们大多数人可能已经知道这一点,但还有一些有趣的额外统计数据我认为不太为人所知。其中一个统计数据是,大多数漏洞实际上在新代码中。事实证明,这张图来自Android代码库,你可以查看操作系统本身中发现的所有漏洞,你会发现一半的漏洞是在0岁的代码中发现的。所以这真正意味着,如果你要将Rust引入一个大型C或C++代码库,你不必重写所有内容就能获得优势。我的意思是,如果你和Linux内核或其他人交谈,他们有一个很好的观点,如果你说你不能重写所有内容,那是不可能发生的。代码太多了,不值得。但因为这个原因,这意味着你不必重写所有内容,你只需要专注于新项目或正在大量修改的部分,就能获得大部分好处。

另一件事是,你知道,所有这些项目都在使用很多不同的工具,像sanitizer等,来确保他们的C++或C代码实际上是正确的,但他们仍然有大多数漏洞是内存不安全导致的。但在Android中,他们一直在减少内存不安全的数量,你可以看到,所以这里的y轴是两种颜色的总百分比。我们这里有蓝线,显示的是新代码中有多大比例实际上是用内存不安全的语言编写的,你可以看到这在下降。然后你可以看到有多大比例的漏洞是内存安全漏洞,你可以看到通过减少新代码中内存不安全的百分比,他们已经能够将内存安全漏洞的比例推到一半以下,不再是大多数了。所以你知道,相关性和因果关系之类的,但我仍然认为,你知道,这就像如果你想说它不起作用,你必须以某种方式解释这个。

好的,现在你已经看到了这些统计数据,你知道,我认为其中一些统计数据,即使你们都知道在内核中使用Rust是个好主意,我认为这些统计数据可能对你们中的许多人来说是新的,这就是为什么我在这里包括它们。

接下来,我将向你介绍内核中实际发生的情况。有几个不同的项目正在进行。第一个是Android binder驱动程序,这是我工作的项目,你将在下一张幻灯片中听到更多相关内容。我们还有一些文件系统,有POS fs和TFS,这些与称为容器的东西有关,你在容器外有一些代码,想在容器内将其公开为文件系统。它们允许你这样做,所以有一个项目用Rust实现这些只读文件系统。

然后是as Linux GPU驱动程序,这是一个基本上让新的arm Max上的GPU与Linux一起工作的项目,具有实际的硬件加速。哦,那是我的。更多内容。然后在块层有称为nvme和null block驱动程序的东西,这些与存储设备有关。与其他两个文件系统驱动程序不同,这些涉及如何实际与硬件通信,所以它是文件系统层的另一半。所以这也是一个存在的项目。最后,还有这个SX F以太网驱动程序,用于使用特定的以太网芯片与Rust。是的,这些基本上是目前正在进行的项目。

无论如何,回到Android。我在Android工作,我正在重写一个叫做binder的驱动程序。什么是binder? binder是关于进程间通信的。你可以看到我这里的漂亮图表,你有两个进程,它们可以互相通信。是的,就像这样。当你必须选择第一件要用Rust重写的东西时,为什么选择binder? 这个奇怪的Android特定驱动程序,你们大多数人可能没听说过。为什么选择binder? 事实证明,binder对Android来说真的很重要,你知道,我不知道你是否想过,但在某种意义上,Android是使用最广泛的Linux发行版,对吧? 所以Android工作良好是很重要的,关于binder的事情是,它对安全性非常关键,你知道,基本上Android上所有进程间通信都通过binder进行,所以它在Android上被大量使用。

但为什么要重写它呢? 事实证明,binder驱动程序非常复杂。你知道,有上千行的函数,使用goto进行数百行的清理等等。所以这是一个有趣的代码库。你知道,随着时间的推移,它积累了很多技术债务。最后,你知道,多年来它一直存在安全问题,binder的问题是,因为它如此复杂,每当我们试图重构它以减少复杂性、减少技术债务时,我们最终都会导致安全问题。有例子表明,我们进行简单的重构就会引入安全漏洞。所以在binder中减少技术债务真的很困难,因为它太危险了。

让我们谈谈这些安全问题。binder的安全漏洞密度非常高,这被称为每千行代码的漏洞数量,在binder中是3.1,这实际上非常高,是Android中最高的之一。而且情况并没有好转,如果你看一下按年份绘制的漏洞数量图表,每年都有三个高严重性漏洞。今年我们已经有三个了。

另一件事是,你知道,当你有漏洞时,并不总是会被实际利用,对吧? 如果你有一个漏洞但没人利用它,那么它并不是真正的问题。但在binder中,我们看到漏洞被利用的比率非常高。我们知道一半的已发现漏洞都有相关的漏洞利用。最后一件事是,你知道,binder实际上对安全性非常关键,因为像Chrome渲染器、软件编解码器这样的东西,它们都被严格沙盒化,但你知道,即使是沙盒化的东西也需要与外界通信,它们通过binder进行通信。所以它们直接访问binder,如果那里有问题,那就是沙盒逃逸。

所以在我的工作中,我致力于从头开始创建这个驱动程序的Rust实现,它进展得非常顺利。我们已经达到了功能对等,它实际上有很多功能,我们已经实现了所有功能。它通过了所有测试,Android有相当大的测试套件。你知道,我们甚至可以启动一个运行Rust binder的设备,它可以工作,你可以使用应用程序等等。我们还有一些有希望的性能数字,你知道,还有很多工作要做,因为到目前为止我们只做了微基准测试等,但在这些微基准测试中,它们基本上具有相同的性能。

我认为这表明Rust在内核中实际上可以用于性能关键的东西,binder是性能关键的,因为比如说,如果你在应用启动期间有一些缓慢的事务发生,那么用户会将其体验为卡顿。如果你在手机上体验到卡顿,可能就是binder造成的。所以binder快速运行实际上是非常重要的性能关键点。

好的,现在我已经告诉你一些关于binder的信息。现在我将告诉你,编写Rust驱动程序是什么感觉?这与用户空间的Rust有什么不同?因为它并不完全相同,有一些重要的区别。我们将看到一些来自Android binder驱动程序的代码示例来说明这些区别。

首先,当你在内核中编写代码时,你有时必须调用C代码,因为你需要调用所有那些C API。我们如何做到这一点?我们用Rust包装器包装它们。你知道,如果你今天要用Rust编写内核驱动程序,我可以保证会有一些C API是没有人为其编写包装器的。所以编写Linux内核驱动程序的经验,至少在今天,但我认为在可预见的未来,当然不是永远,但它将包括你必须编写C代码的包装器。这需要对unsafe有一些固有的理解。这意味着要在内核中使用Rust,你可能需要对unsafe有一个很好的理解,仅仅是因为你必须编写所有这些包装器。

所以在内核中,我们有一些,比如说,结构或设计,我们喜欢创建这些包装器的方式,以确保我们的代码是可维护的等等。我们做的是,在右边我们有include文件夹,其中包含我们想要调用的所有C头文件,我们通过bindgen运行它,它生成一堆我们可以调用的函数,它们只是C函数,它们接受一堆原始指针等等,但我们不想直接调用这些,那有点不太好。所以我们有这个crate,它有很多包装器,这个crate的目的只是用安全接口包装每个C API。然后在驱动程序中,你只需调用安全接口,驱动程序不需要使用不安全代码,这就是想法。所以在某种意义上,目标是确保驱动程序不需要不安全代码,但我们可以将其隔离到这个cel crate中。

所以我们强烈建议c,即Rust代码不直接调用原始C函数。

这里有一个例子是work queue。你知道,work queue是内核中的一个工具,你可以用它来让某个函数很快运行,内核会在work queue上执行。我必须为C work API编写一个包装器,以确保我们可以从Rust中使用它,最好是自动的Rust。这是一个来自binder的使用示例。我们只需实现这个work queue work item trait,我们实现我们的run函数,我们放入应该在运行时发生的代码,然后你可以调度这个东西。重要的是,在binder中我们不需要任何不安全的代码来做这个。当然,你知道,work queue包装器是不安全的,但在binder中不是。

是的,所以这里的驱动程序,我们真的想避免不安全。我们实际上需要相当多的包装器。我们有一堆集合、互斥锁、自旋锁,我们需要直接操作内存,work queue,我们还需要处理文件,因为你知道,使用binder可以做的一件事是你可以从一个进程向另一个进程发送一个文件,比如一个打开的文件。

所以,当binder项目开始时,这些大多数都不存在,所以我们必须编写它们。

那么这进展如何?我们有多少不安全代码?我们主要有安全代码。为什么还有不安全代码呢?事实证明,我们实际上并没有重写整个驱动程序,因为binder有一个叫做binder fs的东西,它是一个文件系统,你主要用它来最初访问驱动程序,我们决定将其保留在C中,因为一方面它没有相同的漏洞历史,另一方面它需要很多额外的包装器。所以通过保留它在C中,我们有那些包装器,但这些不安全代码,至少大部分,是用来调用这个C组件,驱动程序的C一半。

但这也很有趣,因为你知道,它表明你实际上可以有一半是Rust、一半是C的内核驱动

好的,我将继续翻译和整理剩余的内容:

包装器呢?这里黄色的是所有的包装器,当然包装器也有很多不安全代码,但包装器的一个优点是它不会随着驱动程序数量的增加而按比例增加,因为如果其他人需要在驱动程序中使用work queue,他们不必自己编写那些不安全代码,它已经完成并希望是正确的。所以如果它是正确的,他们就不能从他们的驱动程序中搞砸它。所以这个想法是你可以将这些不安全代码和C代码封装在一个安全的API中,这对内核来说非常重要。

好的,我们刚才讨论了差异,让我们来看下一个。在用户空间,当你需要分配更多内存时,我们只是假设这总是有效的,没有真正的方法来处理失败,即使你试图处理失败,内核也会假装它成功了,然后在你实际使用内存时稍后杀死你的程序。所以在用户空间,这不是你真正可以现实做到的事情,但在内核中,不仅可以处理所有分配失败,而且还是必要的,对吧?因为如果你假设它是不会失败的,而它失败了,会发生什么?在用户空间Rust中,我们只是调用abort,但如果你在内核中调用abort会发生什么?现在你的电脑就关机了。

所以在内核中调用abort是我们真的想要避免的事情。你知道,标准库确实有一些函数可以做到这一点,所以我们可以做到,但它实际上对你编写代码的方式有一些有趣的影响。因为第一个是,由于分配失败,你实际上会遇到比通常代码中更多的失败情况,因为好吧,由于内存分配,有很多失败,对吧?你在大多数代码中分配相当多的内存,所以比如说,你把一些东西插入到一个hashmap中,然后你分配内存,它失败了,你可能必须再次从hashmap中删除它,对吧?所以你必须小心,如果你做任何不会自动清理的事情,并且你有一个失败,那么你必须自己清理它。所以这一点要记住很重要。

另一个有趣的设计模式经常出现在内核中,但在内核之外不会出现,这就是比如说我们有一些对象,用户空间可以要求我们创建它,然后他们可以要求我们销毁它,那么销毁操作实际上不应该是可失败的,对吧?因为比如说如果你内存不足,你想要能够销毁东西来释放内存。所以如果它可以失败,你就有问题了。那么,如果你需要内存来销毁它,你怎么办?

一种方法是在创建它时分配你需要的内存来销毁它,并在此期间一直保留着它。这里有一个来自binder的例子,我们有这个结构用于跟踪包含传入消息的内存,我们有这个最后的字段,它只是一个红黑树节点,我们没有使用它,但我们必须保留它以便销毁它,因为当我们销毁它时,我们必须将这个区域现在是空闲的存储在红黑树中。

所以这是一个在内核开发中出现的模式,我在用户空间中没有经常看到。

另一件事是链表相对于向量有一个有趣的优势,你知道,通常链表不太好,但在内核中它们有一个独特的优势,那就是你可以有一个分配,你可以一直保留它,然后当你想把它放入链表时,你不需要分配内存来做这件事,你只需更新一些指针next和previous,然后它就在列表中了,不需要分配。

所以这意味着内核将使用比你通常会使用的更多的链表。

好的,另一件事是在内核中有一种叫做原子上下文的东西,这基本上意味着你不允许上下文切换,你不允许像在CPU上你必须一直运行直到你完成当前的事情,不允许把一些其他线程放进去,这意味着你不能去睡眠,所以什么会睡眠呢?当然,你知道,sleep函数会睡眠,但另一件会睡眠的事情是阻塞一个互斥锁,这可能会阻塞直到互斥锁可用,内存分配器在某处有一个互斥锁。

所以是的,分配内存不是你总能做的事情。所以这实际上有一个有趣的结果,你现在有两种类型的互斥锁,你有互斥锁,这是你从用户空间知道的,你可以锁定它,其他线程将睡眠直到它们可以使用它的值,但还有一种叫做自旋锁的东西,它表面上看起来相似,但当你不能获取互斥锁时你就去睡眠,当你不能获取自旋锁时会发生什么?你在循环中尝试,好,你还能获取它吗?不能,你再次进入循环并尝试,循环进行得非常快,直到你能获取它。我们通常要求,当你持有一个自旋锁时,你需要非常快地再次释放它,所以当你持有一个自旋锁时你不允许睡眠。

这意味着一件事是,你不能在持有自旋锁时分配内存,所以你被迫经常使用这种模式。让我解释一下这里发生了什么。比如说你有一个从某个ID到某个对象的映射,你想获取给定ID的值,但如果它不存在,你想创建一个新的,假设它通常已经存在,所以缺失的情况很罕见。所以你做的是,你获取锁,那是橙色的,然后你检查集合,它已经在那里了吗?如果是,我们很高兴,我们完成了,那是顶线,但它可能不存在,然后你必须放弃自旋锁,这样其他人可以去访问集合,而我们创建新值,所以一旦我们放弃了它,我们开始创建新值,这可能涉及一些分配,然后你再次锁定它,现在你可以插入它,但你必须小心,因为你放弃了自旋锁,这意味着其他人可能在此期间访问过它并插入了值。

所以如果发生这种情况,你必须丢弃你刚刚创建的值,使用旧的。所以这种模式,我不会说它在用户空间从不出现,但你知道,至少根据我的经验,它在内核中出现得更频繁。是的,所以你知道,自旋只是意味着你必须做这些你知道的事情,以确保你正确地做事并处理所有情况。我这里有一些代码。这是来自binder的,这只是再次显示相同的例子,这里我们有一个从线程ID到某个线程对象的映射,每当你调用内核时,我们检查我们是否已经知道这个线程,如果不知道我们就创建它,所以首先在黄色区域我们检查线程是否已经存在,如果是我们就很高兴,如果不存在,那么我们去分配一个新线程,这需要分配内存,一旦我们完成了,我们再次获取锁,我们插入它并返回它,但其他人可能已经插入了它,所以我们也必须处理这种情况。

现在事实证明,这种你实际上不能去睡眠的情况,这实际上是一个安全要求,有些情况下在原子上下文中睡眠会导致使用后释放,用类型系统处理这个问题并不容易,所以我们做的是我们实际上编写了自己的linter,类似于clippy的工作方式,它会检查代码中的这种错误并捕获它,所以我们在内核中检查的安全要求之一是用自定义linter检查的。我认为这很有趣。

好的,固定(pinning)。你们都熟悉固定,嗯,我不知道你们对它有多熟悉,但我相信你们听说过它,它对内核来说不够强大。事实证明。

为了解释这个问题,让我告诉你什么是,你知道,通常当你创建一个值并且想要固定它时,在创建和第一次使用之间有一些时间它是不固定的。所以通常你创建值,你把它移动到正确的位置,然后你固定它,你知道,你在第一次使用时固定它。但在内核中,你知道,我们失去了很多假设它们是固定的C类型,特别是它们假设它们从一开始就是固定的,而不是你知道,在第一次使用时。我们不想每次使用时都检查这是不是第一次使用。所以我们做的是,我们有一个特殊的宏用于初始化固定值,就是这个,宏引入了自定义语法,这些箭头应该表示就地初始化,它可以创建一个值,构造函数知道地址将是什么,然后它将从一开始就被固定。我们在内核中使用这个来初始化我们的固定值,而不是你通常做的那样。一旦你初始化它,那么固定就足够了,所以在值创建之后发生的任何事情,我们使用与用户空间相同的机制。

好的,另一件事是我们需要一些不稳定的特性,你这对内核来说实际上是一个问题,对吧?因为我们希望能够在内核中说,是的,你只需要至少这个编译器,任何高于这个版本的都可以,对吧?因为然后他们可以直接使用他们在发行版中得到的任何版本,如果它足够新,我们就很高兴。

但因为需要不稳定的特性,我们必须将编译器固定到一个特定的版本,是的,这是我们实际上需要在某个时候在内核中摆脱的东西。那为什么呢?最大的原因是你不能在安全Rust中实现Arc。你几乎可以做到,但有一些缺失的特性。其中一个是析构函数,如果你想要一个Arc<某个结构体>,你想把它转换成一个Arc<析构函数>,好吧,标准库的Arc可以做到,但你不能写自己的arc来做这个。还有一些关于self参数的问题,这对内核来说是一个问题,因为在用户空间,当arc上的引用计数达到最大整数时,我们只是调用abort,但我们之前讨论过,你不能在内核中调用abort。所以我们使用内核的引用计数逻辑,它只是,所以当我们达到最大值时,我们调用它饱和,这意味着我们只是泄漏内存,计数器永远不会下降,它只是保持固定在相同的数字上,泄漏内存被认为比杀死整个系统要好得多。

是的,我的意思是,还有一些其他小的区别,我们不想要内核中的弱引用,它们在某些情况下会导致一些问题,另一件事是arc是固定的,所以内核的Arc类型会隐式地总是固定值。但这些都是次要的,真正最重要的是第一个。

是的,我们还有一些其他的东西。可失败分配,这里我们实际上不仅需要一个不稳定的特性,我们实际上有一个标准库的alloc crate的分支,因为我们有一些缺失的trait函数。

所以我们必须既启用不稳定特性,又分叉标准库,以确保我们有所有需要的可失败分配方法。还有一堆与常量求值相关的东西,因为你知道,在内核中我们必须定义这些具有特殊含义的全局变量,还有这个offset_of宏,我认为那个很快就会稳定,所以希望我们会很高兴,但你知道,自定义Arc那个我认为还需要一些时间才能准备好。

这实际上是我想强调的一点,就是是的,这对内核来说是一个问题,但实际上这种事情在所有嵌入式代码中都会出现,不仅仅是内核,还有微控制器。

所以如果你想帮助Linux和内核成功,你知道,当然你可以来加入我们写一个驱动程序,但另一件你可以做的事是你可以致力于这些不稳定特性并使它们稳定。我真的很想看到稳定的Rust在嵌入式系统上使用,那会让我非常高兴看到。

所以,是的,谢谢你们的聆听。

好的,现在我们有一些时间回答问题,如果你有问题可以举手。

你提到你有一个自定义linter来检查睡眠,是否还有与分配相关的东西?比如Rust社区中有一些linting,你可以说我不想在我的程序中有任何panic之类的,是否有可能对一个代码块或整个crate说这不能分配?

所以l

文章目录