背景| Linux 内核模块
Linux 内核模块在概念和原理层面与动态链接模块(DLL或so)类似。但对于 Linux 来说,内核模块可以在系统运行期间动态扩展系统功能,而无须重新启动系统,更无须重新编译新的系统内核镜像。所以,内核模块这个特性为内核开发者提供了极大的便利,因为对于号称世界上最大软件项目的Linux来说,重启或重新编译的时间耗费肯定是巨大的。
虽然设备驱动程序不一定都是内核模块,并且内核模块也不一定就是设备驱动程序,但是内核模块这种特性似乎注定是为设备驱动程序而生。Linux 系统下设备程序驱动开发过程中使用最多的工具之一是 insmod,用于向系统动态加载模块。
以内核模块存在的驱动程序,其文件数据组织形式上是ELF(Executable and Linkable Format)格式,更具体来说,内核模块是一种普通的可重定位目标文件(比如 demodev.ko)。对于静态编译链接而成的内核镜像而言,所有的符号引用都在静态链接阶段完成了。但是内核模块要使用内核提供的基础设施(通过调研内核函数的方式),所以内核和内核模块都通过符号表的形式向外部世界导出符号的相关信息,这种导出形式在代码层面是以EXPORT_SYMBOL
宏定义形式存在。然后通过内核模块加载机制加载模块,所有成功加载的模块都会以链表的形式放在内核的一个全局变量模块中。
正是因为内核模块这种机制,方便了Linux 贡献者选择设备驱动成为进入 Linux 复杂系统的一个入口点,而不会被 Linux 代码的复杂性而压倒。也正是因为内核模块这个特点,Rust for Linux 项目的目标就是让 Rust 成为Linux内核模块开发的第二语言。然后通过慢慢“蚕食”的方法,使得 Linux 中越来越多的组件使用 Rust 语言实现,最终达到提高 Linux 安全性的目的。当然,目前仅仅是实验性,很可能在 Linux 5.20 中把 Rust 支持合并进入。
将 Rust 引入 Linux 除了安全性,也带来另外一个好处,就是让越来越多的新人对 Linux 及 对其贡献充满兴趣,因为他们可以使用 Rust 语言。毕竟 Rust 语言是世界上最受欢迎的语言。用 Linus 的话来说,“我说过,内核很无聊,但我的意思是,从某种意义上说,许多新技术应该更有趣”。
作为一名技术人,同时也是一名 Rustaceans ,可以亲自目睹 Linux 引入 Rust 语言作为第二语言,也算是见证历史了。如果能参与一份贡献,那就更好了。现在这篇文章就是带你了解如何通过 Rust 为 Linux 编写内核模块。当然,为 Linux 做贡献并不容易,Linus 在前几天的开源峰会上也透露,虽然允许 Rust 进入 Linux,但毕竟也是实验性的,而且他还提前向未来为 Linux 做贡献的 Rust 开发者道歉,因为他很可能会对进入Linux 的 Rust 代码挑刺!
内核模块的生命周期
kernel-module-life
在编写模块之前需要知道模块的生命周期:
- 从内核模块被加载以后,会进行初始化。
- 接下来会在子系统(Subsystem,诸如进程调度、内存管理、虚拟文件系统、网络接口、进程间通信)上进行注册。
- 随后,可能一些系统角色(Actor),比如来自用户空间的用户可能要做一些动作,比如内存读取。会触发子系统的一些操作。子系统知道谁来处理。如果是内核模块,就会通过一个回调来告诉模块做它该做的事情,最终返回给 Actor。这一步实际可能会发生很多次。
- 模块被卸载的时候,会通过特定退出机制,让模块从子系统中注销,然后返回。
以上就是模块的整个生命周期,也可作为我们编写内核模块的一个宏观的心智模型。
从零编写一个字符驱动
Linux 中设备通常被分为三类,每个驱动模块通常实现为这三类中的其中一种:
- 字符设备。通常是指可以当作一个字节流来存取的设备(比如文件)。
- 块设备。通常是可以驻有文件系统的设备(比如磁盘),和字符设备类似,但块设备有一个请求缓冲区,因此它们可以选择响应请求的最佳顺序。
- 网络设备。通常是指能与其他主机交换数据的设备。
我们以编写一个简单的字符设备驱动为例,展示如何用 Rust 来编写内核驱动。
R4L 开发环境准备
为了方便,我们把 Rust for Linux 简称为 R4L。
首先,下载 Rust for Linux。
代码语言:javascript复制git clone https://github.com/Rust-for-Linux/linux.git
其他依赖项安装以及内核编译等详细内容可以参考这篇文章:[Rust Kernel Module: Getting Started](https://wusyong.github.io/posts/rust-kernel-module-00/) 。或者查看视频:Mentorship Session: Writing Linux Kernel Modules in Rust 。
系统要求:各种 Linux 发行版,比如 Ubuntu 等。最终是在 Qemu 上运行,并且可以使用 GDB 调试。
编写Scull 驱动
Scull(Simple Character Utility for Loading Locationslities)是 Linux 中实际存在的一个字符驱动。我们用 Rust 从头实现它。因为字符驱动比较容易理解。选择 Scull 也是因为它不依赖于硬件,它只是操作一些内核分配的内存,并且它基本只是用于演示和测试。
简单来说,Scull 就是用于操作内存区域的字符设备驱动程序。在《Linux 设备驱动程序》一书中拿它作为示例。
实现步骤
大约分为十一步来实现一个Scull驱动。
STEP 1:增加配置和空文件
在项目根目录下,打开 samples/rust/Kconfig
文件,添加相关配置:
// 复制 Kconfig 中 `config SAMPLE_RUST_ECHO_SERVER` 的配置进行修改
config SAMPLE_RUST_SCULL
tristate "Scull module"
help
This option builds the Rust Scull module sample.
To compile this as a module, choose M here:
the module will be called rust_scull.
If unsure, say N.
Linux kernel的目录结构下一般都会存在Kconfig和Makefile两个文件。分布在各级目录下的Kconfig构成了一个分布式的内核配置数据库,每个Kconfig分别描述了所属目录源文件相关的内核配置菜单。Kconfig是各种配置界面的源文件,内核的配置工具读取各个Kconfig文件,生成配置界面供开发人员配置内核,最后生成配置文件.config
。
然后打开 Makefile
:
// 复制 rust_echo_server.o 那行的配置,在其下方新增并修改为 rust_scull.0 相关内容
obj-$(CONFIG_SAMPLE_RUST_SCULL) = rust_scull.o
然后执行 make menuconfig
命令,会启动一个配置菜单界面,你可以搜索scull
,就能看到你配置的SAMPLE_RUST_SCULL
符号信息,然后选择exit
,就可以找到 Sample kernel code
列表。然后进入到该列表中,找到 Rust 目录,在里面就可以启用 SCULL 模块。(切换M
到Kernel hacking --> Sample kernel code --> Rust samples -->SAMPLE_RUST_SCULL
. )确认后退出配置界面,最终生成 .config
配置文件。
此时执行 make
命令。你会发现报错。因为此时实际并没有真正编写 SCULL 驱动。
接下来,创建一个空文件 samples/rust/rust_scull.rs
,并且执行 make
命令,你会看到它正常编译。
当前 git 状态为:
代码语言:javascript复制modified: samples/rust/Kconfig
modified: samples/rust/Makefile
new file: samples/rust/rust_scull.rs
STEP 2:模块声明和初始化,打印信息
在编写真正的驱动代码之前,需要先配置好 rust-analyzer
。在根目录下执行命令 make rust-analyzer
之后会创建 rust-product.json
文件。
“编写 Rust 内核模块的模版文件可以在这里找到:Rust-for-Linux/rust-out-of-tree-module Kernel crate 文档:https://rust-for-linux.github.io/docs/kernel/
现在打开 samples/rust/rust_scull.rs
来编写代码。
// SPDX-License-Identifier: GPL
//! Rust Scull sample
//!
// Rust 编写内核模块,不可以直接使用 std,而是用 `kernel` crate包装好的API。
// 当然,在需要的时候也可以使用 `core`和`alloc` crate,只不过是由 R4L 自己定义的,
// 包含了一些针对 R4L 特别定制的API,这些也同步到了官方 Rust 上游。
// 所以这里直接导入 kernel 库中预加载的一些模块,方便开发者使用。
use kernel::prelude::*;
// module! 是一个宏,用于声明内核模块,所以它是必须的。
// 通过文档或rust-analyzer 对其的代码提示,你能知道其具体用法
// 该宏必须指定的三种参数类型是: `type`、`name`和`license`
// 模块宏也可以接受命令行参数,但不是通过 `env::args()`,而是特定的宏语法
module! {
type: Scull,
name: b"scull",
author: b"ChaosBot",
description: b"Rust scull sample",
license: b"GPL",
// params: {
// /* 指定命令行参数 */
//}
}
// 对应模块定义中的 type
struct Scull;
// 为 Scull 实现 `kernel::Module` trait
// 该方法init相当于C API 中的宏 `module_init`,通过这个方法创建实例
impl kernel::Module for Scull {
// ThisModule 定义参加下方源码定义
fn init(_name: &'static CStr, _module: &'static ThisModule) -> Result<Self> {
// 映射到内核的打印宏
pr_info!("Rust Scull sample (init)n");
Ok(Scull)
}
}
然后执行 make
命令,正常编译。然后通过相关qemu
命令,比如:
// 具体执行命令详细可查看环境安装相关内容
$ sudo qemu-system-x86_64
-kernel vmlinux
-initrd initrd.img
- nic
user,
model=rtl1839,
hostfwd=tcp::553
然后就可以从 Qemu 的输出中看到 scull: Rust Scull sample (init)
这样的输出。
另,kernel::Module
trait 的定义:
/// The top level entrypoint to implementing a kernel module.
///
/// For any teardown or cleanup operations, your type may implement [`Drop`].
pub trait Module: Sized Sync {
/// Called at module initialization time.
///
/// Use this method to perform whatever setup or registration your module
/// should do.
///
/// Equivalent to the `module_init` macro in the C API.
fn init(name: &'static str::CStr, module: &'static ThisModule) -> Result<Self>;
}
/// Equivalent to `THIS_MODULE` in the C API.
///
/// C header: `include/linux/export.h`
pub struct ThisModule(*mut bindings::module);
可以对比一下 C 语言写的HelloWord 模块:
代码语言:javascript复制/*
* hello-1.c - The simplest kernel module.
*/
#include <linux/kernel.h> /* Needed for pr_info() */
#include <linux/module.h> /* Needed by all modules */
int init_module(void)
{
pr_info("Hello world 1.n");
/* A non 0 return means init_module failed; module can't be loaded. */
return 0;
}
void cleanup_module(void)
{
pr_info("Goodbye world 1.n");
}
MODULE_LICENSE("GPL");
看得出来, 内核模块必须至少有两个函数:一个在模块被编入内核时调用的初始化函数,以及一个 在将模块从内核中删除之前调用的清理函数。Rust 模块目前暂时不需要清理。
STEP 3: 增加最小化文件操作的实现
接下来我们为Scull 模块增加一个简单的文件操作功能。
代码语言:javascript复制// SPDX-License-Identifier: GPL
//! Rust Scull sample
//!
use kernel::prelude::*;
// 使用kernel的文件模块
use kernel::file;
module! {
type: Scull,
name: b"scull",
author: b"ChaosBot",
description: b"Rust scull sample",
license: b"GPL",
// params: {
// /* 指定命令行参数 */
//}
}
// 对应模块定义中的 type
struct Scull;
// 为 Scull 实现 file::Operations trait
// 该 trait 定义了内核文件操作的各种方法,诸如 `open/read/write/seek/fsync/mmap/poll 等
// 对应于内核的 `file_operations` 结构体,支持多线程/多进程
// 该结构在include/linux/fs.h中定义,并保存指向由驱动程序定义的函数的指针,
// 这些函数在设备上执行各种操作。该结构的每个字段对应于驱动程序定义的某些函数的地址,以处理请求的操作。
// 文档地址:https://rust-for-linux.github.io/docs/kernel/file/trait.Operations.html
// `#[vtable]`宏表示要建立一个vtable,在这个表中执行文件
#[vtable]
impl file::Operations for Scull {
fn open(_context: &(), _file: &file::File) -> Result {
pr_info!("File was openedn");
Ok(())
}
}
// 为 Scull 实现 `kernel::Module` trait
// 该方法init相当于C API 中的宏 `module_init`,通过这个方法创建实例
impl kernel::Module for Scull {
// ThisModule 定义参加下方源码定义
fn init(_name: &'static CStr, _module: &'static ThisModule) -> Result<Self> {
// 映射到内核的打印宏
pr_info!("Rust Scull sample (init)n");
Ok(Scull)
}
}
此时,如果编译并运行,会看到 qemu输出 :ls: /dev/scull*: No such file or directory
。
我们现在编写的是一个字符设备。字符设备是通过设备文件访问的,设备文件通常位于 /dev
。这是约定俗成的。编写驱动程序时,将设备文件放在当前目录下即可。只需确保将其放在/dev
中作为生产驱动程序即可。
SETP4: 注册一个 misc 设备
回想一下前面内核模块的生命周期,接下来我们需要将驱动程序注册到子系统。
我们要将设备注册的是misc子系统,它是 Linux 中最小的子系统。对于编写功能简单的字符设备驱动,使用 misc 子系统提供的接口是最方便的。
misc 设备共享一个主设备号MISC_MAJOR(10)
,所有的misc设备形成一个链表,对设备访问时内核根据次设备号查找对应的 misc设备,然后调用其中的file_operations
结构体中注册的文件操作接口进程操作。
// SPDX-License-Identifier: GPL
//! Rust Scull sample
//!
use kernel::prelude::*;
// kernel crate中提供了对misc设备的包装
use kernel::{file, miscdev};
module! {
type: Scull,
name: b"scull",
author: b"ChaosBot",
description: b"Rust scull sample",
license: b"GPL",
// params: {
// /* 指定命令行参数 */
//}
}
// 对应模块定义中的 type
// 为 Scull 增加 dev 字段来保存这个注册信息
struct Scull {
_dev: Pin<Box<miscdev::Registration<Scull>>>,
}
// 为 Scull 实现 file::Operations trait
// 该 trait 定义了内核文件操作的各种方法,诸如 `open/read/write/seek/fsync/mmap/poll 等
// 对应于内核的 `file_operations` 结构体,支持多线程/多进程
// 该结构在include/linux/fs.h中定义,并保存指向由驱动程序定义的函数的指针,
// 这些函数在设备上执行各种操作。该结构的每个字段对应于驱动程序定义的某些函数的地址,以处理请求的操作。
// 文档地址:https://rust-for-linux.github.io/docs/kernel/file/trait.Operations.html
// `#[vtable]`宏表示要建立一个vtable,在这个表中执行文件
#[vtable]
impl file::Operations for Scull {
fn open(_context: &(), _file: &file::File) -> Result {
pr_info!("File was openedn");
Ok(())
}
}
// 为 Scull 实现 `kernel::Module` trait
// 该方法init相当于C API 中的宏 `module_init`,通过这个方法创建实例
impl kernel::Module for Scull {
// ThisModule 定义参加下方源码定义
fn init(_name: &'static CStr, _module: &'static ThisModule) -> Result<Self> {
// 映射到内核的打印宏
pr_info!("Rust Scull sample (init)n");
// 新增注册代码
let reg = miscdev::Registration::new_pinned(fmt!("scull"), ())?;
Ok(Self { _dev: reg })
}
}
然后将其编译且执行,在日志中会看到scull: File was opened
这样的信息。但是会出现 read error
之类的错误,因为并没有真正读取什么内容。
关于 miscdev 模块 API 中使用 Pin
的原因
因为当 misc 设备注册成功的时候,正如前面所说,会将传入的 miscdevice 添加到一个链表中,而这个链表正是侵入式链表。这意味着,它存在一个自引用结构,所以在注册成功的时候是 Unsafe 的,所以必须使用Pin将其固定防止移动,否则会出现悬空指针。具体可以参考 rust/kernel/src/miscdev.rs
源码。
这个接口设计其实有两个选择,一种是使用 Box 来包装 misc 设备注册,另一种是使用复杂的 Pin API。前者性能不好,需要额外分配内存。所以选择了后者,是零成本抽象。new_pinned
命名也是特意为之,为了呈现Pin语义。
STEP5: 增加文件读写
代码语言:javascript复制// SPDX-License-Identifier: GPL
//! Rust Scull sample
//!
use kernel::prelude::*;
// kernel crate中提供了对misc设备的包装
use kernel::{file, miscdev};
module! {
type: Scull,
name: b"scull",
author: b"ChaosBot",
description: b"Rust scull sample",
license: b"GPL",
// params: {
// /* 指定命令行参数 */
//}
}
// 对应模块定义中的 type
// 为 Scull 增加 dev 字段来保存这个注册信息
struct Scull {
_dev: Pin<Box<miscdev::Registration<Scull>>>,
}
// 为 Scull 实现 file::Operations trait
// 该 trait 定义了内核文件操作的各种方法,诸如 `open/read/write/seek/fsync/mmap/poll 等
// 对应于内核的 `file_operations` 结构体,支持多线程/多进程
// 该结构在include/linux/fs.h中定义,并保存指向由驱动程序定义的函数的指针,
// 这些函数在设备上执行各种操作。该结构的每个字段对应于驱动程序定义的某些函数的地址,以处理请求的操作。
// 文档地址:https://rust-for-linux.github.io/docs/kernel/file/trait.Operations.html
// `#[vtable]`宏表示要建立一个vtable,在这个表中执行文件
#[vtable]
impl file::Operations for Scull {
fn open(_context: &(), _file: &file::File) -> Result {
pr_info!("File was openedn");
Ok(())
}
// 新增文件读
// 可从 `file::Operations` trait 文档中直接查看该函数签名
fn read(
_data: (),
_file: &file::File,
_writer: &mut impl IoBufferWriter,
_offset: u64,
) -> Result<usize> {
pr_info!("File was readn");
Ok(0)
}
// 新增文件写
// 可从 `file::Operations` trait 文档中直接查看该函数签名
fn write(
_data: (),
_file: &file::File,
reader: &mut impl IoBufferReader,
_offset: u64,
) -> Result<usize> {
pr_info!("File was writtenn");
Ok(reader.len())
}
}
// 为 Scull 实现 `kernel::Module` trait
// 该方法init相当于C API 中的宏 `module_init`,通过这个方法创建实例
impl kernel::Module for Scull {
// ThisModule 定义参加下方源码定义
fn init(_name: &'static CStr, _module: &'static ThisModule) -> Result<Self> {
// 映射到内核的打印宏
pr_info!("Rust Scull sample (init)n");
// 新增注册代码
let reg = miscdev::Registration::new_pinned(fmt!("scull"), ())?;
Ok(Self { _dev: reg })
}
}
STEP6: 为设备保存写入数据
每个设备都有自己的附加数据,当 write 被调用时,我们可以更新它。
代码语言:javascript复制// SPDX-License-Identifier: GPL
//! Rust Scull sample
//!
use kernel::prelude::*;
// kernel crate中提供了对misc设备的包装
use kernel::{file, miscdev};
// 新增 sync 模块的引用计数指针 Ref ,以及对 Ref 借用的 RefBorrow
use kernel::sync::{Ref, RefBorrow};
module! {
type: Scull,
name: b"scull",
author: b"ChaosBot",
description: b"Rust scull sample",
license: b"GPL",
// params: {
// /* 指定命令行参数 */
//}
}
// 新增设备
struct Device {
number: usize, // 设备号
contents: Vec<u8>, // 设备数据
}
// 对应模块定义中的 type
// 为 Scull 增加 dev 字段来保存这个注册信息
struct Scull {
_dev: Pin<Box<miscdev::Registration<Scull>>>,
}
// 为 Scull 实现 file::Operations trait
// 该 trait 定义了内核文件操作的各种方法,诸如 `open/read/write/seek/fsync/mmap/poll 等
// 对应于内核的 `file_operations` 结构体,支持多线程/多进程
// 该结构在include/linux/fs.h中定义,并保存指向由驱动程序定义的函数的指针,
// 这些函数在设备上执行各种操作。该结构的每个字段对应于驱动程序定义的某些函数的地址,以处理请求的操作。
// 文档地址:https://rust-for-linux.github.io/docs/kernel/file/trait.Operations.html
// `#[vtable]`宏表示要建立一个vtable,在这个表中执行文件
#[vtable]
impl file::Operations for Scull {
type OpenData = Ref<Device>;
type Data = Ref<Device>;
// 在调用 open 的时候会指向 Device 指针,所以用 Ref 包起来
fn open(context: &Ref<Device>, _file: &file::File) -> Result<Ref<Device>> {
pr_info!("File for device {} was openedn", context.number);
Ok(context.clone())
}
fn read(
data: RefBorrow<'_, Device>,
_file: &file::File,
_writer: &mut impl IoBufferWriter,
_offset: u64,
) -> Result<usize> {
pr_info!("File for device {} was readn", data.number);
Ok(0)
}
fn write(
data: RefBorrow<'_, Device>,
_file: &file::File,
reader: &mut impl IoBufferReader,
_offset: u64,
) -> Result<usize> {
pr_info!("File for device {} was writtenn", data.number);
let copy = reader.read_all()?;
data.contents = copy;
Ok(copy.len())
}
}
// 为 Scull 实现 `kernel::Module` trait
// 该方法init相当于C API 中的宏 `module_init`,通过这个方法创建实例
impl kernel::Module for Scull {
// ThisModule 定义参加下方源码定义
fn init(_name: &'static CStr, _module: &'static ThisModule) -> Result<Self> {
// 映射到内核的打印宏
pr_info!("Rust Scull sample (init)n");
// 初始化设备
let dev = Ref::try_new(Device {
number: 0,
contents: Vec::new(),
})?;
// 新增注册代码
let reg = miscdev::Registration::new_pinned(fmt!("scull"), ())?;
Ok(Self { _dev: reg })
}
}
因为 Rust 所有权管理内存,就不需要手动释放内存了。
STEP 7: 增加互斥锁
当前的Scull 程序存在一个并发缺陷:假设有多个进程试图对Device进行读写,那里将会产生数据竞争。所以需要加锁。
代码语言:javascript复制// SPDX-License-Identifier: GPL
//! Rust Scull sample
//!
use kernel::prelude::*;
// kernel crate中提供了对misc设备的包装
use kernel::{file, miscdev};
// 增加 sync 模块的引用计数指针 Ref ,以及对 Ref 借用的 RefBorrow
// 新增 smutex::Mutex
use kernel::sync::{smutex::Mutex, Ref, RefBorrow};
module! {
type: Scull,
name: b"scull",
author: b"ChaosBot",
description: b"Rust scull sample",
license: b"GPL",
// params: {
// /* 指定命令行参数 */
//}
}
// 新增设备
struct Device {
number: usize, // 设备号
// 使用Mutex 来保护 contents ,避免数据竞争
contents: Mutex<Vec<u8>>, // 设备数据
}
// 对应模块定义中的 type
// 为 Scull 增加 dev 字段来保存这个注册信息
struct Scull {
_dev: Pin<Box<miscdev::Registration<Scull>>>,
}
// 为 Scull 实现 file::Operations trait
// 该 trait 定义了内核文件操作的各种方法,诸如 `open/read/write/seek/fsync/mmap/poll 等
// 对应于内核的 `file_operations` 结构体,支持多线程/多进程
// 该结构在include/linux/fs.h中定义,并保存指向由驱动程序定义的函数的指针,
// 这些函数在设备上执行各种操作。该结构的每个字段对应于驱动程序定义的某些函数的地址,以处理请求的操作。
// 文档地址:https://rust-for-linux.github.io/docs/kernel/file/trait.Operations.html
// `#[vtable]`宏表示要建立一个vtable,在这个表中执行文件
#[vtable]
impl file::Operations for Scull {
type OpenData = Ref<Device>;
type Data = Ref<Device>;
// 在调用 open 的时候会指向 Device 指针,所以用 Ref 包起来
fn open(context: &Ref<Device>, _file: &file::File) -> Result<Ref<Device>> {
pr_info!("File for device {} was openedn", context.number);
Ok(context.clone())
}
fn read(
data: RefBorrow<'_, Device>,
_file: &file::File,
_writer: &mut impl IoBufferWriter,
_offset: u64,
) -> Result<usize> {
pr_info!("File for device {} was readn", data.number);
Ok(0)
}
fn write(
data: RefBorrow<'_, Device>,
_file: &file::File,
reader: &mut impl IoBufferReader,
_offset: u64,
) -> Result<usize> {
pr_info!("File for device {} was writtenn", data.number);
let copy = reader.read_all()?;
let len = copy.len();
// 获取锁
*data.contents.lock() = copy;
Ok(copy.len())
}
}
// 为 Scull 实现 `kernel::Module` trait
// 该方法init相当于C API 中的宏 `module_init`,通过这个方法创建实例
impl kernel::Module for Scull {
// ThisModule 定义参加下方源码定义
fn init(_name: &'static CStr, _module: &'static ThisModule) -> Result<Self> {
// 映射到内核的打印宏
pr_info!("Rust Scull sample (init)n");
// 初始化设备
let dev = Ref::try_new(Device {
number: 0,
contents: Mutex::new(Vec::new()), // 加锁
})?;
// 新增注册代码
let reg = miscdev::Registration::new_pinned(fmt!("scull"), ())?;
Ok(Self { _dev: reg })
}
}
STEP8: 在 read 时返回保存的数据
代码语言:javascript复制// SPDX-License-Identifier: GPL
//! Rust Scull sample
//!
use kernel::prelude::*;
// kernel crate中提供了对misc设备的包装
use kernel::{file, miscdev};
// 增加 sync 模块的引用计数指针 Ref ,以及对 Ref 借用的 RefBorrow
// 新增 smutex::Mutex
use kernel::sync::{smutex::Mutex, Ref, RefBorrow};
// 新增 io_buffer 模块
use kernel::io_buffer::{IoBufferReader, IoBufferWriter};
module! {
type: Scull,
name: b"scull",
author: b"ChaosBot",
description: b"Rust scull sample",
license: b"GPL",
// params: {
// /* 指定命令行参数 */
//}
}
// 新增设备
struct Device {
number: usize, // 设备号
// 使用Mutex 来保护 contents ,避免数据竞争
contents: Mutex<Vec<u8>>, // 设备数据
}
// 对应模块定义中的 type
// 为 Scull 增加 dev 字段来保存这个注册信息
struct Scull {
_dev: Pin<Box<miscdev::Registration<Scull>>>,
}
// 为 Scull 实现 file::Operations trait
// 该 trait 定义了内核文件操作的各种方法,诸如 `open/read/write/seek/fsync/mmap/poll 等
// 对应于内核的 `file_operations` 结构体,支持多线程/多进程
// 该结构在include/linux/fs.h中定义,并保存指向由驱动程序定义的函数的指针,
// 这些函数在设备上执行各种操作。该结构的每个字段对应于驱动程序定义的某些函数的地址,以处理请求的操作。
// 文档地址:https://rust-for-linux.github.io/docs/kernel/file/trait.Operations.html
// `#[vtable]`宏表示要建立一个vtable,在这个表中执行文件
#[vtable]
impl file::Operations for Scull {
type OpenData = Ref<Device>;
type Data = Ref<Device>;
// 在调用 open 的时候会指向 Device 指针,所以用 Ref 包起来
fn open(context: &Ref<Device>, _file: &file::File) -> Result<Ref<Device>> {
pr_info!("File for device {} was openedn", context.number);
Ok(context.clone())
}
fn read(
data: RefBorrow<'_, Device>,
_file: &file::File,
writer: &mut impl IoBufferWriter,
offset: u64,
) -> Result<usize> {
pr_info!("File for device {} was readn", data.number);
let offset = offset.try_into()?;
let vec = data.contents.lock(); // 获取锁,避免脏读
let len = core::cmp::min(writer.len(), vec.len().saturating_sub(offset));
writer.write_slice(&vec[offset..][..len])?;
Ok(len)
}
fn write(
data: RefBorrow<'_, Device>,
_file: &file::File,
reader: &mut impl IoBufferReader,
_offset: u64,
) -> Result<usize> {
pr_info!("File for device {} was writtenn", data.number);
let copy = reader.read_all()?;
let len = copy.len();
// 获取锁
*data.contents.lock() = copy;
Ok(copy.len())
}
}
// 为 Scull 实现 `kernel::Module` trait
// 该方法init相当于C API 中的宏 `module_init`,通过这个方法创建实例
impl kernel::Module for Scull {
// ThisModule 定义参加下方源码定义
fn init(_name: &'static CStr, _module: &'static ThisModule) -> Result<Self> {
// 映射到内核的打印宏
pr_info!("Rust Scull sample (init)n");
// 初始化设备
let dev = Ref::try_new(Device {
number: 0,
contents: Mutex::new(Vec::new()), // 加锁
})?;
// 新增注册代码
let reg = miscdev::Registration::new_pinned(fmt!("scull"), ())?;
Ok(Self { _dev: reg })
}
}
STEP9: 优化缓冲数据
代码语言:javascript复制// SPDX-License-Identifier: GPL
//! Rust Scull sample
//!
use kernel::prelude::*;
// kernel crate中提供了对misc设备的包装
use kernel::{file, miscdev};
// 增加 sync 模块的引用计数指针 Ref ,以及对 Ref 借用的 RefBorrow
// 新增 smutex::Mutex
use kernel::sync::{smutex::Mutex, Ref, RefBorrow};
// 新增 io_buffer 模块
use kernel::io_buffer::{IoBufferReader, IoBufferWriter};
module! {
type: Scull,
name: b"scull",
author: b"ChaosBot",
description: b"Rust scull sample",
license: b"GPL",
// params: {
// /* 指定命令行参数 */
//}
}
// 新增设备
struct Device {
number: usize, // 设备号
// 使用Mutex 来保护 contents ,避免数据竞争
contents: Mutex<Vec<u8>>, // 设备数据
}
// 对应模块定义中的 type
// 为 Scull 增加 dev 字段来保存这个注册信息
struct Scull {
_dev: Pin<Box<miscdev::Registration<Scull>>>,
}
// 为 Scull 实现 file::Operations trait
// 该 trait 定义了内核文件操作的各种方法,诸如 `open/read/write/seek/fsync/mmap/poll 等
// 对应于内核的 `file_operations` 结构体,支持多线程/多进程
// 该结构在include/linux/fs.h中定义,并保存指向由驱动程序定义的函数的指针,
// 这些函数在设备上执行各种操作。该结构的每个字段对应于驱动程序定义的某些函数的地址,以处理请求的操作。
// 文档地址:https://rust-for-linux.github.io/docs/kernel/file/trait.Operations.html
// `#[vtable]`宏表示要建立一个vtable,在这个表中执行文件
#[vtable]
impl file::Operations for Scull {
type OpenData = Ref<Device>;
type Data = Ref<Device>;
fn open(context: &Ref<Device>, file: &file::File) -> Result<Ref<Device>> {
pr_info!("File for device {} was openedn", context.number);
// 以只写模式打开文件,则对contents清零
if file.flags() & file::flags::O_ACCMODE == file::flags::O_WRONLY {
context.contents.lock().clear();
}
Ok(context.clone())
}
fn read(
data: RefBorrow<'_, Device>,
_file: &file::File,
writer: &mut impl IoBufferWriter,
offset: u64,
) -> Result<usize> {
pr_info!("File for device {} was readn", data.number);
let offset = offset.try_into()?;
let vec = data.contents.lock(); // 获取锁,避免脏读
let len = core::cmp::min(writer.len(), vec.len().saturating_sub(offset));
writer.write_slice(&vec[offset..][..len])?;
Ok(len)
}
// 优化: 精细化内存管理,减少内存分配
fn write(
data: RefBorrow<'_, Device>,
_file: &file::File,
reader: &mut impl IoBufferReader,
offset: u64,
) -> Result<usize> {
pr_info!("File for device {} was writtenn", data.number);
let offset = offset.try_into()?;
let len = reader.len();
let new_len = len.checked_add(offset).ok_or(EINVAL)?;
let mut vec = data.contents.lock();
if new_len > vec.len() {
vec.try_resize(new_len, 0)?;
}
reader.read_slice(&mut vec[offset..][..len])?;
Ok(len)
}
}
// 为 Scull 实现 `kernel::Module` trait
// 该方法init相当于C API 中的宏 `module_init`,通过这个方法创建实例
impl kernel::Module for Scull {
// ThisModule 定义参加下方源码定义
fn init(_name: &'static CStr, _module: &'static ThisModule) -> Result<Self> {
// 映射到内核的打印宏
pr_info!("Rust Scull sample (init)n");
// 初始化设备
let dev = Ref::try_new(Device {
number: 0,
contents: Mutex::new(Vec::new()), // 加锁
})?;
// 新增注册代码
let reg = miscdev::Registration::new_pinned(fmt!("scull"), ())?;
Ok(Self { _dev: reg })
}
}
STEP10: 定义内核模块参数
为了支持多个设备,需要让模块支持外部参数。
代码语言:javascript复制// SPDX-License-Identifier: GPL
//! Rust Scull sample
//!
use kernel::prelude::*;
// kernel crate中提供了对misc设备的包装
use kernel::{file, miscdev};
// 增加 sync 模块的引用计数指针 Ref ,以及对 Ref 借用的 RefBorrow
// 新增 smutex::Mutex
use kernel::sync::{smutex::Mutex, Ref, RefBorrow};
// 新增 io_buffer 模块
use kernel::io_buffer::{IoBufferReader, IoBufferWriter};
module! {
type: Scull,
name: b"scull",
author: b"ChaosBot",
description: b"Rust scull sample",
license: b"GPL",
params: { // 定义模块参数 nr_devs
nr_devs: u32 {
default: 1,
permissions: 0o644,
description: b"Number of scull devices",
},
},
}
// 新增设备
struct Device {
number: usize, // 设备号
// 使用Mutex 来保护 contents ,避免数据竞争
contents: Mutex<Vec<u8>>, // 设备数据
}
// 对应模块定义中的 type
// 为 Scull 增加 dev 字段来保存这个注册信息
struct Scull {
_dev: Pin<Box<miscdev::Registration<Scull>>>,
}
// 为 Scull 实现 file::Operations trait
// 该 trait 定义了内核文件操作的各种方法,诸如 `open/read/write/seek/fsync/mmap/poll 等
// 对应于内核的 `file_operations` 结构体,支持多线程/多进程
// 该结构在include/linux/fs.h中定义,并保存指向由驱动程序定义的函数的指针,
// 这些函数在设备上执行各种操作。该结构的每个字段对应于驱动程序定义的某些函数的地址,以处理请求的操作。
// 文档地址:https://rust-for-linux.github.io/docs/kernel/file/trait.Operations.html
// `#[vtable]`宏表示要建立一个vtable,在这个表中执行文件
#[vtable]
impl file::Operations for Scull {
type OpenData = Ref<Device>;
type Data = Ref<Device>;
fn open(context: &Ref<Device>, file: &file::File) -> Result<Ref<Device>> {
pr_info!("File for device {} was openedn", context.number);
// 以只写模式打开文件,则对contents清零
if file.flags() & file::flags::O_ACCMODE == file::flags::O_WRONLY {
context.contents.lock().clear();
}
Ok(context.clone())
}
fn read(
data: RefBorrow<'_, Device>,
_file: &file::File,
writer: &mut impl IoBufferWriter,
offset: u64,
) -> Result<usize> {
pr_info!("File for device {} was readn", data.number);
let offset = offset.try_into()?;
let vec = data.contents.lock(); // 获取锁,避免脏读
let len = core::cmp::min(writer.len(), vec.len().saturating_sub(offset));
writer.write_slice(&vec[offset..][..len])?;
Ok(len)
}
// 优化: 精细化内存管理,减少内存分配
fn write(
data: RefBorrow<'_, Device>,
_file: &file::File,
reader: &mut impl IoBufferReader,
offset: u64,
) -> Result<usize> {
pr_info!("File for device {} was writtenn", data.number);
let offset = offset.try_into()?;
let len = reader.len();
let new_len = len.checked_add(offset).ok_or(EINVAL)?;
let mut vec = data.contents.lock();
if new_len > vec.len() {
vec.try_resize(new_len, 0)?;
}
reader.read_slice(&mut vec[offset..][..len])?;
Ok(len)
}
}
// 为 Scull 实现 `kernel::Module` trait
// 该方法init相当于C API 中的宏 `module_init`,通过这个方法创建实例
impl kernel::Module for Scull {
// ThisModule 定义参加下方源码定义
fn init(_name: &'static CStr, _module: &'static ThisModule) -> Result<Self> {
// 获取锁并读取模块参数
let lock = module.kernel_param_lock();
pr_info!("Hello world, {} devices!n", nr_devs.read(&lock));
// 初始化设备
let dev = Ref::try_new(Device {
number: 0,
contents: Mutex::new(Vec::new()), // 加锁
})?;
// 新增注册代码
let reg = miscdev::Registration::new_pinned(fmt!("scull"), ())?;
Ok(Self { _dev: reg })
}
}
STEP11: 支持多个设备
代码语言:javascript复制// SPDX-License-Identifier: GPL
//! Rust Scull sample
//!
use kernel::prelude::*;
// kernel crate中提供了对misc设备的包装
use kernel::{file, miscdev};
// 增加 sync 模块的引用计数指针 Ref ,以及对 Ref 借用的 RefBorrow
// 新增 smutex::Mutex
use kernel::sync::{smutex::Mutex, Ref, RefBorrow};
// 新增 io_buffer 模块
use kernel::io_buffer::{IoBufferReader, IoBufferWriter};
module! {
type: Scull,
name: b"scull",
author: b"ChaosBot",
description: b"Rust scull sample",
license: b"GPL",
params: { // 定义模块参数 nr_devs
nr_devs: u32 {
default: 1,
permissions: 0o644,
description: b"Number of scull devices",
},
},
}
// 新增设备
struct Device {
number: usize, // 设备号
// 使用Mutex 来保护 contents ,避免数据竞争
contents: Mutex<Vec<u8>>, // 设备数据
}
// 对应模块定义中的 type
// 为 Scull 增加 dev 字段来保存这个注册信息
struct Scull {
// 使用 Vec 来保存多个设备的信息
_dev: Vec<Pin<Box<miscdev::Registration<Scull>>>>,
}
// 为 Scull 实现 file::Operations trait
// 该 trait 定义了内核文件操作的各种方法,诸如 `open/read/write/seek/fsync/mmap/poll 等
// 对应于内核的 `file_operations` 结构体,支持多线程/多进程
// 该结构在include/linux/fs.h中定义,并保存指向由驱动程序定义的函数的指针,
// 这些函数在设备上执行各种操作。该结构的每个字段对应于驱动程序定义的某些函数的地址,以处理请求的操作。
// 文档地址:https://rust-for-linux.github.io/docs/kernel/file/trait.Operations.html
// `#[vtable]`宏表示要建立一个vtable,在这个表中执行文件
#[vtable]
impl file::Operations for Scull {
type OpenData = Ref<Device>;
type Data = Ref<Device>;
fn open(context: &Ref<Device>, file: &file::File) -> Result<Ref<Device>> {
pr_info!("File for device {} was openedn", context.number);
// 以只写模式打开文件,则对contents清零
if file.flags() & file::flags::O_ACCMODE == file::flags::O_WRONLY {
context.contents.lock().clear();
}
Ok(context.clone())
}
fn read(
data: RefBorrow<'_, Device>,
_file: &file::File,
writer: &mut impl IoBufferWriter,
offset: u64,
) -> Result<usize> {
pr_info!("File for device {} was readn", data.number);
let offset = offset.try_into()?;
let vec = data.contents.lock(); // 获取锁,避免脏读
let len = core::cmp::min(writer.len(), vec.len().saturating_sub(offset));
writer.write_slice(&vec[offset..][..len])?;
Ok(len)
}
// 优化: 精细化内存管理,减少内存分配
fn write(
data: RefBorrow<'_, Device>,
_file: &file::File,
reader: &mut impl IoBufferReader,
offset: u64,
) -> Result<usize> {
pr_info!("File for device {} was writtenn", data.number);
let offset = offset.try_into()?;
let len = reader.len();
let new_len = len.checked_add(offset).ok_or(EINVAL)?;
let mut vec = data.contents.lock();
if new_len > vec.len() {
vec.try_resize(new_len, 0)?;
}
reader.read_slice(&mut vec[offset..][..len])?;
Ok(len)
}
}
// 为 Scull 实现 `kernel::Module` trait
// 该方法init相当于C API 中的宏 `module_init`,通过这个方法创建实例
impl kernel::Module for Scull {
fn init(_name: &'static CStr, module: &'static ThisModule) -> Result<Self> {
let count = {
let lock = module.kernel_param_lock();
// 通过内核模块参数传入多个模块的数量
(*nr_devs.read(&lock)).try_into()?
};
pr_info!("Hello world, {} devices!n", count);
let mut devs = Vec::try_with_capacity(count)?;
// 根据传入的数量注册多个设备信息
for i in 0..count {
let dev = Ref::try_new(Device {
number: i,
contents: Mutex::new(Vec::new()),
})?;
let reg = miscdev::Registration::new_pinned(fmt!("scull{i}"), dev)?;
devs.try_push(reg)?;
}
Ok(Self { _devs: devs })
}
}
参考
- 视频:Mentorship Session: Writing Linux Kernel Modules in Rust ,视频作者:Wedson Almeida Filho, Google软件工程师,Rust for Linux 维护者之一 。代码:https://github.com/wedsonaf/linux/commits/lf-session
- 《Linux 设备驱动程序》和 《深入 Linux 设备驱动程序内核机制》
- 在线免费阅读 - Linux 内核模块编程指南- 英文版- 2022 年 7 月 2 日
- https://github.com/Rust-for-Linux/linux/pull/4
- sample: rust: Add a selftest module #819
- [RFC] sample: Rust: Add alloc tests as a sample module