AIGC标识 使用 WSL2 和 QEMU 体验 Rust for Linux:从零写出并运行自己的模块

AIGC 声明:【chip-tip计划】 这是一个使用 AIGC 帮助收集工作生活中经验的计划。本文内容由我实践得出, 请 AI 润色,如有疏漏,敬请提出。

这篇文章的目标很具体:在一台 Windows 电脑上,使用 WSL2 编译一份带 Rust 支持的 Linux
内核,在 QEMU 里启动它,然后让你写的 Rust 模块真的运行起来。

走完主线,你会得到三种相同结果的办法:

  1. 把模块编进内核(CONFIG_SAMPLE_RUST_HELLO=y),启动时自动运行;
  2. 把模块编成可加载文件(hello_rust.ko),在运行中的 guest 里用
    insmod 和 rmmod 管理;
  3. 不修改内核源码,把模块放在树外,用 Kbuild 的 M= 机制编译后加载。

这里的 guest 是 QEMU 里的 Linux,host 是 WSL2。三种方式使用同一份内核和同一套工具链,
区别只在模块被放进内核的时机。

文章按“先得到现象,再解释原因”的顺序写。命令可以整段复制。
每一步都有验收条件;没有看到验收结果,就先不要进入下一步。

先建立一个正确的 R4L 心智模型

Rust for Linux(下文简称 R4L)不是把整个 Linux 内核翻译成 Rust,也不是给 C 代码
套一层语法糖。它解决的是一个更窄、更实际的问题:

把内核中容易靠约定出错的规则,尽量变成 Rust 类型和编译器能够检查的规则,
同时保留 Linux 已经拥有的调度、内存、设备和驱动模型。

三层边界

把一段 Rust 驱动放进 Linux 后,代码大致经过下面三层:

你的驱动 / 模块
        │  使用 Rust 类型、借用、Result 和内核抽象
        ▼
rust/kernel/                 ← 面向驱动的 Rust API
        │  少量、集中、经过论证的 unsafe
        ▼
rust/bindings/ + rust/helpers/ ← bindgen 生成的 C 声明和辅助函数
        │
        ▼
C 内核、头文件和硬件

rust/bindings 是 C 接口的机械映射。它并不会自动变安全。直接调用
bindings::some_c_function(),本质上等于在 Rust 里写了一次没有类型保护的 C
调用,只是还必须放进 unsafe 块。

