LWN:用 Rust 实现 DebugFS!

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

DebugFS on Rust

By Daroc Alden
October 22, 2025
Kangrejos
Gemini flash translation
https://lwn.net/Articles/1041095/

DebugFS 是内核中一个“无所不能、不拘一格”的接口:每当内核开发者需要快速访问内核内部细节以调试问题,或实现实验性的控制接口时,他们都可以通过 DebugFS 暴露这些细节。之所以可以这样做,是因为 DebugFS 不受限于用户空间接口稳定性的常规规则,也不受限于暴露敏感内核信息的规则。在 Rust 驱动程序中支持 DebugFS 是实现对真实硬件上的真实驱动程序进行调试的重要一步。Matthew Maurer 在 Kangrejos 2025 会议上,谈论了他最近合并的 DebugFS Rust 绑定。

Maurer 首先概述了 DebugFS,包括实现 Rust API 的棘手之处。DebugFS 文件应该比它们允许访问的私有数据(private data)寿命更长,以防底层对象(underlying object)消失后,有人仍持有文件描述符(file descriptor)。此外,DebugFS 目录项(directory entries)可以在任何时候被移除,或者在父目录项被销毁时自动移除。“这将会给我们带来麻烦。”最后,DebugFS 目录必须手动拆除;它们的生命周期不局限于单个内核模块。

所有这些共同构成了一系列难以在 Rust 中严格建模的生命周期约束(lifetime constraints)。起初,Maurer 曾想将 DebugFS 文件实现为指向 Rust trait 对象(trait object)的弱引用计数指针(weak reference-counted pointer)。但这由于几个原因行不通,其中包括 DebugFS 文件没有销毁回调(destruction callback)。此外,DebugFS 为文件提供了一个字长(one word)的私有数据——通常用作指向其所关注对象的指针——但 Rust 指向 trait 对象的指针是两个字长(一个指向对象,一个指向其虚拟方法表(virtual method table))。

这些问题并非不可逾越——Maurer 本可以增加额外的指针间接(pointer indirection)——但这不够优雅。他希望找到一个能够自然契合 DebugFS 目录项生命周期的解决方案,同时只使用一个字长的私有数据并带来最小开销。Maurer 最终提出的设计是让目录项进行引用计数(reference-counted),这样它们只有在其所有子对象被丢弃且目录本身也被丢弃之后才会被销毁。为了实现这一目标,将向 Rust 暴露两种不同的接口:一种是针对具有简单生命周期的 DebugFS 目录的简单接口,以及一种更复杂、更通用的接口。

Maurer 称之为“文件 API”(File API)的简单 API,其 DebugFS 文件实际拥有其关联数据。暴露现有的 Rust 数据就像将其封装在 `debugfs::File<T>` 中一样简单;默认情况下,文件的读写操作会将值转换为字符串或从字符串转换,并相应地读取或更新它。程序员也可以附加自己的回调函数(callbacks)来实现自定义行为。缺点是无法让多个文件引用相同的数据(除非使用内部引用计数指针),并且无法根据某个运行时值(run-time value)的真假有条件地提供文件。

更复杂的 API,即“范围 API”(Scope API),允许多个文件引用相同的数据,以任意组合引用多个独立结构,有条件地创建文件等。但相应地,它不能删除单个子目录或文件——整个 DebugFS 目录需要一次性释放。

Maurer 演示了 如何使用每种 API 的例子;虽然有点复杂,但如果 Rust 获得内置的就地初始化,文件 API 的使用可以大大简化。这两种 API 都不太令人意外——但为了使其高效运作所需的晦涩的扭曲(即:巧妙的技巧)则有趣得多。

指针偷运(pointer smuggling)

如前所述,DebugFS 只为文件结构体提供了单个字长的私有数据,这通常是指向 DebugFS 文件底层数据的指针,而 Maurer 希望保留这一特性。但 DebugFS 的部分实用性在于开发者可以覆盖(override)文件操作(file operations),使用任意函数;这使得响应 DebugFS 文件的读写操作来触发驱动程序中的动作变得容易。这可以通过让 DebugFS 用户填写 struct file_operations 来实现,但 Maurer 希望 API 不那么冗长。在 Rust API 中以符合人体工程学的方式编码这一点,是允许程序员将函数或闭包(closure)附加到 `debugfs::File` 对象。不知何故,这些函数指针(function pointers)需要进入 DebugFS 使用的 `file_operations` 结构体中。但 Maurer 也不希望 API 在运行时为结构体分配空间——他希望在编译时静态生成适当的结构体,从而使整个 Rust DebugFS 接口实现无内存分配(allocation-free)。

