TL;DR:我用 Rust 编写一个 Linux 克隆只是为了好玩。它的目的并不是取代 Linux 内核。最近几个月,我一直在开发一个新的操作系统内核Kerla ,它是用 Rust从头开始编写的,旨在在 ABI 级别与 Linux 兼容。换句话说,支持运行未修改的 Linux 二进制文件!我已经实现了基本功能:fork(2)
and execve(2)
、文件操作、initramfs、TCP/UDP 套接字、信号、tty / pty、管道、轮询等。您可以通过 ssh 进入在临时 Firecracker microVM 上运行的 Kerla,该虚拟机会自动为您启动(它接受任意密码):
$ ssh root@demo.kerla.dev
在这篇文章中,我将分享我在操作系统内核中使用 Rust 的第一印象。
pub fn hello() -> bool {
println!("Hello, world!");
true
}
Rust 在用户领域的成功
Rust 是一种深受喜爱的编程语言,它使您能够高效地开发可靠且高性能的软件。Rust 的所有权概念和类型系统让您专注于修复逻辑错误,而不是痛苦的内存错误和竞争。enum
迫使您处理所有可能的输入/输出模式。此外,它的构建系统 ( cargo
) 和 IDE 支持 ( rust-analyzer
) 非常棒。Rust 逐渐在生产中得到采用:AWS 的轻量级 VMM、npm 注册表的一部分 [PDF]以及许多其他公司。Rust 无疑被认为是 C 和 C++ 的绝佳替代品(而不是替代品!)。
内核领域的 Rust
近年来,用 Rust 重写现有软件一直受到关注。Bryan Cantrill 在“是时候用 Rust 重写操作系统了吗? ”中讨论了有关操作系统的主题,在演示中,提出了一种混合方法:因此,一种混合方法是保留现有的基于 C/汇编的内核,这是我们多年来一直采用的方式。然后你可以开发基于 Rust 的东西、基于 Rust 的驱动程序、基于 Rust 的文件系统、基于 Rust 的内核软件。这就是Rust for Linux的人们正在做的事情。您最近可能听说过LKML 上建议的补丁。我完全同意重写现有的大型、功能丰富且强大的操作系统内核并不是一个好主意。然而,我想到了一个问题:用 Rust从头开始编写一个实用的类 UNIX 操作系统内核有什么优点和缺点?它看起来怎样?这就是我开始这个项目的原因。为了探索 Rust 在内核领域的优点和缺点,我决定开发一些实用的东西,特别是一个与 Linux 兼容的内核,可以用作虚拟机上的函数即服务运行时环境。为此,您只需要很少的设备驱动程序(例如virtio),并且我们不必支持 eBPF 等高级 Linux 功能。此外,与其他长时间运行的工作负载相比,内核崩溃不会成为严重问题。这听起来并非不可能,不是吗?
Rust 对内核有好处吗?
在我看来,是的,除非目标环境资源不太受限。内核空间有点古怪:panic!
会导致系统崩溃,内存分配失败应该在不恐慌的情况下处理,隐藏的控制流和隐藏的分配通常不受欢迎。Rust 的优势也适用于内核领域。我最喜欢的例子是可为空指针处理:如果指针可为空(即Option<NonNull<T>>
),则在显式处理 null ( ) 情况之前,您无法取消引用它None
!此外,使用新类型 idiom,您可以区分内核指针和用户指针。然而,在内核领域使用 Rust 仍然存在一些问题:
分配失败的处理方式是panic!
Linus Torvalds 在 Linux 内核的 Rust 支持补丁中也提到了这个问题。让我们看一下 的定义Arc::new()
,它是一个创建线程安全引用计数指针的构造函数:
pub fn new(data: T) -> Arc<T>
构造一个新的Arc<T>
.看起来超级直观,对吧?然而,它有一个隐含的恐慌情况:内部缓冲区分配失败。处理分配失败很无聊。当我启动 C 项目时,第一步是编写自己的项目xmalloc(3)
,这样我就不需要检查结果是否为 NULL。如果内存不足,我会让它崩溃。没什么大不了的。我所需要的只是生成一个具有更多内存的新虚拟机或在 Amazon 上购买新内存。然而,panic!
内核空间中的 -ing 确实会导致内核恐慌。这是一个大问题。我们应该设法从内存不足的情况中恢复以保持系统正常运行。没有人愿意看到蓝屏。在我的项目中,为了利用现有的方便的板条箱,我决定采用当前的 Rust 方式:允许因分配失败而出现恐慌。也就是说,在不久的将来,我确实认为我需要使用(或编写我们自己的)动态分配容器的可失败版本。
更大的二进制大小
Resea是一个用 C 语言编写的基于微内核的操作系统(由我编写),仅需要 845KiB(发布版本,剥离),包括 TCP/IP 服务器、基于 Intel VT-x 的虚拟机管理程序和 Linux ABI 仿真支持等用户态应用程序。相比之下,Kerla 映像大约需要 1.1 MiB(发布版本、opt-level = 'z'
精简版),不包括 initramfs。尽管极简微内核和整体内核具有相反的理念,但在我看来,Rust 实现往往比 C 实现更大。虽然可以使用一些尺寸优化方法,但在极其受限的设备(例如1 类设备)中这可能是一个问题。
make menuconfig
不见了
操作系统内核有很多参数。make menuconfig
在 Linux 内核中,可以使用和 等配置工具来配置它们make xconfig
。在 Rust 中,我们有功能标志来启用/禁用 crate 中的功能。但是,如果您想更改硬编码参数(例如心跳间隔)怎么办?你是通过环境变量和env!
宏来配置的吗?不,它只接受一个字符串。我们可能需要一个功能丰富的构建配置机制,就像Cargo 中的Kconfig一样。
这些问题迟早都会得到解决!
我要强调的是,这些问题并不是源于语言设计。Rust 正在不断改进。关于分配失败,人们已经开始研究它(参见跟踪问题)。此外,您不必使用liballoc
. heapless
板条箱将是一个不错的选择。
Rust 在内核领域的优点
尽管存在我上面提到的问题,但我发现用 Rust 编写内核是很有成效的,并且相信。让我谈谈我在内核态 Rust 上最喜欢的好东西:
- Rust 让我充满信心:它的类型系统以及所有权和生命周期概念让我意识到我的实现在编译时无法工作,例如,因为违反了共享 XOR 可变规则。一旦编译通过,它就会正常工作(例如令人讨厌的数据竞争和悬空指针取消引用)。
- 强制处理所有输入模式:
enum
和模式匹配 (match
) 允许您以富有表现力的方式处理所有可能的情况。您不需要被杂乱的链所困扰else if
。 - 它已经有了我编写内核所需的东西: 打包结构、原始指针、改进的内联汇编语法、嵌入汇编文件……
no_std
提供方便的(独立的)板条箱: 位标志操作库,基于数组的向量和字符串实现,多生产者多消费者队列,...- 内置单元测试:在 Rust 中编写和运行单元测试非常简单。此外,借助custom_test_frameworks功能,您可以在 QEMU 或真实机器上运行单元测试。
- 开发人员友好的强大工具链: linter帮助您编写良好的 Rust 代码,交叉编译非常简单,rust-analyzer将您最喜欢的编辑器变成像 IntelliJ IDEA 这样的高效 IDE。
我需要你的帮助!
该内核仍处于早期阶段。一些关键功能如 futex、epoll、UNIX 域套接字尚未实现。反过来说,代码还是简单易懂的!如果您有兴趣用 Rust 编写操作系统内核,请加入开发:)最后,我想快速提到 C 的另一种有前途的替代方案:Zig 编程语言。如果您是 C 程序员,您一定会印象深刻。我认为它在内核领域也有巨大的潜力。