11. Linux驱动 - Rust编写Linux驱动

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

Rust编写Linux驱动

转载自

  • https://rustcc.cn/article?id=3a9ea964-6692-4d6f-9a11-8c4abf35856f
  • https://mp.weixin.qq.com/s/m2eZ0lEzQHjrNVC6YCC_IA

背景| 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

在编写模块之前需要知道模块的生命周期

  1. 从内核模块被加载以后会进行初始化。

  2. 接下来会在子系统Subsystem诸如进程调度、内存管理、虚拟文件系统、网络接口、进程间通信上进行注册。

  3. 随后可能一些系统角色(Actor)比如来自用户空间的用户可能要做一些动作比如内存读取。会触发子系统的一些操作。子系统知道谁来处理。如果是内核模块就会通过一个回调来告诉模块做它该做的事情最终返回给 Actor。这一步实际可能会发生很多次。

  4. 模块被卸载的时候会通过特定退出机制让模块从子系统中注销然后返回。

以上就是模块的整个生命周期也可作为我们编写内核模块的一个宏观的心智模型。

从零编写一个字符驱动

Linux 中设备通常被分为三类每个驱动模块通常实现为这三类中的其中一种

  • 字符设备。通常是指可以当作一个字节流来存取的设备比如文件。

  • 块设备。通常是可以驻有文件系统的设备比如磁盘和字符设备类似但块设备有一个请求缓冲区因此它们可以选择响应请求的最佳顺序。

  • 网络设备。通常是指能与其他主机交换数据的设备。

我们以编写一个简单的字符设备驱动为例展示如何用 Rust 来编写内核驱动。

R4L 开发环境准备

为了方便我们把 Rust for Linux 简称为 R4L。

首先下载 Rust for Linux。

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 驱动

ScullSimple 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 状态为

`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`来编写代码。

```c  
// 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 模块

/*   
 * 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 模块增加一个简单的文件操作功能。

// 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 opened\n");  
        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_MAJOR10所有的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 opened\n");  
        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: 增加文件读写

// 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 opened\n");  
        Ok(())  
    }  
      
    // 新增文件读  
    // 可从 `file::Operations` trait 文档中直接查看该函数签名  
    fn read(  
        _data: (),  
        _file: &file::File,  
        _writer: &mut impl IoBufferWriter,  
        _offset: u64,  
    ) -> Result<usize> {  
        pr_info!("File was read\n");  
        Ok(0)  
    }  
  
    // 新增文件写  
    // 可从 `file::Operations` trait 文档中直接查看该函数签名  
    fn write(  
        _data: (),  
        _file: &file::File,  
        reader: &mut impl IoBufferReader,  
        _offset: u64,  
    ) -> Result<usize> {  
        pr_info!("File was written\n");  
        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 被调用时我们可以更新它。

// 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 opened\n", 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 read\n", 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 written\n", 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进行读写那里将会产生数据竞争。所以需要加锁。

// 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 opened\n", 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 read\n", 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 written\n", 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 时返回保存的数据

// 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 opened\n", 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 read\n", 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 written\n", 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: 优化缓冲数据

// 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 opened\n", 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 read\n", 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 written\n", 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: 定义内核模块参数

为了支持多个设备需要让模块支持外部参数。

// 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 opened\n", 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 read\n", 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 written\n", 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: 支持多个设备

// 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 opened\n", 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 read\n", 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 written\n", 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

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: linux