Rust in Kernel 6.2 的进展

在一些技术媒体上看到 LWNJonathan Corbet 发表了一篇 Miguel 最新提交的 28 个补丁的分析文章 Rust in the 6.2 Kernel 。仔细搜索没有找到原文,发现这篇文章目前还是 Subscriber Only 的状态,为了抢先阅读详细内容就订阅了一年 LWN 的服务。以下内容是对这篇文章的概述,如果有错误在所难免,请 TG 联系 genedna 更正。


在这篇文章通篇都表达了 Jonathan Corbet 认为目前 Rust for Linux 的状态是初级。 Kernel 6.1 版本虽然带来 "Basic" 的支持,但是离实际应用还有很长的路要走。 Jonathan Corbet 强调 "Basic" 的支持仅限于写出 "Hello World" 级别的模块,还有很多相关的代码等待被社区 "Reviewed" 和 "merged" 到主线。Miguel Ojeda 最新提交的 28 个补丁,是从 "infrastructure" 级别支持使用 Rust 编写 Linux Kernel,更加接近 "real functionality"。

当前并不清楚这 28 个补丁 是否是为 6.2 版本合入的全部代码,虽然这和开发者期待马上使用 Rust 开发还有很大距离,但是对于 Linux Kernel 社区的 Maintainer 来说是好事,对于他们理解、Review 和合入代码是有帮助的。

The Rust-for-Linux work has been underway for a few years already; getting up to full functionality may well take a while longer yet. - _Jonathan CorbetLWN 的文章中最后说到。

1. 支持 Kernel 8 个级别的日志输出 macro

之前在 6.1 版本中,仅有 2 个 macro 支持 "info" 和 "emergency" 的日志输出,这次实现了 8 个完整的支持,包括 pr_err!()pr_warn!()pr_info!()pr_debug!()pr_devel!()pr_cont!()pr_emerg!()pr_alert!()。这些 macro 的使用方式和 C 语言中的 printk 完全一致,只是在 Rust 中使用 ! 代替了 %

use kernel::pr_cont;
use kernel::prelude::*;

module! {
    type: RustPrint,
    name: b"rust_print",
    author: b"Rust for Linux Contributors",
    description: b"Rust printing macros sample",
    license: b"GPL",
}

struct RustPrint;

impl kernel::Module for RustPrint {
    fn init(_module: &'static ThisModule) -> Result<Self> {
        pr_info!("Rust printing macros sample (init)\n");

        pr_emerg!("Emergency message (level 0) without args\n");
        pr_alert!("Alert message (level 1) without args\n");
        pr_crit!("Critical message (level 2) without args\n");
        pr_err!("Error message (level 3) without args\n");
        pr_warn!("Warning message (level 4) without args\n");
        pr_notice!("Notice message (level 5) without args\n");
        pr_info!("Info message (level 6) without args\n");

        pr_info!("A line that");
        pr_cont!(" is continued");
        pr_cont!(" without args\n");

        pr_emerg!("{} message (level {}) with args\n", "Emergency", 0);
        pr_alert!("{} message (level {}) with args\n", "Alert", 1);
        pr_crit!("{} message (level {}) with args\n", "Critical", 2);
        pr_err!("{} message (level {}) with args\n", "Error", 3);
        pr_warn!("{} message (level {}) with args\n", "Warning", 4);
        pr_notice!("{} message (level {}) with args\n", "Notice", 5);
        pr_info!("{} message (level {}) with args\n", "Info", 6);

        pr_info!("A {} that", "line");
        pr_cont!(" is {}", "continued");
        pr_cont!(" with {}\n", "args");

        Ok(RustPrint)
    }
}

impl Drop for RustPrint {
    fn drop(&mut self) {
        pr_info!("Rust printing macros sample (exit)\n");
    }
}

关于 Linux Kernel 的日志输出,可以参考 Message logging with printk

2. #[vtable]

在这次提交的 28 个补丁 中,添加了相当复杂的 #[vtable]。 在内核中广泛使用充满函数指针的结构,文章中给了一个读写操作的一个 struct 作为例子。

struct file_operations {
	struct module *owner;
	loff_t (*llseek) (struct file *, loff_t, int);
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
	ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
	ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
	int (*iopoll)(struct kiocb *kiocb, struct io_comp_batch *,
			unsigned int flags);
	int (*iterate) (struct file *, struct dir_context *);
	int (*iterate_shared) (struct file *, struct dir_context *);
	__poll_t (*poll) (struct file *, struct poll_table_struct *);
	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
	long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
	int (*mmap) (struct file *, struct vm_area_struct *);
	unsigned long mmap_supported_flags;
	int (*open) (struct inode *, struct file *);
	int (*flush) (struct file *, fl_owner_t id);
	int (*release) (struct inode *, struct file *);
	int (*fsync) (struct file *, loff_t, loff_t, int datasync);
	int (*fasync) (int, struct file *, int);
	int (*lock) (struct file *, int, struct file_lock *);
	ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
	unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
	int (*check_flags)(int);
	int (*flock) (struct file *, int, struct file_lock *);
	ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
	ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
	int (*setlease)(struct file *, long, struct file_lock **, void **);
	long (*fallocate)(struct file *file, int mode, loff_t offset,
			  loff_t len);
	void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
	unsigned (*mmap_capabilities)(struct file *);
#endif
	ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
			loff_t, size_t, unsigned int);
	loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,
				   struct file *file_out, loff_t pos_out,
				   loff_t len, unsigned int remap_flags);
	int (*fadvise)(struct file *, loff_t, loff_t, int);
	int (*uring_cmd)(struct io_uring_cmd *ioucmd, unsigned int issue_flags);
} __randomize_layout;

#[vtable] 的目的是在 C structRust trait 之间进行必要的 阻抗匹配阻抗匹配 是一个电学中的名词,大意是将两个不同的电路连接起来,使其能够正常工作。在这里,我觉得可以理解为将 C structRust trait "合适" 的映射起来,使其能够正常工作。

Rust 中的一个 struct 如果传递给 Linux Kernel 的其他部分使用,它必须被转换为合适的 Cstruct 。这个过程是由 bindgen 完成的,但是 bindgen 并不知道 Rusttrait 哪些操作已经实现,哪些操作应该用空指针表示,所以它无法将 Rusttrait 转换为 Cstruct。这就是 #[vtable] 的作用,它可以为每个定义的函数生成一个特殊的常量成员,在编译时生成 C 的代码可以查询这些常量,并为缺失的操作插入空指针。 这是一个相当复杂的 macro ,在文章中也没有进行更多的解释。

3. Vec 对内存申请的改进

Vec 是我们是我们写代码最经常用到的,这次补丁增加了 try_with_capacity() and try_with_capacity_in() 两个函数。这两个函数的作用是在 Vec 的内存申请失败时,返回 Err 而不是 panic。这样就可以在 Vec 的内存申请失败时,进行一些处理,而不是直接 panic

4. CStr 和 CString

提交了两个和 C 语言代码功能一致的 CStrCString ,我理解这提高了将 Rust 代码和 Kernel 其他代码进行交互的体验,这部分内容在反而评论区引起了热议,#[vatable] 倒是没有太多评论的声音。


我坚信 Rust for Linux 是这两年整个操作系统里面最大的进步,这个项目的存在,让我对 Rust 的未来充满了信心。 更多的开发者可以借助 Rust 进入到 Linux Kernel 领域,这对于 Linux Kernel 的发展是非常重要的。