内核代码选用Linux-6.3.4, 对其中有关Rust代码进行简单的分析.
Minimal Rust
1// SPDX-License-Identifier: GPL-2.0
2
3//! Rust minimal sample.
4
5use kernel::prelude::*;
6
7module! {
8 type: RustMinimal,
9 name: "rust_minimal",
10 author: "Rust for Linux Contributors",
11 description: "Rust minimal sample",
12 license: "GPL",
13}
14
15struct RustMinimal {
16 numbers: Vec<i32>,
17}
18
19impl kernel::Module for RustMinimal {
20 fn init(_module: &'static ThisModule) -> Result<Self> {
21 pr_info!("Rust minimal sample (init)\n");
22 pr_info!("Am I built-in? {}\n", !cfg!(MODULE));
23
24 let mut numbers = Vec::new();
25 numbers.try_push(72)?;
26 numbers.try_push(108)?;
27 numbers.try_push(200)?;
28
29 Ok(RustMinimal { numbers })
30 }
31}
32
33impl Drop for RustMinimal {
34 fn drop(&mut self) {
35 pr_info!("My numbers are {:?}\n", self.numbers);
36 pr_info!("Rust minimal sample (exit)\n");
37 }
38}
参考 A first look at Rust in the 6.1 kernel [LWN.net]:
-
引入
rust/kernel/prelude.rs
定义的若干声明 -
用 C 编写的内核模块包括许多对宏的调用,如
MODULE_DESCRIPTION()
和MODULE_LICENSE()
,它们将模块的元数据存储在单独的 ELF 部分中。module_init ()
和module_exit()
宏分别标识模块的构造函数和析构函数。Rust 将大部分样板文件等价的放入单个宏调用中module!
-
使用
RustMinimal
包装Vec类型, 使得我们可以为其实现Module以及Drop等trait. 参考 孤儿规则与绕过方案:Rust的孤儿规则(Orphan Rule, OR)约束了当为某类型实现某trait时, 必须保证类型或特征至少有一个在当前作用域中定义. 这个原则可以保证别人用你库的时候不会破坏你库里函数的功能(同理, 你也不能破坏标准库函数的功能), 以此来保证一致性.
如何绕过孤儿规则? 使用newtype模式, 即创建一个新的元组结构体来封装该类型.
好消息是, 其在运行时没有任何性能损耗, 因为在编译期间, 该类型就会被自动忽略; 同时, 还可以通过重载warpper的一些方法来避免暴露底层的所有方法, 实现隐藏的目的. …
-
init ()函数应该执行通常的模块初始化工作.
-
在这种情况下, 它会向系统日志发出一些信息(在该过程中显示可用于在编译时查询内核配置参数的cfg!()宏).
-
然后它分配一个可变的Vec并尝试将三个数字放入其中. try_push()的使用在这里很重要: Vec会在必要时自行调整大小. 这涉及分配内存, 这在内核环境中可能会失败. 如果该分配失败, try_push() 将返回失败状态, 这反过来又会导致init() 返回失败
-
Rust中问号的使用:
具体来说,当使用 ? 运算符时,Rust 会自动进行如下操作: 检查当前表达式的结果是否为错误类型(例如 Result 或 Option)。 如果是错误,则立即将错误传播到当前函数的调用者,并终止当前函数的执行。 如果是正常值,则将其解包并继续执行后续的代码。
- 最后, 如果一切顺利, 它会返回一个带有分配的Vec和成功状态的RustMinimal结构. 由于此模块没有与任何其他内核子系统交互, 因此除了耐心等待被删除外, 它实际上不会做任何事情.
-
-
由于Module特征只有init方法, 见
rust/kernel/lib.rs
:
1/// The top level entrypoint to implementing a kernel module.
2///
3/// For any teardown or cleanup operations, your type may implement [`Drop`].
4pub trait Module: Sized + Sync {
5 /// Called at module initialization time.
6 ///
7 /// Use this method to perform whatever setup or registration your module
8 /// should do.
9 ///
10 /// Equivalent to the `module_init` macro in the C API.
11 fn init(module: &'static ThisModule) -> error::Result<Self>;
12}
因而使用了Drop特征作为RustMinimal
的析构函数.
Rust支持
rust/kernel/prelude.rs
1// SPDX-License-Identifier: GPL-2.0
2
3//! The `kernel` prelude.
4//!
5//! These are the most common items used by Rust code in the kernel,
6//! intended to be imported by all Rust code, for convenience.
7//!
8//! # Examples
9//!
10//! ```
11//! use kernel::prelude::*;
12//! ```
13
14#[doc(no_inline)]
15pub use core::pin::Pin;
16
17#[doc(no_inline)]
18pub use alloc::{boxed::Box, vec::Vec};
19
20#[doc(no_inline)]
21pub use macros::{module, vtable};
22
23pub use super::build_assert;
24
25// `super::std_vendor` is hidden, which makes the macro inline for some reason.
26#[doc(no_inline)]
27pub use super::dbg;
28pub use super::{pr_alert, pr_crit, pr_debug, pr_emerg, pr_err, pr_info, pr_notice, pr_warn};
29
30pub use super::static_assert;
31
32pub use super::error::{code::*, Error, Result};
33
34pub use super::{str::CStr, ThisModule};
-
core::pin: 这个命名空间应该是 Rust 标准库(std crate)的一部分,但为了避免对嵌入式设备等环境的过度依赖,Rust 也提供了一个核心库(core crate),其中包含了许多常用的类型和函数。因此,在这里使用 core::pin 而不是 std::pin 可以让代码更具有通用性。
-
alloc::{boxed, vec}: 这个命名空间中包含了 Rust 分配器 alloc crate 中的两个常见类型:Box 和 Vec。与核心库类似,alloc crate 应该也是标准库的一部分,但需要在程序中显式引入。
-
macros::{module, vtable}: 这个命名空间中包含了自定义宏 module 和 vtable。由于 Rust 的宏是编译时展开的语法扩展,因此它们本身并不占用运行时资源,也不存在库或模块的概念。在 Rust 中,宏可以在任何作用域内使用,只要它们已经被正确定义和导入。
-
super
表示到处父模块下的一些公共API:-
build_assert, static_assert, error, str:CStr
定义在rust/kernel/
下对应的rust文件下 -
dbg
定义在rust/kernel/std_vendor.rs
-
pr_
系列的宏定义在rust/kernel/print.rs
-
ThisModule
定义在rust/kernel/lib.rs
-
-
error::{code, Error, Result}, str::CStr, ThisModule: 这几个命名空间中包含了来自父模块的一些类型和函数,用于错误处理、字符串处理和模块元数据获取。由于这些功能较为特定,因此这些命名空间可能只存在于该 crate 中。
有关#[doc(no_inline)]
:
#[doc(no_inline)] 是 Rust 中的一个属性(attribute),用于指定某个项的文档注释不应该被内联到其他模块中。具体来说,当一个项被标记为 no_inline 时,在生成 API 文档时,该项的文档注释将只出现在自己所在的模块中,而不会出现在直接或间接导入该项的其他模块中。
需要注意的是,#[doc(no_inline)] 只影响文档注释的内联情况,而不影响代码本身的内联机制。例如,即使一个函数被标记为 no_inline,如果编译器认为将其内联到调用点更高效,仍然可能会在代码中进行内联。因此,在使用 no_inline 属性时,需要同时考虑文档注释和实际代码的内联情况,以便达到最佳的代码可读性和执行效率。
Rust代码目录结构
1rqdmap in ~/tmp/linux-6.3.4/rust via v13.1.1-gcc via v1.72.0-nightly
2[ 17:27:20 ] tree
3.
4├── alloc
5│ ├── alloc.rs
6│ ├── boxed.rs
7│ ├── collections
8│ │ └── mod.rs
9│ ├── lib.rs
10│ ├── raw_vec.rs
11│ ├── README.md
12│ ├── slice.rs
13│ └── vec
14│ ├── drain_filter.rs
15│ ├── drain.rs
16│ ├── into_iter.rs
17│ ├── is_zero.rs
18│ ├── mod.rs
19│ └── partial_eq.rs
20├── bindgen_parameters
21├── bindings
22│ ├── bindings_helper.h
23│ └── lib.rs
24├── build_error.rs
25├── compiler_builtins.rs
26├── exports.c
27├── helpers.c
28├── kernel
29│ ├── allocator.rs
30│ ├── build_assert.rs
31│ ├── error.rs
32│ ├── lib.rs
33│ ├── prelude.rs
34│ ├── print.rs
35│ ├── static_assert.rs
36│ ├── std_vendor.rs │ ├── str.rs │ ├── sync
37│ │ └── arc.rs
38│ ├── sync.rs
39│ └── types.rs
40├── macros
41│ ├── concat_idents.rs
42│ ├── helpers.rs
43│ ├── lib.rs
44│ ├── module.rs
45│ └── vtable.rs
46└── Makefile
- alloc目录的README中所说:
Rust 内核开发中使用的 alloc crate 来源于 Rust 标准库,以 Apache-2.0 或 MIT 许可证发布,并针对内核使用进行了一定的修改和适配。
为什么需要在内核中使用 alloc crate 呢?原因在于,Rust 的标准库中包含了一些内存分配、字符串、向量等常用的数据结构和算法,而这些功能在内核开发中也是非常有用的。但是,由于内核的运行环境与普通用户空间程序不同,存在一些限制和特殊需求(例如无法访问堆内存),因此需要对 alloc 进行适当的修改和限制,从而使它能够在内核中安全有效地使用。
在选择是否将 alloc 引入内核时,内核开发人员和 Rust 社区之间存在一些分歧。为了平衡两者的需求,他们决定保留一个基于 alloc 的子集,只添加必要的新方法,避免与 Rust 标准库过多地分叉。随着时间的推移,内核可能需要更多的功能和改进,而这些功能和改进应该被纳入 Rust 标准库中。到那时,内核就可以放弃自己的版本,转而使用标准库中的实现,以避免分歧和维护成本。
有关alloc的官方crate则可以参考: