摘要
StarryOS 作为基于 Rust 的宏内核操作系统,已在前序工作中引入了可加载内核模块(LKM)基础设施,但彼时仅支持显式手动加载。本文在此基础之上,设计并实现了 ondemand-kmod——一个通用的 #![no_std] 按需加载内核模块框架,支持懒加载(lazy loading)与空闲超时自动卸载(idle unloading)。该框架将”何时加载”的策略与”怎么加载”的机制彻底解耦,形成一个可复用的独立库。
随后,该框架深度集成进 StarryOS,先后实现了 procfs 与 FUSE 两个真实模块的按需加载。在 FUSE 方向,本文从零手写了内核侧驱动 Starryfuse,包含基础 FUSE 协议解析、字符设备通信、VFS 桥接与阻塞读/poll 多路复用支持;将其包装为 fuse.ko 可加载模块,并开发了三组用户态测试程序,在 QEMU/RISC-V 环境下验证了从首次 open("/dev/fuse") 触发加载到空闲卸载、内存回收的完整生命周期。
关键词:StarryOS;按需加载内核模块;FUSE;用户态文件系统;Rust;RISC-V
1. 引言
1.1 背景
StarryOS 是一个基于 Rust 的宏内核操作系统,底层依托 ArceOS 的模块化架构。之前有同学为 StarryOS 引入了完整的 LKM 机制:内核能够在运行期动态加载 ELF 格式的 .ko 文件,完成重定位、符号解析并执行模块的 init/exit 函数。
然而,当时的 LKM 仅支持显式手动加载(insmod 语义)。对于 procfs、FUSE 这类并非始终活跃的子系统,如果将其静态编译进内核,会造成启动时内存与体积的浪费;如果完全手动管理,又增加了系统管理员的负担。因此,有必要在 LKM 基础之上构建一套按需加载框架,使内核模块能够在首次被访问时自动装载,在空闲超时时自动卸载并回收资源。
1.2 设计目标
围绕上述背景,本文工作围绕以下核心目标展开:
- 按需加载:当用户态首次访问某功能(如打开
/dev/fuse或/proc/meminfo)且 VFS 返回NotFound时,系统自动加载对应模块并重试。 - 自动卸载:模块在空闲(无引用、无打开文件描述符)超过设定时间后,自动卸载并释放物理内存。
- 策略与机制解耦:将”何时加载”的策略与”怎么加载”的机制分离,形成可复用的独立框架。
- FUSE 完整闭环:实现内核侧 FUSE 驱动,支持标准 Linux FUSE ABI,使用户态守护进程能在 StarryOS 中挂载并操作虚拟文件系统。
1.3 报告结构
本文剩余章节安排如下:第 2 节介绍相关工作与背景;第 3 节阐述 ondemand-kmod 框架与 FUSE 按需加载的系统架构;第 4 节详细描述实现细节;第 5 节介绍测试验证方案与结果;第 6 节总结全文并展望未来工作。
2. 相关工作与背景
2.1 StarryOS 的 LKM 基础设施
前序工作为 StarryOS 引入了完整的 LKM 支持,包含 ELF 解析器、符号重定位器(ksym)以及模块加载器(kmod-loader)。内核能够在运行期读取 .ko 文件,将其映射到独立的虚拟地址空间,解析未定义符号并回填地址,最终调用模块入口函数。这一基础设施为按需加载提供了”怎么加载”的底层能力,但缺乏”何时加载”的自动化策略层。
2.2 已有按需加载实践
AlloyStack:面向Serverless按需加载的库操作系统。该项目作为libos实现了模块按需自动加载,服务于云函数运行。
本项目开发初期,对于StarryOS 的 procfs 文件系统也尝试过”懒挂载”(lazy mount)——在启动时注册挂载点工厂函数,首次访问时才真正挂载文件系统。后期因FUSE测试需要,将procfs重归静态加载。
2.3 StarryOS 面临的独特挑战
与 Linux 相比,StarryOS 的按需加载面临若干独特挑战:
- Rust
#![no_std]环境:无法使用std::sync::Mutex、std::thread等标准库设施,所有并发控制必须基于自旋锁和内核原语。 - 跨模块符号解析:模块与主内核之间的符号绑定通过
KALLSYMS字典完成,需要处理 Rust Nightly 裁剪导致的符号缺失问题。 - RISC-V QEMU 调试环境:物理内存固定,模块加载/卸载的内存行为需要可观测、可量化,以验证按需加载的实际收益。
- VFS 上下文安全:动态加载的文件系统模块不能在中断上下文或持有全局锁时执行可能阻塞的操作。
- 缺乏现有 FUSE 内核态驱动 Rust 实现借鉴:现有 FUSE Rust 实现开源项目仓库均为用户态侧设计,为Linux系统适配。
3. 系统架构与设计
3.1 整体架构
StarryOS 的按需加载系统组成部分:
ondemand-kmod:独立的#![no_std]Rust 库,提供通用的模块生命周期管理。api/src/kmod/ondemand.rs:StarryOS 内核集成层,桥接框架与现有 LKM 基础设施。api/src/kmod/ondemand_builtin.rs: 负责具体模块注册加载卸载触发逻辑。
当用户态程序访问某个路径(如 /dev/fuse)时,VFS 层通过 with_ondemand() 钩子捕获 NotFound 错误,触发 ondemand-kmod 加载对应的 .ko 文件;模块初始化完成后,VFS 操作自动重试。空闲时,后台监控任务通过三阶段卸载算法安全回收模块内存。
图 1 展示了按需加载系统的整体架构。图中上侧为用户态进程,下侧为内核态组件。用户态的首次访问沿 VFS 路径向下传播,若目标不存在则进入 with_ondemand 重试路径;框架层的 registry 与 lifecycle 负责状态转换;底层通过 kmod-loader 实际完成 ELF 加载。
如果图片显示失败,图片链接:https://github.com/DINGBROK423/ondemand-kmod/blob/main/doc/report_figures/p1.png
3.2 模块生命周期状态机
ondemand-kmod 使用六状态有限状态机(FSM)描述模块的生命周期:
Unloaded:初始状态,模块尚未加载。Loading:正在执行 ELF 加载与符号解析。Loaded:模块已成功初始化,可供使用。Active:模块正被使用(存在打开的文件描述符或挂载点引用)。Idle:模块已加载但当前无活跃引用。Unloading:正在执行模块退出函数并回收内存。
状态转换遵循以下规则:
Unloaded --(触发器/首次访问)--> LoadingLoading --(成功)--> Loaded --> ActiveActive --(最后一个引用释放)--> IdleIdle --(超时 / 空闲时间达到阈值)--> UnloadingIdle --(新的访问请求)--> ActiveUnloading --(完成)--> Unloaded
图 2 为六状态生命周期状态机示意图。
如果图片显示失败,图片链接:https://github.com/DINGBROK423/ondemand-kmod/blob/main/doc/report_figures/p2.png
3.3 安全卸载的三阶段算法
自动卸载是按需加载框架中最容易出错的环节,因为模块可能仍被内核数据结构间接引用。ondemand-kmod 采用三阶段卸载算法保证安全:
- 标记阶段(Mark):将模块状态从
Idle迁移到Unloading,禁止新的引用获取。 - 等待阶段(Quiesce):等待所有已存在的引用释放。对于
procfs,这意味着等待所有打开的/proc/xxx文件关闭;对于 FUSE,这意味着等待所有/dev/fuse文件描述符关闭以及 VFS 挂载点解除。 - 回收阶段(Teardown):调用模块的
exit函数,解除符号绑定,释放 ELF 占用的物理页,最终将状态迁移回Unloaded。
3.4 VFS 层触发重试机制
为了避免在每个系统调用路径中手动插入加载逻辑,StarryOS 在 VFS 层引入了一个通用函数 with_ondemand()。当 VFS 操作(如 lookup、open、read)返回 NotFound 时,该函数会:
- 检查失败路径是否匹配某个已注册的按需加载触发器(如
/proc/*对应procfs.ko,/dev/fuse对应fuse.ko)。 - 若匹配,则调用
ondemand-kmod::try_load()尝试加载。 - 加载成功后,自动重试原始的 VFS 操作。
- 若加载失败或重试后仍返回错误,则将错误返回给用户态。
这种设计的关键优势在于:触发逻辑对上层完全透明,无论是用户态程序、libc 还是 Shell,都不需要任何修改。
3.5 Starryfuse 内核驱动架构
Starryfuse 是 StarryOS 中 FUSE 的内核侧实现,被包装为 fuse.ko 可加载模块。其内部采用四层架构:
abi层:FUSE 协议数据结构的 Rust 定义,严格对齐 Linux FUSE ABI(如fuse_in_header、fuse_out_header、fuse_init_in、fuse_init_out)。dev层:字符设备/dev/fuse的实现,负责内核与用户态守护进程之间的字节流传输。包含FuseDev结构体、PollSet多路复用、以及WaitQueue阻塞/唤醒机制。vfs层:VFS 桥接层,将 StarryOS 的axfs_vfs操作(lookup、read、write、readdir等)翻译为 FUSE 请求,通过dev层发送给用户态守护进程,再将其响应翻译回 VFS 语义。lib层:starry_fuser用户态库,封装了与Starryfuse内核驱动的交互细节,使开发者能够像使用libfuse一样编写用户态文件系统。
图 3 展示了 FUSE 按需加载执行的完整时序。
如果图片显示失败,图片链接:https://github.com/DINGBROK423/ondemand-kmod/blob/main/doc/report_figures/p3.png
图 4 展示了 Starryfuse 内核驱动的分层架构。
如果图片显示失败,图片链接:https://github.com/DINGBROK423/ondemand-kmod/blob/main/doc/report_figures/p4.png
4. 实现细节
4.1 ondemand-kmod 框架实现
ondemand-kmod 被设计为一个独立的 #![no_std] Rust crate,核心文件包括:
registry.rs:维护一个全局的模块注册表,记录每个模块的名称、触发器(路径前缀或设备号)、.ko文件路径、超时阈值以及当前状态。lifecycle.rs:定义六状态 FSM 的State枚举、ModuleDesc模块描述符、ModuleGuardRAII 引用计数守卫以及ManagedModule运行时 bookkeeping 结构。状态转换的实际逻辑由registry.rs(on_access处理加载触发与状态迁移动作)和monitor.rs(tick中完成Active→Idle迁移及卸载决策)驱动。monitor.rs:实现IdleMonitor::tick()三阶段卸载算法。Phase 1 持锁扫描注册表,将引用计数为零且空闲超时的模块从Idle标记为Unloading;Phase 2 在无锁环境下调用ModuleLoader::unload()执行实际卸载;Phase 3 再次持锁将成功卸载的模块状态回写为Unloaded。该函数由api/src/kmod/ondemand.rs的tick_ondemand()定期调用。
框架通过 ModuleLoader trait 与具体的操作系统解耦,定义了 load()、unload() 等方法。StarryOS 在 api/src/kmod/ondemand.rs 中提供 KmodOnDemandLoader 结构体作为该 trait 的具体实现,其内部调用现有的 kmod-loader 与 axalloc 内存管理接口。
4.2 with_ondemand VFS 集成
with_ondemand 是一个泛型函数,其实现位于 api/src/kmod/ondemand.rs。以 lookup 为例:
1 | with_ondemand(&path, || fs.resolve(&path)) |
函数调用逻辑:
- 首次调用闭包
vfs.lookup(path)。 - 若返回
Err(NotFound),提取路径中的前缀,查询registry是否有匹配。 - 若有匹配,调用
try_load();加载成功后继续下一次循环(重试)。 - 若返回其他错误或连续重试次数超过上限,则直接返回错误。
该函数被包裹在 open、stat、chmod、chown 等关键系统调用路径中,确保几乎所有文件系统操作都能触发按需加载。
4.3 FuseDev 的并发与同步重构
早期的 /dev/fuse 实现使用单线程自旋锁保护整个设备状态,导致当守护进程阻塞在 read 等待请求时,其他线程无法并发写入新请求。为此,本文对 FuseDev 进行了并发重构:
- 引入
PollSet:支持多线程同时poll,read通过WaitQueue串行服务,内核可以在任意线程上向PollSet投递可读/可写事件。 - 引入
WaitQueue:当没有待处理请求时,read调用将当前任务挂起到WaitQueue;当有新的 VFS 请求到达时,由vfs层唤醒等待队列中的任务。 - 锁安全:调用文件系统函数时先短暂拿锁检查请求队列,无数据则立即释放锁,再通过外部 WaitQueue 安全阻塞睡眠,消除”持自旋锁睡眠”导致的死锁风险。
4.4 vfs.rs 的协议桥接
vfs.rs 是 Starryfuse 中最复杂的模块,负责将 StarryOS VFS 的语义映射到 FUSE 协议。
Opcode 映射:vfs.rs 中所有 FUSE 请求的 opcode 均严格对照 Linux 内核头文件定义,确保与用户态守护进程的协议语义一致。例如 FUSE_INIT 使用 opcode 26,与标准 FUSE ABI 对齐。
INIT 协议握手:FuseFs::new() 注册文件系统后,在独立内核线程中异步发起 FUSE_INIT 握手,避免阻塞 sys_mount。构造 FuseInitIn 请求体携带主版本号、次版本号与 max_readahead 等能力字段下发至用户态守护进程,解析返回的 FuseInitOut 完成协议版本确认。
4.5 用户态 starry_fuser 库
对接外部fuse用户侧驱动,处于开发中。
5. 测试与验证
5.1 测试内容
为验证按需加载与 FUSE 功能的正确性,本文设计了三组测试程序:
fuse_test:基础功能测试,验证/dev/fuse的按需加载、FUSE_INIT 握手、简单的lookup/read/readdir以及空闲卸载。fuse_rw_test:读写功能测试,验证write+read闭环、文件截断覆盖、目录创建与遍历。fuse_mem_test:内存与稳定性测试,通过单次加载/卸载 FUSE 模块并读取内核内存快照,观测内存占用是否回归基线,检测是否存在内存泄漏。
5.2 测试环境
- 目标平台:
riscv64gc-unknown-none-elf - 运行环境:QEMU 7.2+
virt机器 - 内核配置:开启
KALLSYMS、LKM、ONDEMAND_KMOD、FUSE - 测试方式:在 QEMU 中运行 StarryOS,启动测试程序,串口输出日志
5.3 测试结果
5.3.1 fuse_test
注:空闲卸载触发时间:5 s
测试日志:
1 | [318.750672 0:11 kmod_loader::loader:354] Module(Some("fuse")) loaded successfully! |
5.3.2 fuse_rw_test
测试日志:
1 | [ 15.872268 0:11 kmod_loader::loader:354] Module(Some("fuse")) loaded successfully! |
5.3.3 fuse_mem_test
测试日志:
1 | [ 8.163909 0:11 kmod_loader::loader:354] Module(Some("fuse")) loaded successfully! |
对日志中的内存数据整理如 表 1 与 表 2 所示。
表 1:内存快照原始数据
| Phase | RustHeap (B) | Δ RustHeap | Pages | Δ Pages |
|---|---|---|---|---|
| Before load | 8,315,080 | — | 4,762 | — |
| After load | 8,862,712 | +547,632 | 4,779 | +17 |
| Before unload | 8,352,536 | +37,456 | 4,788 | +26 |
| After unload | 8,351,108 | +36,028 | 4,771 | −17 |
表 2:内存占用构成分析
| Configuration | Size (KB) | Pages | Contribution |
|---|---|---|---|
| A. Static baseline (fuse.ko + starryfuse resident) | 1,071 | — | baseline |
| ‑ fuse.ko | 416 | — | — |
| ‑ starryfuse libs | 655 | — | — |
| B. On-demand mapped pages (loader vmalloc) | 68 | 17 | actual load |
| D. Runtime overhead (mount/fork/VFS, transient) | 36 | 9 | transient |
| Memory saving vs static baseline | 1,071 | — | resident reduction |
从 表 1 可见,按需加载在 After load 阶段使内核页数增加了 17 页(约 68 KB),这是 kmod-loader 通过 vmalloc 映射 .ko 产生的实际内存开销。经过 FUSE 自测试验、卸载挂载点并等待 7 s 空闲超时后,模块进入 Unloading 状态,KmodMem::drop 逐页释放物理内存,最终 After unload 页数相比 After load 回落 17 页,证明模块占用的 ELF 内存被完全回收。
测试前后页数从 4,762 增至 4,771(+9 页,约 36 KB),这部分增量属于 mount/fork/VFS 等运行时 transient 开销,并非模块泄漏。
表 2 进一步量化了按需加载的收益:若将 fuse.ko(416 KB)与 starryfuse 依赖库(655 KB)静态编译进内核,常驻内存开销约为 1,071 KB;而按需加载模式下,FUSE 模块卸载后常驻 footprint 降至约 0 KB,实际节省内核常驻内存 1,071 KB。
6. 结论与未来工作
6.1 工作总结
本文设计并实现了 ondemand-kmod——一个面向 #![no_std] 环境的通用按需加载内核模块框架,并将其成功集成到 StarryOS 中。在此基础上,本文完成了 procfs 与 FUSE 的按需加载闭环。对于 FUSE,本文从零实现了内核侧驱动 Starryfuse,涵盖协议解析、字符设备通信、VFS 桥接与用户态库,支持了完整的 FUSE 文件系统生命周期。
6.2 未来工作
- 块设备文件系统按需加载:当前框架主要面向用户态文件系统与伪文件系统。未来可将其扩展至
ext4、fat32等块设备文件系统。 - 完善
starry_fuser功能集:补充FUSE_MKNOD、FUSE_IOCTL等高级操作码,提升与现有libfuse的兼容性。 - vDSO 与系统调用优化:探索将部分 FUSE 请求路径通过 vDSO 优化,减少用户态/内核态切换次数。