Maurer 的解决方案依赖于这样一个事实:在 Rust 中,每个函数和闭包在编译时都有其自己独特的类型。这样做是因为它使得 LLVM Rust 编译器更容易应用某些优化——通过 Rust 函数指针的调用通常可以被降级为直接跳转或通过调度表(dispatch table)跳转,而不是通过实际指针调用。这使得 Rust 函数类型成为独有的零大小类型(zero-sized types):它们没有实际数据与之关联,因为类型足以让编译器确定函数的地址。

他的新 API 中的 `*_callback_file()` 函数,接受回调函数以实现文件的读写操作,但它们实际上并没有在任何地方存储所提供的函数指针。相反,回调函数的类型被作为泛型参数(generic argument)传递给填充 `file_operations` 结构体实例的代码。当 Rust 代码在编译期间被单态化(monomorphized)时,对于每个使用不同回调集的文件,都会生成一个不同的 `file_operations` 结构体。泛型代码将函数的类型转换回实际函数本身的指针,并调用它。由于转换是在编译时完成的,回调函数的指针在运行时实际上无需存储在 `file_operations` 结构体之外的任何地方。这个技巧有效地通过类型系统“偷运”(smuggles)了函数指针,这使得 Maurer 能够将构建所有所需 `file_operations` 结构体的工作转交给编译器的单态化实现,并避免分配内存。

对这个解释的反应褒贬不一。尽管在场的所有人都认为它很巧妙,并且允许编写一个优雅的 API,但也有一些人认为它可能 过于 聪明了。Gary Guo 指出了 Maurer 编写的(unsafe)代码的一个潜在问题,该代码将函数类型转换回实际函数指针:虽然它对于函数类型是正确的,但尝试将其用于其他零大小类型可能会导致未定义行为(undefined behavior),因为它没有确保检查类型的内部不变量(internal invariants)。

Guo 解释说,有些零大小类型,其值的实际地址很重要。例如,程序员可以创建一个零大小类型,表示特定地址的数据是可读的。Alice Ryhl 建议将函数限制为仅对实现 `Copy` trait 的类型操作,因为它们不能有依赖于稳定地址的不变量。Maurer 回答说,在这种情况下他并不担心,因为该函数旨在作为 DebugFS 接口的内部实现细节,但他同意在普遍情况下要求类型实现 `Copy` trait 是有意义的。一位在场的开发者询问 Pierre-Emmanuel Patry,他是否预计支持这样的代码会给 gccrs 带来问题;他认为这不会增加任何额外负担,因为标准库(standard library)的某些部分已经依赖于函数类型的行为。

Andreas Hindborg 询问了更多关于为何允许以这种方式通过类型系统“偷运”指针的细节——特别是 Maurer 为何声称类型需要“被实例化”("inhabited")才能使这个技巧奏效。Maurer 解释说,零大小类型可以有一个有效值(典型情况),或者没有有效值。因此,如果有人试图利用他的技巧创建一个指向存在的类型但无法构造该类型的值的指针,他们就可能破坏 Rust 的类型系统——这就是为什么辅助函数是 `unsafe` 的原因。

Hindborg 问这个指针偷运技巧是否在任何地方有文档记录。Maurer 回答说:“它在 代码 中有很好的文档记录”,引来一片笑声。Guo 问他们是否可以简单地更改 DebugFS C 结构体以包含两个指针,从而避免整个权宜之计。Maurer 将这个问题转给了 Greg Kroah-Hartman,后者回答说他认为不能,因为它会影响 inode 结构体(inode structure)的布局,而 inode 结构体在 DebugFS 之外被广泛使用。在他看来,这是一个“你为了乐趣而优化”("you optimized for fun")的例子——等效的 C 代码只会分配并承担额外的指针间接(pointer indirection)开销。但他认为这里使用奇怪的技术并没有什么问题;在许多方面,它正是 DebugFS 存在的意义。

最终,指针偷运解决方案确实保留在最终合并到 6.18 内核的补丁集中。不过,这种技巧不太可能在内核的 Rust 绑定中更广泛的上下文中使用。

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

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

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

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值