R4L 的约定因此很重要:

  • 叶子模块(驱动、样例、你的实验代码)优先调用 rust/kernel/ 的抽象;
  • 需要接触 C 指针、寄存器或不变量时,把 unsafe 集中在抽象层;
  • 每个 unsafe 都要写清楚前置条件(通常是 // SAFETY: ...);
  • 驱动作者得到的是“能被类型系统检查的接口”,而不是一堆散落在业务代码里的 C 细节。

可以用锁来理解这种设计。C 代码通常把锁和数据分成两个变量:

static DEFINE_MUTEX(lock);
static struct config cfg;

mutex_lock(&lock);
cfg.timeout = 5;
mutex_unlock(&lock);

“访问 cfg 之前必须先持有 lock”是约定,编译器看不见。

Rust 抽象把数据放进锁里:

let mut guard = config.lock();
guard.timeout = 5;
// guard 离开作用域时自动解锁

没有拿到 guard,就拿不到受保护的数据;guard 的生命周期不能超过锁;离开作用域时
Drop 自动解锁。底层调用 C 锁函数的 unsafe 仍然存在,只是被收敛
在少数经过审查的地方。

这就是 R4L 的核心哲学:不是假装内核没有不安全操作,而是把不安全操作放在可审计的
边界内,让调用者使用更强的类型契约。

一个模块由什么组成

最小的 Rust 内核模块通常包含三块:

use kernel::prelude::*;

module! {
    type: HelloRust,
    name: "hello_rust",
    authors: ["R4L learner"],
    description: "A first Rust module",
    license: "GPL",
}

struct HelloRust;

impl kernel::Module for HelloRust {
    fn init(_module: &'static ThisModule) -> Result<Self> {
        pr_info!("模块初始化成功\n");
        Ok(HelloRust)
    }
}

impl Drop for HelloRust {
    fn drop(&mut self) {
        pr_info!("模块正在卸载\n");
    }
}
  • module! 生成模块元数据、参数和初始化入口;
  • Module::init 是模块被加载时运行的路径,返回 Ok(Self) 才表示初始化
    成功;
  • Drop 是可卸载模块的清理路径;
  • 没有 std,也没有用户态的进程库。你使用的是内核提供的
    kernel crate。

CONFIG_...=y 和 CONFIG_...=m 会改变生命周期:

配置 产物 什么时候运行 能否卸载
y 链进vmlinux / bzImage 内核启动阶段 不能
m 独立的.ko guest 里执行insmod 时 可以,前提是打开卸载支持

0. 你要准备什么

本文按下面的环境写:

  • Windows 10 2004 及以上,或 Windows 11;
  • BIOS/UEFI 已开启硬件虚拟化;
  • WSL2 Ubuntu 22.04;
  • 至少 30 GB 可用磁盘,8 GB 内存,4 个 CPU;
  • 能访问 kernel.org、apt.llvm.org 和 crates.io。

把内核源码放在 WSL 的 Linux 文件系统中,例如 ~/workspace,不要放在
/mnt/c 下。跨 Windows 挂载点编译会明显变慢,还可能遇到权限和大小写问题。

全文会出现三种提示符:

提示符 位置 用途
PS> Windows 管理员 PowerShell 安装 WSL
` 提示符 位置
------- ------------------------- ----------------------------
PS> Windows 管理员 PowerShell 安装 WSL
WSL Ubuntu 编译、制作 rootfs、启动 QEMU
/ # QEMU guest 挂载 9p、加载和卸载模块

命令前的提示符不是命令本身。复制代码块时,只复制提示符后面的内容。

1. 安装 WSL2 和 Ubuntu

在 Windows 中打开“管理员 PowerShell”:

wsl --install -d Ubuntu-22.04
wsl --update

如果系统提示重启,就重启。第一次进入 Ubuntu 时,创建一个 Linux 用户名和密码;这个
密码以后给 sudo 使用。

进入 Ubuntu 后检查磁盘:

df -h /
mkdir -p ~/workspace
cd ~/workspace

/ 的可用空间最好不少于 30 GB。

2. 下载内核树

仍在 WSL 中执行:

git clone --depth=1 \
  https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git \
  ~/workspace/linux

cd ~/workspace/linux
git describe --always --dirty
test -f scripts/min-tool-version.sh

最后一个 test 没有输出且退出码为 0,就说明内核树已经就位。内核是持续开发
的,本文不把某个 commit 当成永久 API;真正的样例和最低版本,以你下载的树为准。

3. 安装工具链

3.1 通用依赖

sudo apt update
sudo apt install -y \
  bc bison flex build-essential \
  libssl-dev libelf-dev libdw-dev libncurses-dev \
  dwarves cpio gzip git curl wget ca-certificates \
  python3 pkg-config qemu-system-x86 busybox-static

3.2 LLVM/Clang

本文选择 LLVM 18,并在所有 Kbuild 命令中明确写 LLVM=-18,避免系统里另一个
旧版 clang 被误选中。

cd ~
wget -q https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh 18
sudo apt install -y clang-18 lld-18 libclang-18-dev

clang-18 --version | head -1
ld.lld-18 --version | head -1

内核树中的最低版本脚本是最终判据:

cd ~/workspace/linux
printf "llvm: "; ./scripts/min-tool-version.sh llvm
printf "rustc: "; ./scripts/min-tool-version.sh rustc
printf "bindgen: "; ./scripts/min-tool-version.sh bindgen

如果未来的内核把 LLVM 最低版本抬到 19,把上面的 18 换成你安装的版本,并
同步修改后文所有 LLVM=-18。

3.3 Rust、rust-src 和 bindgen

使用 rustup,避免 Ubuntu 自带的旧版 Rust 混进来:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
  | sh -s -- -y --default-toolchain stable

. "$HOME/.cargo/env"
rustup component add rust-src rustfmt

# 当前内核树的最低要求是 bindgen 0.71.1;若脚本显示更高版本,按它修改这里。
cargo install --locked bindgen-cli --version 0.71.1

cd ~/workspace/linux
rustup override set stable

rust-src 不是可选项。内核需要交叉编译 Rust 的 core,所以要用到标准库
源码。bindgen 则在构建时读取 C 头文件并生成 Rust 绑定。

检查命令是否来自 rustup:

which -a rustc cargo bindgen
rustc -V
bindgen --version
rustup show active-toolchain

第一行中,$HOME/.cargo/bin 应该排在 /usr/bin 前面。如果 bindgen 报找不到
libclang,先定位动态库:

find /usr/lib -name 'libclang-18.so*' -print

如果输出位于 /usr/lib/x86_64-linux-gnu,执行:

export LIBCLANG_PATH=/usr/lib/x86_64-linux-gnu

3.4 内核自检

cd ~/workspace/linux
make LLVM=-18 rustavailable

必须看到:

Rust is available!

如果没有这行,先处理报错,不要继续配内核。常见原因是当前 shell 没加载 rustup:

. "$HOME/.cargo/env"
export PATH="$HOME/.cargo/bin:$PATH"
make LLVM=-18 rustavailable

4. 配置能运行 Rust 的内核

先生成 x86-64 默认配置,再打开实验所需选项:

cd ~/workspace/linux
make LLVM=-18 x86_64_defconfig

./scripts/config --enable RUST
./scripts/config --enable SAMPLES
./scripts/config --enable SAMPLES_RUST

# initramfs、.ko 和 9p 共享目录
./scripts/config --enable BLK_DEV_INITRD
./scripts/config --enable RD_GZIP
./scripts/config --enable MODULES
./scripts/config --enable MODULE_UNLOAD
./scripts/config --enable NET_9P
./scripts/config --enable NET_9P_VIRTIO
./scripts/config --enable 9P_FS

make LLVM=-18 olddefconfig

scripts/config 是内核树里的脚本,必须在内核树根目录执行。olddefconfig 会重新
解析 Kconfig 依赖;脚本成功返回不代表选项最终保留。

grep -E '^(CONFIG_(RUST|SAMPLES|SAMPLES_RUST|BLK_DEV_INITRD|RD_GZIP|MODULES|MODULE_UNLOAD|NET_9P|NET_9P_VIRTIO|9P_FS))=' .config

至少应包含:

CONFIG_RUST=y
CONFIG_SAMPLES=y
CONFIG_SAMPLES_RUST=y
CONFIG_BLK_DEV_INITRD=y
CONFIG_RD_GZIP=y
CONFIG_MODULES=y
CONFIG_MODULE_UNLOAD=y
CONFIG_NET_9P=y
CONFIG_NET_9P_VIRTIO=y
CONFIG_9P_FS=y

y 表示编进内核,m 表示编成模块。RUST 是 bool 选项,不能设成
m;只有声明为 tristate 的驱动选项才能在 y 与 m 之间切换。

5. 写一个树内 Rust 模块

树内模块放在内核的 samples/rust/,这样它能使用内核已经准备好的
kernel crate 和 Kbuild 规则。

5.1 Rust 源码

创建 samples/rust/hello_rust.rs:

cd ~/workspace/linux
cat > samples/rust/hello_rust.rs <<'RS'
// SPDX-License-Identifier: GPL-2.0

//! My first Rust module.

use kernel::prelude::*;

module! {
    type: HelloRust,
    name: "hello_rust",
    authors: ["R4L learner"],
    description: "A first Rust for Linux module",
    license: "GPL",
    params: {
        times: i64 {
            default: 1,
            description: "How many times to print the greeting",
        },
    },
}

struct HelloRust;

impl kernel::Module for HelloRust {
    fn init(_module: &'static ThisModule) -> Result<Self> {
        pr_info!("进入 Rust 模块,times={}\n", module_parameters::times.value());
        Ok(HelloRust)
    }
}

impl Drop for HelloRust {
    fn drop(&mut self) {
        pr_info!("离开 Rust 模块\n");
    }
}
RS

pr_info! 的字符串里没有手写 hello_rust:。日志前缀由
module! 的 name 生成;手写一次会得到重复前缀。

module! 中的 params 让内核注册参数。内核启动命令行使用
hello_rust.times=3;编成 .ko 后,insmod 使用裸参数
times=3,后文会验证。

5.2 接入 Kconfig 和 Kbuild

内核不会因为目录里出现一个 .rs 文件就自动编译它。下面的脚本会:

  • 在 samples/rust/Kconfig 中声明 tristate;
  • 在 samples/rust/Makefile 中加入目标;
  • 支持重复执行,不会插入重复条目。
cd ~/workspace/linux
python3 - <<'PY'
from pathlib import Path

kconfig = Path("samples/rust/Kconfig")
text = kconfig.read_text()
if "config SAMPLE_RUST_HELLO" not in text:
    lines = text.splitlines(keepends=True)
    position = next(
        i for i in range(len(lines) - 1, -1, -1)
        if lines[i].lstrip().startswith("endif")
    )
    block = """config SAMPLE_RUST_HELLO
	tristate "My first Rust module"
	help
	  This option builds my first Rust module.

	  To compile this as a module, choose M here:
	  the module will be called hello_rust.

	  If unsure, say N.

"""
    lines.insert(position, block)
    kconfig.write_text("".join(lines))

makefile = Path("samples/rust/Makefile")
line = "obj-$(CONFIG_SAMPLE_RUST_HELLO)\t+= hello_rust.o\n"
text = makefile.read_text()
if "CONFIG_SAMPLE_RUST_HELLO" not in text:
    makefile.write_text(text + line)
PY

这段脚本把 Kconfig 项插入最后一个 endif 之前,也就是
if SAMPLES_RUST 块内。Makefile 里的 Tab 已由 Python 写入,不要手动换成空格。

6. 方式一:编进内核(=y)

6.1 选择 built-in 并编译

cd ~/workspace/linux
./scripts/config --enable SAMPLE_RUST_HELLO
make LLVM=-18 olddefconfig

grep '^CONFIG_SAMPLE_RUST_HELLO=' .config
# 应该是 CONFIG_SAMPLE_RUST_HELLO=y

make LLVM=-18 -j"$(nproc)"
test -x arch/x86/boot/bzImage
ls -lh arch/x86/boot/bzImage vmlinux

第一次编译需要几分钟,之后只改一个 Rust 文件通常是增量编译。=y 模块在内核
启动阶段自动执行,不能在 guest 中卸载。

6.2 制作最小 initramfs

没有 rootfs 时,模块可能已经运行,但内核最后会因找不到 /init 而 panic。
为了让三种方式都能进入 shell,制作一个只有 BusyBox 的 initramfs:

ROOTFS="$HOME/r4l-initramfs"
rm -rf "$ROOTFS"
mkdir -p "$ROOTFS"/{bin,dev,proc,sys,tmp}

cp /usr/bin/busybox "$ROOTFS/bin/busybox"
for applet in sh ls cat echo mount umount dmesg uname mkdir \
              poweroff insmod rmmod lsmod; do
  ln -sf busybox "$ROOTFS/bin/$applet"
done

cat > "$ROOTFS/init" <<'INIT'
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs none /dev 2>/dev/null
echo "R4L initramfs is ready"
exec /bin/sh
INIT

chmod 0755 "$ROOTFS/init"

cd "$ROOTFS"
find . -print0 \
  | cpio --null -o --format=newc 2>/dev/null \
  | gzip -9 > "$HOME/r4l-initramfs.cpio.gz"

ls -lh "$HOME/r4l-initramfs.cpio.gz"

newc 格式和可执行的 0755 权限不能省。QEMU 会把归档交给内核,
内核解开后执行 /init。

6.3 启动 QEMU

cd ~/workspace/linux
qemu-system-x86_64 \
  -kernel arch/x86/boot/bzImage \
  -initrd "$HOME/r4l-initramfs.cpio.gz" \
  -append "console=ttyS0 hello_rust.times=3" \
  -nographic -no-reboot -m 1024 -smp 2

你应该看到类似:

hello_rust: 进入 Rust 模块,times=3
R4L initramfs is ready
/ #

时间戳和其他内核日志会不同。看到 hello_rust: 这一行,方式一就成功了。

在 guest 中输入 poweroff -f 关机。若 guest 没响应,按
Ctrl-A,再按 X 退出 QEMU。

7. 方式二:编成 .ko,在运行中的 guest 里加载

这种方式适合快速迭代:只重编 .ko,然后在同一个 guest 中
rmmod / insmod,不必重启。

7.1 改为模块

cd ~/workspace/linux
./scripts/config --module SAMPLE_RUST_HELLO
make LLVM=-18 olddefconfig

grep '^CONFIG_SAMPLE_RUST_HELLO=' .config
# 应该是 CONFIG_SAMPLE_RUST_HELLO=m

make LLVM=-18 -j"$(nproc)"
ls -lh samples/rust/hello_rust.ko

这次启动日志里不会自动出现 hello_rust,因为它需要 insmod。

7.2 用 9p 把 .ko 共享给 QEMU

cd ~/workspace/linux
qemu-system-x86_64 \
  -kernel arch/x86/boot/bzImage \
  -initrd "$HOME/r4l-initramfs.cpio.gz" \
  -virtfs "local,path=$HOME/workspace/linux/samples/rust,mount_tag=host0,security_model=none" \
  -append "console=ttyS0" \
  -nographic -no-reboot -m 1024 -smp 2

进入 / # 后执行:

mkdir -p /mnt/host
mount -t 9p -o trans=virtio,version=9p2000.L host0 /mnt/host
insmod /mnt/host/hello_rust.ko times=7
lsmod
rmmod hello_rust

应看到:

hello_rust: 进入 Rust 模块,times=7
hello_rust: 离开 Rust 模块

对 insmod 来说,参数必须写成 times=7。hello_rust.times=7 是
内核启动命令行的写法,不是 insmod 的写法。

7.3 体验增量迭代

保持 QEMU 运行,在另一个 WSL 终端改源码并重编:

cd ~/workspace/linux
sed -i 's/进入 Rust 模块/再次进入 Rust 模块/' samples/rust/hello_rust.rs
make LLVM=-18 -j"$(nproc)"

回到 guest:

rmmod hello_rust
insmod /mnt/host/hello_rust.ko times=11

9p 共享的是目录,新生成的 .ko 会立即可见。若你修改了内核配置、模块依赖或
内核本身,仍然需要重启 QEMU。

8. 方式三:树外模块(不修改内核树)

树内方式适合学习 API 和准备上游补丁。自己的项目也可以放在内核树外:内核树一行不改,
单独目录里放一个 Rust 文件和一个 Kbuild Makefile。

8.1 创建两个文件

mkdir -p "$HOME/r4l-external"
cd "$HOME/r4l-external"

cat > Makefile <<'MAKE'
obj-m := r4l_external.o
MAKE

cat > r4l_external.rs <<'RS'
// SPDX-License-Identifier: GPL-2.0

//! Out-of-tree Rust module.

use kernel::prelude::*;

module! {
    type: R4lExternal,
    name: "r4l_external",
    authors: ["R4L learner"],
    description: "An out-of-tree Rust module",
    license: "GPL",
}

struct R4lExternal;

impl kernel::Module for R4lExternal {
    fn init(_module: &'static ThisModule) -> Result<Self> {
        pr_info!("树外 Rust 模块已加载\n");
        Ok(R4lExternal)
    }
}

impl Drop for R4lExternal {
    fn drop(&mut self) {
        pr_info!("树外 Rust 模块已卸载\n");
    }
}
RS

obj-m := r4l_external.o 的左边决定最终模块身份,因此它必须和
r4l_external.rs 同名(只把 .rs 换成 .o)。树外模块不需要
Kconfig:执行 make 就表示“我要编译它”。

8.2 使用内核 Kbuild 编译

树外目录不在内核树的子目录里,因此单独设置 rustup override:

cd "$HOME/r4l-external"
rustup override set stable

make -C "$HOME/workspace/linux" M="$PWD" LLVM=-18
ls -lh r4l_external.ko

这条命令的含义:

  • -C:进入已经配置和准备好的内核树;
  • M=$PWD:告诉 Kbuild 这个目录包含外部模块;
  • LLVM=-18:保证模块和内核使用相同的 LLVM 工具链。

不要直接运行 rustc r4l_external.rs。模块还需要内核头文件、内核生成的
kernel crate、编译选项、符号版本和 vermagic,只有 Kbuild 能把这些组合起来。

8.3 在 QEMU 中加载树外模块

关掉方式二的 QEMU,再用同一份内核和 initramfs 启动,换共享目录:

cd ~/workspace/linux
qemu-system-x86_64 \
  -kernel arch/x86/boot/bzImage \
  -initrd "$HOME/r4l-initramfs.cpio.gz" \
  -virtfs "local,path=$HOME/r4l-external,mount_tag=host0,security_model=none" \
  -append "console=ttyS0" \
  -nographic -no-reboot -m 1024 -smp 2

guest 中:

mkdir -p /mnt/host
mount -t 9p -o trans=virtio,version=9p2000.L host0 /mnt/host
insmod /mnt/host/r4l_external.ko
rmmod r4l_external

应看到:

r4l_external: 树外 Rust 模块已加载
r4l_external: 树外 Rust 模块已卸载

三种方式的差异可以这样记:

方式 源码位置 配置/构建 运行方式
树内 built-in samples/rust/ Kconfig 设y 启动时自动运行
树内模块 samples/rust/ Kconfig 设m guest 中insmod
树外模块 独立目录 make -C ... M=... guest 中insmod

9. 为什么这些步骤体现了 R4L 哲学

9.1 不要在驱动里扩散 bindings

你可以写:

unsafe {
    bindings::some_c_api(...);
}

但你还需要自己证明指针有效、对象已初始化、当前上下文可以睡眠、生命周期正确、并发
访问受保护。Rust 编译器无法从一条原始 C 声明推导这些规则。

R4L 的做法是把这些证明放进抽象层,模块只使用
Mutex<T>、设备、文件、DMA 等带契约的类型。若抽象层没有覆盖某个能力,正确
方向是先理解不变量,再给抽象层补 API,而不是在每个驱动中复制 C 绑定。

9.2 为什么 init 返回对象

Module::init 返回 Result<Self>。成功时这个 Self 会由内核持有;
初始化失败可以返回 Err;成功后的资源清理由 Drop 负责。

这不是把 C 的函数名换成 Rust 的函数名,而是把资源所有权放进对象生命周期。Rust 仍然
不会替你修复错误的硬件协议或错误的并发设计,但它会拒绝许多悬垂引用、重复释放和
跨线程移动不允许移动的值。

9.3 推荐阅读顺序

cd ~/workspace/linux
less samples/rust/rust_minimal.rs
less rust/kernel/sync/lock.rs
less Documentation/rust/quick-start.rst
less Documentation/kbuild/modules.rst

先看样例怎样声明模块,再看一个小型抽象如何把 C 不变量封装起来,最后进入设备驱动、
异步上下文、DMA 和锁的组合。

网络版的上游入口是 Rust Quick Start 和
Building External Modules;版本不一致时,
以你手上内核树的 Documentation/ 和 samples/rust/ 为准。

10. 排错表

工具链

  • make rustavailable 没有 Rust is available!:执行
    . "$HOME/.cargo/env",确认 which rustc 指向
    $HOME/.cargo/bin/rustc。
  • 内核编译成功但 CONFIG_RUST 没有:编译成功不代表 Rust 被启用;先修复
    rustavailable,再重新执行配置。
  • bindgen 报找不到 libclang:安装 libclang-18-dev,将
    LIBCLANG_PATH 设置为实际包含 libclang-18.so 的目录。
  • 树外报 unknown codegen option: jump-tables:在树外目录执行
    rustup override set stable,比较内核树和模块目录中的 rustc -V。

Kconfig 和源码

  • SAMPLE_RUST_HELLO 在 olddefconfig 后消失:确认
    SAMPLES=y、SAMPLES_RUST=y,并且 Kconfig 项位于
    if SAMPLES_RUST 块内。
  • ./scripts/config: No such file:你不在内核树根目录。
  • No rule to make target:检查 obj-m/obj-y 的目标名是否与
    .rs 文件名一致。
  • 改源码但日志没变:=y 要重编内核,=m 要重编
    .ko,然后在 guest 中重新加载。

QEMU 和模块

  • QEMU 黑屏:x86 使用 console=ttyS0,并保留 -nographic。
  • 反复重启:加 -no-reboot。
  • VFS: Unable to mount root fs:没有传入 initramfs,或归档中的
    /init 不存在、不可执行、格式不是 newc。
  • mount -t 9p 失败:确认 NET_9P、
    NET_9P_VIRTIO、9P_FS 都是 y,且
    host0 与 mount_tag=host0 完全一致。
  • insmod: Invalid module format:模块必须使用正在运行的同一份
    .config、同一棵内核树和同一套工具链编译。
  • rmmod 找不到模块:使用 Kbuild 目标名,例如
    r4l_external,不要只看 module! 的显示名。
  • 参数没有生效:内核启动命令行使用 hello_rust.times=3;
    insmod 使用 times=3。

11. 接下来怎么学

你已经完成了完整闭环:

写 Rust
  → module! 注册生命周期和参数
  → Kconfig/Kbuild 决定构建形态
  → LLVM + rustc + bindgen 生成内核产物
  → QEMU 启动同一份 bzImage
  → 串口日志证明模块执行

下一步可以:

  1. 把 HelloRust 的状态放进 Mutex<T>,练习 guard 和生命周期;
  2. 追踪 rust/kernel/ 中一个抽象,找到它内部唯一的 unsafe 调用;
  3. 把树外模块改成 misc device,再逐步加入错误处理、并发和资源释放。

每次写 Rust 内核代码,都先问:“这个不变量在哪里被表达和检查?”如果答案只是“我在
注释里记得了”,通常说明抽象边界还没有设计好。

posted @ 2026-09-27 00:31  winddevil  阅读(3)  评论(0)    收藏  举报