0%

摘要

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 语义)。对于 procfsFUSE 这类并非始终活跃的子系统,如果将其静态编译进内核,会造成启动时内存与体积的浪费;如果完全手动管理,又增加了系统管理员的负担。因此,有必要在 LKM 基础之上构建一套按需加载框架,使内核模块能够在首次被访问时自动装载,在空闲超时时自动卸载并回收资源。

1.2 设计目标

围绕上述背景,本文工作围绕以下核心目标展开:

  1. 按需加载:当用户态首次访问某功能(如打开 /dev/fuse/proc/meminfo)且 VFS 返回 NotFound 时,系统自动加载对应模块并重试。
  2. 自动卸载:模块在空闲(无引用、无打开文件描述符)超过设定时间后,自动卸载并释放物理内存。
  3. 策略与机制解耦:将”何时加载”的策略与”怎么加载”的机制分离,形成可复用的独立框架。
  4. 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::Mutexstd::thread 等标准库设施,所有并发控制必须基于自旋锁和内核原语。
  • 跨模块符号解析:模块与主内核之间的符号绑定通过 KALLSYMS 字典完成,需要处理 Rust Nightly 裁剪导致的符号缺失问题。
  • RISC-V QEMU 调试环境:物理内存固定,模块加载/卸载的内存行为需要可观测、可量化,以验证按需加载的实际收益。
  • VFS 上下文安全:动态加载的文件系统模块不能在中断上下文或持有全局锁时执行可能阻塞的操作。
  • 缺乏现有 FUSE 内核态驱动 Rust 实现借鉴:现有 FUSE Rust 实现开源项目仓库均为用户态侧设计,为Linux系统适配。

3. 系统架构与设计

3.1 整体架构

StarryOS 的按需加载系统组成部分:

  1. ondemand-kmod:独立的 #![no_std] Rust 库,提供通用的模块生命周期管理。
  2. api/src/kmod/ondemand.rs:StarryOS 内核集成层,桥接框架与现有 LKM 基础设施。
  3. api/src/kmod/ondemand_builtin.rs: 负责具体模块注册加载卸载触发逻辑。

当用户态程序访问某个路径(如 /dev/fuse)时,VFS 层通过 with_ondemand() 钩子捕获 NotFound 错误,触发 ondemand-kmod 加载对应的 .ko 文件;模块初始化完成后,VFS 操作自动重试。空闲时,后台监控任务通过三阶段卸载算法安全回收模块内存。

图 1 展示了按需加载系统的整体架构。图中上侧为用户态进程,下侧为内核态组件。用户态的首次访问沿 VFS 路径向下传播,若目标不存在则进入 with_ondemand 重试路径;框架层的 registrylifecycle 负责状态转换;底层通过 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 --(触发器/首次访问)--> Loading
  • Loading --(成功)--> Loaded --> Active
  • Active --(最后一个引用释放)--> Idle
  • Idle --(超时 / 空闲时间达到阈值)--> Unloading
  • Idle --(新的访问请求)--> Active
  • Unloading --(完成)--> Unloaded

图 2 为六状态生命周期状态机示意图。

如果图片显示失败,图片链接:https://github.com/DINGBROK423/ondemand-kmod/blob/main/doc/report_figures/p2.png

3.3 安全卸载的三阶段算法

自动卸载是按需加载框架中最容易出错的环节,因为模块可能仍被内核数据结构间接引用。ondemand-kmod 采用三阶段卸载算法保证安全:

  1. 标记阶段(Mark):将模块状态从 Idle 迁移到 Unloading,禁止新的引用获取。
  2. 等待阶段(Quiesce):等待所有已存在的引用释放。对于 procfs,这意味着等待所有打开的 /proc/xxx 文件关闭;对于 FUSE,这意味着等待所有 /dev/fuse 文件描述符关闭以及 VFS 挂载点解除。
  3. 回收阶段(Teardown):调用模块的 exit 函数,解除符号绑定,释放 ELF 占用的物理页,最终将状态迁移回 Unloaded

3.4 VFS 层触发重试机制

为了避免在每个系统调用路径中手动插入加载逻辑,StarryOS 在 VFS 层引入了一个通用函数 with_ondemand()。当 VFS 操作(如 lookupopenread)返回 NotFound 时,该函数会:

  1. 检查失败路径是否匹配某个已注册的按需加载触发器(如 /proc/* 对应 procfs.ko/dev/fuse 对应 fuse.ko)。
  2. 若匹配,则调用 ondemand-kmod::try_load() 尝试加载。
  3. 加载成功后,自动重试原始的 VFS 操作。
  4. 若加载失败或重试后仍返回错误,则将错误返回给用户态。

这种设计的关键优势在于:触发逻辑对上层完全透明,无论是用户态程序、libc 还是 Shell,都不需要任何修改。

3.5 Starryfuse 内核驱动架构

Starryfuse 是 StarryOS 中 FUSE 的内核侧实现,被包装为 fuse.ko 可加载模块。其内部采用四层架构:

  • abi:FUSE 协议数据结构的 Rust 定义,严格对齐 Linux FUSE ABI(如 fuse_in_headerfuse_out_headerfuse_init_infuse_init_out)。
  • dev:字符设备 /dev/fuse 的实现,负责内核与用户态守护进程之间的字节流传输。包含 FuseDev 结构体、PollSet 多路复用、以及 WaitQueue 阻塞/唤醒机制。
  • vfs:VFS 桥接层,将 StarryOS 的 axfs_vfs 操作(lookupreadwritereaddir 等)翻译为 FUSE 请求,通过 dev 层发送给用户态守护进程,再将其响应翻译回 VFS 语义。
  • libstarry_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 模块描述符、ModuleGuard RAII 引用计数守卫以及 ManagedModule 运行时 bookkeeping 结构。状态转换的实际逻辑由 registry.rson_access 处理加载触发与状态迁移动作)和 monitor.rstick 中完成 ActiveIdle 迁移及卸载决策)驱动。
  • monitor.rs:实现 IdleMonitor::tick() 三阶段卸载算法。Phase 1 持锁扫描注册表,将引用计数为零且空闲超时的模块从 Idle 标记为 Unloading;Phase 2 在无锁环境下调用 ModuleLoader::unload() 执行实际卸载;Phase 3 再次持锁将成功卸载的模块状态回写为 Unloaded。该函数由 api/src/kmod/ondemand.rstick_ondemand() 定期调用。

框架通过 ModuleLoader trait 与具体的操作系统解耦,定义了 load()unload() 等方法。StarryOS 在 api/src/kmod/ondemand.rs 中提供 KmodOnDemandLoader 结构体作为该 trait 的具体实现,其内部调用现有的 kmod-loaderaxalloc 内存管理接口。

4.2 with_ondemand VFS 集成

with_ondemand 是一个泛型函数,其实现位于 api/src/kmod/ondemand.rs。以 lookup 为例:

1
with_ondemand(&path, || fs.resolve(&path))

函数调用逻辑:

  1. 首次调用闭包 vfs.lookup(path)
  2. 若返回 Err(NotFound),提取路径中的前缀,查询 registry 是否有匹配。
  3. 若有匹配,调用 try_load();加载成功后继续下一次循环(重试)。
  4. 若返回其他错误或连续重试次数超过上限,则直接返回错误。

该函数被包裹在 openstatchmodchown 等关键系统调用路径中,确保几乎所有文件系统操作都能触发按需加载。

4.3 FuseDev 的并发与同步重构

早期的 /dev/fuse 实现使用单线程自旋锁保护整个设备状态,导致当守护进程阻塞在 read 等待请求时,其他线程无法并发写入新请求。为此,本文对 FuseDev 进行了并发重构:

  • 引入 PollSet:支持多线程同时 pollread 通过 WaitQueue 串行服务,内核可以在任意线程上向 PollSet 投递可读/可写事件。
  • 引入 WaitQueue:当没有待处理请求时,read 调用将当前任务挂起到 WaitQueue;当有新的 VFS 请求到达时,由 vfs 层唤醒等待队列中的任务。
  • 锁安全:调用文件系统函数时先短暂拿锁检查请求队列,无数据则立即释放锁,再通过外部 WaitQueue 安全阻塞睡眠,消除”持自旋锁睡眠”导致的死锁风险。

4.4 vfs.rs 的协议桥接

vfs.rsStarryfuse 中最复杂的模块,负责将 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 功能的正确性,本文设计了三组测试程序:

  1. fuse_test:基础功能测试,验证 /dev/fuse 的按需加载、FUSE_INIT 握手、简单的 lookup/read/readdir 以及空闲卸载。
  2. fuse_rw_test:读写功能测试,验证 write + read 闭环、文件截断覆盖、目录创建与遍历。
  3. fuse_mem_test:内存与稳定性测试,通过单次加载/卸载 FUSE 模块并读取内核内存快照,观测内存占用是否回归基线,检测是否存在内存泄漏。

5.2 测试环境

  • 目标平台riscv64gc-unknown-none-elf
  • 运行环境:QEMU 7.2+ virt 机器
  • 内核配置:开启 KALLSYMSLKMONDEMAND_KMODFUSE
  • 测试方式:在 QEMU 中运行 StarryOS,启动测试程序,串口输出日志

5.3 测试结果

5.3.1 fuse_test

注:空闲卸载触发时间:5 s

测试日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
[318.750672 0:11 kmod_loader::loader:354] Module(Some("fuse")) loaded successfully!
[318.806927 0:11 fuse:41] Fuse module loaded via on-demand mechanism.
[318.808250 0:11 starry_api::kmod:164] Module(fuse) init returned: 0
[318.810531 0:11 starry_api::kmod::ondemand:55] [memtest] after_load_fuse RustHeap=9388272 PageCache=806912 Pages=12970
[318.814701 0:11 starry_api::kmod::ondemand:101] [ondemand] module 'fuse' loaded, handle=0x17c96ff18
Opened /dev/fuse
Mounted /mnt/fuse successfully
About to fork self-test child...
fork returned 13
Spawned self-test child pid=13
fork returned 0
=== FUSE Self-Test Starting ===
Received FUSE request: opcode=26, unique=1, nodeid=0
Sent INIT response
Received FUSE request: opcode=3, unique=2, nodeid=1
Sent GETATTR response for nodeid=1
[TEST] ls /mnt/fuse:
Received FUSE request: opcode=28, unique=3, nodeid=1
Sent READDIR response (offset=0, bytes=96)
test.txt
Received FUSE request: opcode=28, unique=4, nodeid=1
Sent READDIR response (offset=3, bytes=0)
[TEST] ls /mnt/fuse: PASS
Received FUSE request: opcode=1, unique=5, nodeid=1
Sent LOOKUP response for 'test.txt'
Received FUSE request: opcode=3, unique=6, nodeid=100
Sent GETATTR response for nodeid=100
Received FUSE request: opcode=3, unique=7, nodeid=100
Sent GETATTR response for nodeid=100
Received FUSE request: opcode=15, unique=8, nodeid=100
Sent READ response (nodeid=100, offset=0, req_size=13, bytes=13)
Received FUSE request: opcode=15, unique=9, nodeid=100
Sent READ response (nodeid=100, offset=13, req_size=32, bytes=0)
[TEST] read test.txt: PASS (contents: "hello, fuse!\n")
=== FUSE Self-Test Complete ===
Self-test child exited, status=0
Test complete, daemon exiting.
starry:~# [324.572559 0:6 starry_api::kmod::ondemand:113] [ondemand] unload handle=0x17c96ff18
[324.589301 0:6 starry_api::kmod::ondemand:55] [memtest] before_unload_fuse RustHeap=8054592 PageCache=806912 Pages=12832
[324.599847 0:6 kmod_loader::loader:122] Calling module exit function...
[324.603021 0:6 fuse:53] Fuse module exit called.
[324.603687 0:6 starry_api::kmod:179] Module(fuse) exited
[324.604724 0:6 starry_api::kmod:74] KmodMem::drop: Deallocating paddr=PA:0x81a3a000, num_pages=10
[324.606940 0:6 starry_api::kmod:74] KmodMem::drop: Deallocating paddr=PA:0x81a44000, num_pages=5
[324.608016 0:6 starry_api::kmod:74] KmodMem::drop: Deallocating paddr=PA:0x819e3000, num_pages=1
[324.608835 0:6 starry_api::kmod:74] KmodMem::drop: Deallocating paddr=PA:0x81a49000, num_pages=1
[324.610092 0:6 starry_api::kmod::ondemand:55] [memtest] after_unload_fuse RustHeap=8053164 PageCache=806912 Pages=12815
starry:~#

5.3.2 fuse_rw_test

测试日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
[ 15.872268 0:11 kmod_loader::loader:354] Module(Some("fuse")) loaded successfully!
[ 15.891686 0:11 fuse:41] Fuse module loaded via on-demand mechanism.
[ 15.892565 0:11 starry_api::kmod:164] Module(fuse) init returned: 0
[ 15.896285 0:11 starry_api::kmod::ondemand:55] [memtest] after_load_fuse RustHeap=9387756 PageCache=806912 Pages=12973
[ 15.951746 0:11 starry_api::kmod::ondemand:101] [ondemand] module 'fuse' loaded, handle=0x17c96ff18
Opened /dev/fuse
Mounted /mnt/fuse successfully
About to fork self-test child...
fork returned 13
Spawned self-test child pid=13
fork returned 0
=== FUSE RW Self-Test Starting ===
Received FUSE request: opcode=26, unique=1, nodeid=0
Sent INIT response
Received FUSE request: opcode=1, unique=2, nodeid=1
Sent LOOKUP response for 'rw_test.txt'
Received FUSE request: opcode=3, unique=3, nodeid=100
Sent GETATTR response for nodeid=100
Received FUSE request: opcode=3, unique=4, nodeid=100
Sent GETATTR response for nodeid=100
Received FUSE request: opcode=15, unique=5, nodeid=100
Sent READ response (nodeid=100, offset=0, req_size=19, bytes=19)
Received FUSE request: opcode=15, unique=6, nodeid=100
Sent READ response (nodeid=100, offset=19, req_size=32, bytes=0)
[TEST] initial read: PASS (hello from rw test!)
Received FUSE request: opcode=1, unique=7, nodeid=1
Sent LOOKUP response for 'rw_test.txt'
Received FUSE request: opcode=4, unique=8, nodeid=100
Sent SETATTR response for nodeid=100
Received FUSE request: opcode=3, unique=9, nodeid=100
Sent GETATTR response for nodeid=100
Received FUSE request: opcode=16, unique=10, nodeid=100
Sent WRITE response (nodeid=100, offset=0, bytes=16)
[TEST] write existing: PASS
Received FUSE request: opcode=1, unique=11, nodeid=1
Sent LOOKUP response for 'rw_test.txt'
Received FUSE request: opcode=3, unique=12, nodeid=100
Sent GETATTR response for nodeid=100
Received FUSE request: opcode=3, unique=13, nodeid=100
Sent GETATTR response for nodeid=100
Received FUSE request: opcode=15, unique=14, nodeid=100
Sent READ response (nodeid=100, offset=0, req_size=16, bytes=16)
Received FUSE request: opcode=15, unique=15, nodeid=100
Sent READ response (nodeid=100, offset=16, req_size=32, bytes=0)
[TEST] read-back: PASS (new file content)
Received FUSE request: opcode=9, unique=16, nodeid=1
Sent MKDIR response
[TEST] mkdir: PASS
Received FUSE request: opcode=1, unique=17, nodeid=1
Received FUSE request: opcode=35, unique=18, nodeid=1
Sent CREATE response for 'newfile.txt'
Received FUSE request: opcode=4, unique=19, nodeid=200
Sent SETATTR response for nodeid=200
Received FUSE request: opcode=3, unique=20, nodeid=200
Sent GETATTR response for nodeid=200
Received FUSE request: opcode=16, unique=21, nodeid=200
Sent WRITE response (nodeid=200, offset=0, bytes=16)
[TEST] create+write: PASS
Received FUSE request: opcode=3, unique=22, nodeid=200
Sent GETATTR response for nodeid=200
Received FUSE request: opcode=3, unique=23, nodeid=200
Sent GETATTR response for nodeid=200
Received FUSE request: opcode=15, unique=24, nodeid=200
Sent READ response (nodeid=200, offset=0, req_size=16, bytes=16)
Received FUSE request: opcode=15, unique=25, nodeid=200
Sent READ response (nodeid=200, offset=16, req_size=32, bytes=0)
[TEST] read newfile: PASS (new file content)
Received FUSE request: opcode=3, unique=26, nodeid=1
Sent GETATTR response for nodeid=1
Received FUSE request: opcode=28, unique=27, nodeid=1
Sent READDIR response (offset=0, bytes=176)
Received FUSE request: opcode=28, unique=28, nodeid=1
Sent READDIR response (offset=5, bytes=0)
[TEST] readdir: entries=rw_test.txt,mydir,newfile.txt
[TEST] readdir: PASS
Received FUSE request: opcode=3, unique=29, nodeid=300
Sent GETATTR response for nodeid=300
Received FUSE request: opcode=28, unique=30, nodeid=300
Sent READDIR response (offset=0, bytes=64)
Received FUSE request: opcode=28, unique=31, nodeid=300
Sent READDIR response (offset=2, bytes=0)
[TEST] readdir mydir: entries=
[TEST] readdir mydir: PASS
=== FUSE RW Self-Test Complete ===
Self-test child exited, status=0
Test complete, daemon exiting.
starry:~# [ 21.648930 0:6 starry_api::kmod::ondemand:113] [ondemand] unload handle=0x17c96ff18
[ 21.652581 0:6 starry_api::kmod::ondemand:55] [memtest] before_unload_fuse RustHeap=8054346 PageCache=806912 Pages=12832
[ 21.654526 0:6 kmod_loader::loader:122] Calling module exit function...
[ 21.656865 0:6 fuse:53] Fuse module exit called.
[ 21.657902 0:6 starry_api::kmod:179] Module(fuse) exited
[ 21.659324 0:6 starry_api::kmod:74] KmodMem::drop: Deallocating paddr=PA:0x81a3d000, num_pages=10
[ 21.662123 0:6 starry_api::kmod:74] KmodMem::drop: Deallocating paddr=PA:0x81a47000, num_pages=5
[ 21.663854 0:6 starry_api::kmod:74] KmodMem::drop: Deallocating paddr=PA:0x819e9000, num_pages=1
[ 21.665418 0:6 starry_api::kmod:74] KmodMem::drop: Deallocating paddr=PA:0x81a4c000, num_pages=1
[ 21.667279 0:6 starry_api::kmod::ondemand:55] [memtest] after_unload_fuse RustHeap=8052918 PageCache=806912 Pages=12815
starry:~#

5.3.3 fuse_mem_test

测试日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
[  8.163909 0:11 kmod_loader::loader:354] Module(Some("fuse")) loaded successfully!
[ 8.168286 0:11 fuse:41] Fuse module loaded via on-demand mechanism.
[ 8.169185 0:11 starry_api::kmod:164] Module(fuse) init returned: 0
[ 8.170310 0:11 starry_api::kmod::ondemand:55] [memtest] after_load_fuse RustHeap=8862712 PageCache=806912 Pages=4779
[ 8.171749 0:11 starry_api::kmod::ondemand:101] [ondemand] module 'fuse' loaded, handle=0x17c96ff18
Opened /dev/fuse
Mounted /mnt/fuse successfully
About to fork self-test child...
fork returned 13
Spawned self-test child pid=13
fork returned 0
=== FUSE Self-Test Starting ===
Received FUSE request: opcode=26, unique=1, nodeid=0
Sent INIT response
Received FUSE request: opcode=3, unique=2, nodeid=1
Sent GETATTR response for nodeid=1
[TEST] ls /mnt/fuse:
Received FUSE request: opcode=28, unique=3, nodeid=1
Sent READDIR response (offset=0, bytes=96)
test.txt
Received FUSE request: opcode=28, unique=4, nodeid=1
Sent READDIR response (offset=3, bytes=0)
[TEST] ls /mnt/fuse: PASS
Received FUSE request: opcode=1, unique=5, nodeid=1
Sent LOOKUP response for 'test.txt'
Received FUSE request: opcode=3, unique=6, nodeid=100
Sent GETATTR response for nodeid=100
Received FUSE request: opcode=3, unique=7, nodeid=100
Sent GETATTR response for nodeid=100
Received FUSE request: opcode=15, unique=8, nodeid=100
Sent READ response (nodeid=100, offset=0, req_size=13, bytes=13)
Received FUSE request: opcode=15, unique=9, nodeid=100
Sent READ response (nodeid=100, offset=13, req_size=32, bytes=0)
[TEST] read test.txt: PASS (contents: "hello, fuse!\n")
=== FUSE Self-Test Complete ===
Self-test child exited, status=0
Test complete, daemon exiting.
Unmounted /mnt/fuse
FUSE device closed.
Waiting 7s for idle unload...
[ 13.742596 0:6 starry_api::kmod::ondemand:113] [ondemand] unload handle=0x17c96ff18
[ 13.746465 0:6 starry_api::kmod::ondemand:55] [memtest] before_unload_fuse RustHeap=8352536 PageCache=806912 Pages=4788
[ 13.751490 0:6 kmod_loader::loader:122] Calling module exit function...
[ 13.755245 0:6 fuse:53] Fuse module exit called.
[ 13.757140 0:6 starry_api::kmod:179] Module(fuse) exited
[ 13.761513 0:6 starry_api::kmod:74] KmodMem::drop: Deallocating paddr=PA:0x81a3b000, num_pages=10
[ 13.767803 0:6 starry_api::kmod:74] KmodMem::drop: Deallocating paddr=PA:0x81a45000, num_pages=5
[ 13.770209 0:6 starry_api::kmod:74] KmodMem::drop: Deallocating paddr=PA:0x819e5000, num_pages=1
[ 13.770881 0:6 starry_api::kmod:74] KmodMem::drop: Deallocating paddr=PA:0x81a4a000, num_pages=1
[ 13.772583 0:6 starry_api::kmod::ondemand:55] [memtest] after_unload_fuse RustHeap=8351108 PageCache=806912 Pages=4771
=== FUSE On-Demand Memory Test Results ===

Table 1. Raw snapshots from kernel
Phase RustHeap(B) RustHeap(d) Pages Pages(d)
----------------------------------------------------------------------------
Before load 8315080 - 4762 -
After load 8862712 +547632 4779 +17
Before unload 8352536 +37456 4788 +26
After unload 8351108 +36028 4771 -17

Table 2. Memory contribution analysis
Configuration Size(KB) Pages Contribution
-----------------------------------------------------------------------------------------------
A. Static baseline (fuse.ko + starryfuse resident) 1071 - baseline
- fuse.ko (416 KB) 416 - -
- starryfuse libs (655 KB) 655 - -
B. On-demand mapped pages (loader vmalloc) 68 17 actual load
D. Runtime overhead (mount/fork/VFS, unrelated) 0 0 transient
-----------------------------------------------------------------------------------------------
Memory saving vs static baseline 1071 - resident reduction

Conclusion:
- Static linking would keep ~1071 KB of FUSE driver resident in kernel memory.
- On-demand loading reduces this resident footprint to ~0 KB after unload.
- Actual memory saving = 1071 KB (all static baseline reclaimed after unload).

Result: PASS (on-demand loading saves 1071 KB of resident kernel memory)
starry:~#

对日志中的内存数据整理如 表 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 中。在此基础上,本文完成了 procfsFUSE 的按需加载闭环。对于 FUSE,本文从零实现了内核侧驱动 Starryfuse,涵盖协议解析、字符设备通信、VFS 桥接与用户态库,支持了完整的 FUSE 文件系统生命周期。

6.2 未来工作

  1. 块设备文件系统按需加载:当前框架主要面向用户态文件系统与伪文件系统。未来可将其扩展至 ext4fat32 等块设备文件系统。
  2. 完善 starry_fuser 功能集:补充 FUSE_MKNODFUSE_IOCTL 等高级操作码,提升与现有 libfuse 的兼容性。
  3. vDSO 与系统调用优化:探索将部分 FUSE 请求路径通过 vDSO 优化,减少用户态/内核态切换次数。

7.相关链接

项目工程仓库
按需加载库
PPT 演示文件
问题日志
开发日志仓库


自动测试系统 与 EEVDF 调度器实习三个月技术总结

摘要

本报告汇总实习三个月在测试系统和 EEVDF 调度算法的技术产出。测试工程方面:在 AxVisor 中推进 QEMU CI 稳定性,统一 ArceOS/Linux/NimbOS 环境准备脚本、补充中英文快速上手文档,并以 PTY 驱动 NimbOS 自动化与 fail_regex 等增强失败可观测性;在 github-runners 中基于 flock 建立多组织共享开发板的 per-board 硬件锁,迭代修复取消路径竞态与僵尸锁问题;在 axci 中设计规则驱动、依赖感知的自动测试目标选择,结合 git diff、反向依赖图与可配置规则缩小无关全量测试范围。内核方面:在 StarryOS 分支上实现 EEVDF 调度器(双索引、deadline 抢占、统计与文档/单测/演示脚本闭环),并完成 per-CPU 异构调度与 QEMU 下代表性延时与切换行为验证。

自动测试系统

1. AxVisor QEMU CI 稳定性改进

  • 链接:PR #363
  • 合并状态:已合并(Merged)
  • 贡献规模:26 commits,18 个文件变更,+678 / -19

背景问题

在 AxVisor 的 QEMU 自动化测试中,存在以下痛点:

  • 不同 Guest(ArceOS / Linux / NimbOS)环境准备流程分散,维护成本高;
  • NimbOS 场景依赖交互式输入,CI 中容易出现“测试通过但任务失败”的误报;
  • 失败信号不够明确,panic 等异常无法被尽早识别。

关键工作

  1. 统一环境准备入口
  • 新增 scripts/setup_qemu.sh,统一支持 arceos / linux / nimbos 三类 Guest;
  • 自动执行镜像下载、配置 patch、rootfs 准备,减少手工步骤和路径错误。
  1. 补齐文档与上手路径
  1. 增强 NimbOS 测试可自动化能力
  • 新增 scripts/ci_run_qemu_nimbos.py,通过 PTY 方式启动子进程,保障 CI 环境下输入可正确透传;
  • 识别 shell 提示后自动触发 usertests,并在命中 usertests passed! 时返回正确退出码。
  1. 提升 CI 失败可观测性与可诊断性
  • 在 QEMU 配置中补充 fail_regex(如 panicked at),尽早暴露 guest panic;
  • 对 NimbOS 启动依赖(axvm-bios.bin)进行前置校验,避免隐式失败。

结果与价值

  • 测试稳定性:修复了 NimbOS 在 CI 中的交互与退出码问题,显著降低误报失败;
  • 工程效率:统一 setup 脚本后,减少重复脚本与人工排障成本;
  • 可维护性:流程与文档标准化后,跨场景测试可复用性更高;
  • 团队协作:将经验固化为脚本与文档,便于后续同学复用与扩展。

2. 多组织 GitHub Runner 硬件锁机制建设(github-runners)

背景问题

在多组织共享开发板资源时,Runner 缺乏统一锁机制,容易出现并发抢占、取消后资源未释放、锁粒度不一致等问题,导致 CI 任务互相干扰、排队时间增加、故障定位困难。

实现原理(文件锁)

  • 方案基于 Linux 文件锁(flock)实现互斥:将每块开发板抽象为一个 lock file,同一时刻仅允许一个 Runner 持有该文件的独占锁;
  • 锁粒度为 per-board:同一块板上的任务互斥,不同开发板对应不同锁文件,可被不同 Runner 并行使用;
  • 任务启动时先尝试获取独占锁,获取成功后进入执行阶段,失败则等待或退出,避免多任务并发抢占同一硬件;
  • 在正常结束、失败或 Cancel 路径统一执行解锁与清理,降低异常中断后残留“僵尸锁”的概率。

关键工作(按迭代演进)

  1. 建立锁包装能力(PR #2#3
  • 为多组织共享硬件引入 runner-wrapper 锁包装能力(实现见 runner-wrapper.sh);
  • 将锁能力集成进 runner.sh 工作流,并补齐使用文档,形成可落地的基础方案。
  1. 标准化锁标识与隔离策略(PR #4
  • 将板子锁 ID 收敛到 per-board 默认策略;
  • 将容器命名自动拼入 org/repo 维度,降低跨组织任务冲突概率。
  1. 修复并发竞态与取消场景(PR #11#13
  • 加固多组织 Runner 锁机制,修复 cancel 场景下的并行竞态问题;
  • 将 cancel watcher 与 docker compose 生命周期集成,随 Runner 一起启动/回收,对使用者基本无感;
  • 支持在 Cancel 路径自动释放开发板锁,减少“僵尸锁”导致的资源阻塞;
  • 持续补充文档,降低维护门槛并提升团队可复用性。

结果与价值

  • 资源利用率:降低开发板被异常占用的概率,提升共享硬件可用性;
  • 流程鲁棒性:在取消、失败等非理想路径下也能保证锁释放;
  • 并发安全性:减少跨组织并行任务互相抢占与串扰;
  • 可运维性:锁策略、命名规范和文档沉淀后,问题定位更快、迁移成本更低。

3. axci 规则驱动自动目标选择与测试链路重构

  • 链接:PR #9
  • 当前状态:Open(待合并)
  • 变更规模:39 commits,21 个文件变更,+4071 / -397

背景问题

在 CI 全量测试模式下,存在“变更范围小但测试范围大”的问题,导致执行耗时长、资源利用率偏低;同时,测试脚本长期演进后出现结构耦合,扩展与维护成本上升。

实现原理(依赖感知自动选目标)

  • 基于 git diff 获取变更文件,并结合 cargo metadata 将变更路径映射到对应 workspace crate;
  • 从直接变更 crate 出发,在反向依赖图上做 BFS 扩散,得到受影响 crate 集合(affected_crates);
  • 按规则文件(路径规则、crate 规则、全量触发规则)求值得到逻辑目标 key 列表(targets);
  • 在 CI detect 阶段按 target_key 过滤预置候选矩阵,生成最终并行 job,避免无关目标全量执行。

接入方式(落地步骤)

  • 引用方式:在组件仓库 workflow 中显式拉取 arceos-hypervisor/axci(固定分支或 commit),复用其 axci-affected 与规则处理逻辑;
  • 在仓库测试入口(如 tests.sh)接入 --auto-target--base-ref 参数,支持按基线分支自动选择目标;
  • 在 workflow(如 .github/workflows/test.yml)增加 detect 阶段:先计算 targets,再按 target_key 过滤预置矩阵并输出 JSON;
  • 在执行阶段使用 matrix.include: ${{ fromJson(...) }} 并行运行目标任务,skip_all 时直接跳过无关 job;
  • 保留回退路径:axci-affected 不可用时回退到 shell 规则匹配,保证 CI 可用性与渐进迁移。

规则自定义(可配置能力)

  • 规则文件默认位于 configs/test-target-rules.json;组件仓库可在 .github/axci-test-target-rules.json 放置自定义规则,无需改动 axci 主仓代码;
  • 组件侧规则可按仓库测试拓扑覆盖目标映射(如新增/删除 target key、调整 target_order、补充路径或 crate 触发条件);
  • 可按目录/文件模式定义 selection_rules,将路径变更映射到测试目标;
  • 可按 crate 维度定义 crate_rules(含 direct_only),区分仅直接变更还是包含依赖扩散影响;
  • 可通过 run_all_patternsrun_all_crates 定义“全量触发条件”,并用 non_code 规则跳过纯文档类变更;
  • 通过 target_order 统一目标输出顺序,保证选择结果稳定、可预期、便于回归对比。

关键工作

  1. 合并模块化重构并统一测试入口
  • 保持 tests.sh 作为统一入口,兼容已有流程并提升后续可维护性。
  1. 引入规则驱动自动目标选择
  • tests.sh 增加 --auto-target--base-ref 能力;
  • 新增 configs/test-target-rules.json,将路径匹配与依赖规则配置化;
  • 优先使用 axci-affected 引擎做影响范围分析,失败时回退到 shell 规则匹配,保证可用性。
  1. 增强 CI 可观测性与稳定性
  • test.yml 增加 test_targets=auto 相关输入与 detect-targets 检测链路;
  • 输出自动选择决策摘要(selection mode、auto reason、target list)到 GITHUB_STEP_SUMMARY
  • 补充 git 网络抗抖动参数、checkout 超时与关键依赖检查,降低网络和环境抖动带来的不确定失败。
  1. 加固 Starry 测试链路
  • 在运行前增加 disk.img 检查与软链兜底逻辑,减少镜像路径问题导致的无效失败。

阶段性价值

  • 效率收益:为“按影响范围执行测试”打通主链路,预期可显著减少无关测试开销;
  • 工程收益:测试能力从脚本硬编码向“规则配置 + 引擎分析”演进;
  • 质量收益:自动选择过程具备可解释输出,便于排障与规则迭代;
  • 扩展收益:模块化后更便于后续新增 target、suite 与规则。

相关代码(速查)

原理细节与端到端数据流可参考:docs/axci-工作原理.md

EEVDF

代表性成果:StarryOS 中 EEVDF 调度器实现与验证

背景问题

在操作系统调度中,需要同时满足两类目标:

  • 公平性:不同优先级任务应按权重获得合理 CPU 份额;
  • 响应性:交互任务应尽快获得服务,避免高负载下长尾延迟。

传统仅按时间片轮转或仅按 vruntime 最小选择,难以同时兼顾“公平份额”与“截止期驱动响应”。因此在 StarryOS 上实现 EEVDF(Earliest Eligible Virtual Deadline First)调度器,验证其在可解释性、公平性和可观测性上的工程价值。

关键工作

  1. 完成 per-task EEVDF 核心调度逻辑
  • crates/axsched/src/eevdf.rs 中实现 EevdfSchedulerEevdfEntity
  • 任务实体维护 vruntimedeadlineniceslice 等关键元数据;
  • 采用 Linux 兼容 nice->weight 映射(-20..19)并据此计算 vruntime 增量与 deadline。
  1. 设计双索引结构,兼顾选择效率与资格判断
  • ready_queue:按 (deadline, id) 排序,快速获得最早 deadline 任务;
  • vrt_set:按 (vruntime, id) 排序,用于 vruntime <= V 的 eligible 范围查询;
  • id_to_deadline:连接两套索引,保障在慢路径下仍可高效定位候选任务。
  1. 完成 EEVDF 选取与抢占策略
  • pick_next_task:优先走快路径(最早 deadline 且 eligible),否则走慢路径筛选 eligible 中 deadline 最小任务;
  • 当无 eligible 任务时启用 fallback(直接取最早 deadline)保证系统可推进;
  • task_tick 中实现 deadline 驱动抢占:若队首任务 eligible 且 deadline 更早,则触发抢占。
  1. 完成与运行队列集成及可观测性建设
  1. 支持多 CPU 指定调度算法(per-CPU 异构调度)
  • 设计并实现调度器元数据分离,支持不同 CPU 绑定不同调度算法,避免全局单策略耦合;
  • 引入 CPU_SCHED 编译期配置,支持按 CPU 维度声明调度策略;
  • 补充跨调度器迁移路径的设计与验证要点,保证任务迁移过程的状态一致性与可预期行为。
  • 与 Linux 现状相比,当前方案仍以编译期静态指定为主:尚未覆盖运行时动态策略切换、成熟的跨 CPU 负载均衡协同以及更完整的调度域/拓扑感知能力。
  1. 补齐文档与验证闭环

实验结果

以下 QEMU 延时表与切换间隔估算为代表性一次测量;不同主机负载、SAMPLES/LOAD 与内核版本下数值会变化,结论以「base 与 nice19 的相对关系」及当次 serial.log、结果目录为准。

  1. 单元测试结果
  • 等权重公平性测试:3 个 nice=0 任务长期运行后,CPU 占比误差控制在预期范围内;
  • 加权公平性测试:nice -5/0/+5 场景下,CPU 占比与权重比一致性良好;
  • 抢占与 deadline 修正测试:覆盖“时间片耗尽”和“提前抢占后剩余时间片重算”路径;
  • fallback 场景测试:在强制无 eligible 条件下,兜底逻辑与统计计数行为符合预期。
  1. QEMU 实测表现
  • QEMU 下调度统计与现场压测结果一致:负载窗口内调度行为稳定且可解释,未观察到 fallback 退化,可用于后续回归比较与参数调优。
  1. 现场性能与任务切换延时数据(riscv64-qemu-virt,SMP=1)
  • 前台延时对比(4 个后台 yes 压力任务):

    场景 N p50 p95 p99 max
    base 50 0.630s 0.640s 0.640s 0.850s
    nice19 50 0.040s 0.040s 0.040s 0.040s
    • 结论:降低后台优先级后,前台命令 tail latency(p95/p99)约改善 16x0.64s -> 0.04s),最坏时延从 0.85s 降到 0.04s
  • 任务切换延时(EEVDF 周期统计日志):

    • 观测窗口:interval_ticks=256ticks_per_sec=100,单窗口时长 2.56s
    • 负载阶段多窗口 delta[picks] 稳定在 51 左右(停压过渡窗口约 45
    • 估算平均任务切换间隔:2560 / 51 ≈ 50.2ms(过渡窗口 2560 / 45 ≈ 56.9ms
    • 对应切换频率约 19.9Hz(过渡窗口约 17.6Hz
  • 调度行为解释:

    • slice_expired 在负载窗口持续增长,说明切换主要由时间片驱动;
    • preempt_by_deadline 有触发但非主导路径;
    • fallback_no_eligible=0,未出现“无 eligible 任务”退化情况。
    • 小结:在持续负载下,EEVDF 调度表现为稳定的时间片主导切换(约 50ms/次),偶发 deadline 抢占,且无 fallback 退化,行为与设计预期一致。
  • 测量方法(可复现):

    • 环境:riscv64-qemu-virtrelease 构建,SMP=1LOG=info,启用 eevdf-stats-demo
    • 负载与探针:后台启动 4yes >/dev/null,前台以 ls 作为短任务探针,采样次数 N=50
    • 前台延时统计:使用 /usr/bin/time -f "%e" 记录每次 ls 的 wall time,按升序计算 p50/p95/p99/max
    • 任务切换延时统计:读取 eevdf stats 日志中的 delta[picks];窗口配置为 interval_ticks=256ticks_per_sec=100(窗口时长 2.56s);平均任务切换间隔按 2560 / delta_picks (ms) 估算,切换频率按其倒数换算为 Hz

结果与价值

  • 算法落地:将 EEVDF 从概念层落到可运行、可测试、可观测的内核实现;
  • 工程可维护性:双索引 + 统计设计使问题定位路径清晰,便于持续迭代;
  • 测试体系收益:建立了单测、演示、回归脚本三层验证,减少调度改动引入回归风险;
  • 团队协作收益:文档化沉淀完整,便于新成员快速理解调度设计与验证方法。

相关代码(速查)

经验复盘

  1. 调度算法实现不仅是“选下一个任务”,更关键是数据结构设计与状态一致性维护;
  2. 可观测性应与算法实现同步建设,否则难以在真实负载下解释行为差异;
  3. 对“无 eligible”这类边界路径提前设计 fallback 与测试,能显著降低线上不确定性;
  4. 通过“文档 + 脚本 + 单测”三位一体沉淀,能让调度改动从个人经验升级为团队资产。

附录:关键复现命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 0) 串口落盘(host,在 StarryOS 仓库根目录;目录需事先存在)
mkdir -p bench-results
make ARCH=riscv64 run LOG=info FEATURES=eevdf-stats-demo 2>&1 | tee bench-results/serial.log

# 1) 单元测试(host,可选)
cargo test -p axsched

# 2) 运行前台延时回归(guest)
# 使用仓库 scripts/bench-regression-eevdf.sh(wget 到 /root/ 或自行拷贝)。
# StarryOS/busybox 下建议先建结果目录,并显式指定 RESULT_DIR,避免 mkdir 行为差异:
mkdir /tmp/bench-results 2>/dev/null || true
export RESULT_DIR=/tmp/bench-results
export SAMPLES='50,200'
export LOAD=4
sh /root/bench-regression-eevdf.sh
# 结束后可检查:ls -la /tmp/bench-results 与 cat .../ls-latest-table.md

# 3) 施加/停止 CPU 负载以观测 eevdf stats(guest)
for i in 1 2 3 4; do yes >/dev/null & done
sleep 10
killall yes 2>/dev/null

# 4) 解析串口日志(host;TICKS_PER_SEC 需与内核 tick 频率一致,否则毫秒类估算仅作相对参考)
TICKS_PER_SEC=100 INTERVAL_TICKS=256 \
sh scripts/parse-eevdf-stats-log.sh ./bench-results/serial.log

说明:上文「实验结果」中的延时表与切换间隔估算来自代表性一次测量,不同 QEMU/主机负载下数值会波动;报告引用时建议以当次 serial.log/tmp/bench-results 输出及解析脚本结果为准。

相关链接

本节汇总与本报告对应的实习过程记录仓库主要上游/个人源码仓库,以及周报、月报与部署类文档入口,便于对照 PR 与日常开发轨迹。

实习日志与过程文档(本仓库)

  • 仓库根与索引:os-internship-log(开源社区实习日志;README.md 中含文档索引、周报与月报列表)
  • 按周记录:logs/week1.mdlogs/week12.md(路径相对仓库根,例如 logs/
  • 月报与技术报告:技术报告2月.md技术报告3月-贾一飞.md;部署与实施说明见 自动测试系统部署文档.md多组织共享测试环境实施文档.md

源代码仓库(报告涉及的主要工程)

工程 说明
arceos-hypervisor/axvisor AxVisor QEMU CI 稳定性改进
arceos-hypervisor/github-runners 多组织共享 Runner 与开发板锁
arceos-hypervisor/axci 组件 CI 与测试编排,自动目标选择相
yoinspiration/StarryOS EEVDF 调度器实现与验证脚本所在个人分支

现在是我第5次参加操作系统训练营了。我第一次接触Rust和参加操作系统训练营是2024年初。经过漫长时间的学习和消化,我意识到了自己接受新事物的速度还是比较慢的。在学习的旅途中,我遇到了很多志同道合之人——有些人擅长理论推导,有些人喜欢折腾底层汇编,还有些人总能在我卡住的时候扔来一个关键链接。这种氛围让我觉得,慢一点也没关系,只要还在往前走。

以下是我参加训练营的经历、感悟和收获。

Rust and Programming Language

在此之前,除了汇编语言,我只用过C系列的编程语言(C, C++ 和 C#)编写项目和代码。按照原理来说,很多编程语言都可以实现操作系统,至少只要实现了LLVM的前端,就能得到对应的后端二进制代码。但是近年来,使用Rust进行操作系统和嵌入式开发变成了新的趋势。

最初,我无法想象“内存安全性”会是怎样的情况。毕竟C++的智能指针已经宣称解决了大部分问题,可实际项目里引用计数循环、悬垂指针照样防不胜防。但接触了Rust之后,我发现Rust不仅仅是一种简单的语言。Rust在编译时就能检查代码,考虑到多线程的使用、内存泄漏以及整体的优化等问题。它不是在运行时给你抛出一个段错误,而是在你保存文件的那一刻就告诉你:“这里有问题,而且我连怎么改都建议好了。”这种安全感,让人安心。在语法上,Rust的一些设计和Scala、C#非常相像,比如模式匹配、闭包的写法,还有那种“表达式无处不在”的风格,一开始不太习惯,但写了几周之后回头看C++的?:三目运算符,总觉得少了点灵活。

第一点、Rust的标准库非常好。与标准C/C++库相比,Rust的标准库应用更加简洁,对字符串的支持非常省心。C等传统语言对Unicode编码的支持并不友好,也缺少统一的标准,而Rust则很好地解决了这个问题。我印象很深的一次经历:在做训练营的某个文件系统实验时,需要解析一个包含中文路径的用户输入。用C写的话,我得纠结是UTF-8还是GBK,还得手动处理字节长度;而在Rust里,std::path::Path和String直接就把这些事情理得清清楚楚。这种“不用自己造轮子,而且轮子还是圆的”的体验,大大降低了编写操作系统外围工具的心智负担。

第二点、Rust在类型上有创新。例如切片和引用,切片不仅包含起始地址,还有数据长度,这让Rust函数在很多时候可以简化传参,例如,其他语言传递基址还需要包含一个尺寸量,像memset、memcpy这类函数,但是包含数据长度可以少传一个参数,直接当成“Array”使用。这样做有一个特点(不能说是缺点),就是Rust的字符串与C互操作时需要在末尾添加NULL终止符,否则无法正确解析ASCIZ字符串。我在写内核的串口驱动时,就经常需要做这种转换——Rust的&str传到C的printf风格函数之前,得手动as_ptr()再加个\0,稍微有点啰嗦,但习惯了也就变成了肌肉记忆。默认,Rust变量和引用都是不可变的,这让我在写代码时更加注重变量的可写性。对于很大部分人而言,写代码的时候很少注意变量的可写性,导致以后想给一个接口的参数换为const的时候需要付出很多的修改。采用不可变性这一特性促使我在编写代码时声明更多的变量,从而使代码更具可读性。以前写C++的时候,我习惯把一个变量反复重用——一个int i既能当循环下标,又能存中间结果,最后还得存个标志位。现在回过头看,那简直就是“变量的一生被反复蹂躏”。Rust逼着我给每个新状态起一个新名字,代码反而变得像流水账一样清晰。

第三点、unsafe关键字。与C#一样,Rust也有一个“unsafe”关键字,用于标记那些无法由编译器保证安全性的代码。这使我们能够插入特殊代码,解决可能存在的执行问题。在做操作系统训练营的过程中,我发现unsafe其实是一个很好的设计——它把“危险的角落”用高亮笔圈了出来。比如直接操作页表、读写外设寄存器、或者调用一段手写的汇编启动代码,这些必须放进unsafe块里。我在写内核的内存分配器时,整个模块只有三处unsafe:一处是操作裸指针,一处是调用内联汇编刷TLB,还有一处是绕过借用检查器来初始化静态变量。剩下的几百行代码都享受着编译器的保护。相比C++,Rust的unsafe就像是给你一张地图,上面标注了“此处有地雷”,你只要小心这几块区域,其他地方的草可以放心踩。

第四点、Rust 有一种非常独特的所有权系统。与 C 系列语言有所不同。所有权系统能帮助我们避免内存泄漏或多重引用的问题。在其他语言中,我们可能会忘记考虑变量使用的相关机制。在内核的编写中,野指针是非常致命的存在。在长期的C++开发中,我已经屡次领教过野指针带来的问题,往往是忘记使用 memset 或者没有使用构造函数导致的问题。

第五点、Rust 有着非常独特的trait。这种特性可以使代码更加灵活且可复用。我对这个特性的第一感觉是没有C++的虚表和继承的功能更方便。但是Rust提供的 impl 机制可以比较自由地增加方法,并且可以指定方法在哪个特性中实现。比C#中的partial 机制还要更美妙一些。受到Rust的影响,我在写C++代码时也用trait来命名一些公共接口。

第六点、使用panic和Result处理异常。在C++的世界里,异常处理一直是个让人又爱又恨的话题,这个机制需要专门的开关支持,在内核编程基本没人用。noexcept又不够精细,导致你永远不知道一个函数到底会冒出什么幺蛾子。我见过不少C++项目,最后干脆禁用异常(-fno-exceptions),所有函数返回错误码,然后调用方用一堆if去判断。Rust的做法让我眼前一亮:它把“可能出错的操作”分成了两类。一类是“NMI型问题”,用panic!;另一类是“小问题,调用方可以处理”,用Result<T, E>

Architecture

我尝试在 x86, RISCV, ARM 架构上实现裸机程序。

  • x86 算是现代计算机架构的鼻祖,也是目前使用最为广泛的架构。从我们身边的台式机、笔记本,到数据中心的服务器,x86 几乎无处不在。它的历史包袱很重,从16位的8086一路延伸到64位的x86_64,里面塞满了各种兼容模式、分段机制、中断描述符表的复杂规则。这是我从小接触的架构。
  • RISCV 是一种开源的指令集架构,目前很多开源项目都在使用RISCV架构。
  • ARM 是一种商业的指令集架构,在嵌入式单片机、移动设备手机平板等电子设备上使用广泛。

我发现了RISCV有以下优势:

第一、RISCV 是一种开源的指令集架构,注定了生产使用的成本更低。x86和ARM的授权费用高,而且还有各种专利壁垒。这种低成本不仅仅体现在钱上,更体现在试错成本上。我们可以在模拟器里随便改指令集,完全不用考虑商业兼容性。这对于我们这些写操作系统的人来说,意味着底层的行为是完全透明、可预期的。

第二、RISC-V的32位与64位指令集架构高度相似。这一点,x86与ARM均未实现。RISC-V的32位(RV32)与64位(RV64)在分页机制上仅存在细微差异,而ARM的不同运行模式(如32位与64位)之间的分页技术则差异显著。x86的32位与64位架构在分段模型、寻址方式及指令使用等方面均表现出较大差异,导致跨位宽兼容的实现复杂度显著上升。这种相似性不仅体现在分页技术上,还贯穿于异常处理、控制状态寄存器(CSR)布局以及原子操作指令中。对于操作系统开发者而言,这意味着同一套内核代码可以通过条件编译(如根据 __riscv_xlen 宏)平滑地适配两种位宽,无需重写核心的页表遍历、上下文切换或系统调用入口逻辑。反观x86-64,其兼容32位模式时需要启用兼容子模式(compatibility mode),并且段寄存器、门描述符格式以及中断栈帧布局均与纯64位模式存在结构性差异,这迫使许多操作系统(如Linux)在x86上维护两套独立的低级入口代码。RISC-V通过设计上的统一性,显著降低了多平台内核的维护成本。

第三、RISCV的M态陷入默认是关闭分页的。这一点设计真的很好,可以直接使用恒等映射,这样在内核态和用户态切换时,不需要做任何的映射操作,直接使用恒等映射即可。而x86和ARM则需要在用户态和内核态切换时想办法转到内核页面。但是,RISCV的S态陷入默认是开启分页的。M态关闭分页的特性使得最低特权级的固件(如引导加载程序或安全监视器)能够完全绕过虚拟地址转换,直接以物理地址访问内存。这一设计简化了启动阶段的早期初始化流程:在设置页表之前,M态代码即可完成内存检测、设备树解析或安全服务调用,无需预先建立任何地址映射。x86架构则缺乏类似的干净抽象——即使是在系统管理模式(SMM)下,处理器仍然处于实模式或保护模式的分页语境中,开发者必须小心处理段基址与分页的相互作用,稍有不慎便会引发三重故障。与之相对,RISC-V的S态默认开启分页则是为了保障常规操作系统的运行效率:内核与用户进程共享统一的虚拟地址空间,通过页表隔离权限。但这一设计也带来了额外的约束——在S态下直接访问未映射的物理地址需要临时切换页表或使用恒等映射区域,否则会触发页错误。

Experience and Another Design

今年的训练营,给我的最大挑战的是ArceOS环境的配置。我试过调整Toolchain的版本,也试过修改单个依赖的版本,都失败了。最终,我决定采用最新的工具版本,针对其中的一些版本适配问题,我会将一些依赖的内容固定到仓库中。

与之前几期相比,Rustlings 后又增加一个阶段。主要涉及了多线程下的Rust应用,功能很实用。rCore 的前几个章节有略微的改动。


print_with_color作业

一开始要实现一个彩色打印功能,与rCore一开始实现的一样。原理都是 Linux 上面的终端支持。我实现的是绿色的,用这个颜色的原因是其他例程pass时也是用绿色输出的。

这个终端颜色的原理也让我将其用作其他程序的Logging输出。

sys_mmap作业

这个作业让我对内存管理有了更深的理解。将文件内容映射为内存页,这个设计很好。让我掌握到了匿名映射,按需分配等概念,以后可以自己从头实现试一试。

support_hashmap, ramfs_rename作业

要注意到修改Cargo.toml文件里依赖和版本

我使用 C++ 设计中另一个类组件化的微内核(mecocoa)。设计思路和经历如下

IO系统

IO 系统是我最早着手实现的部分之一,也是经历重构次数最多的模块。最初只是想做一个能显示字符的文本控制台,后来逐步加入了图形界面、TTY 虚拟终端、键盘鼠标输入以及窗口管理。

系统支持多个虚拟终端(VTTY),每个终端拥有自己的输入输出队列。当一个进程调用 INNC 等待键盘输入时,它的 PID 会被加入等待序列。主循环 serv_cons_loop 会轮询所有阻塞进程对应的 TTY 输入队列,一旦有字符到达就唤醒该进程并发送消息。这个设计简单直观,但轮询效率不高,而且没有优先级区分。

键盘中断服务程序将扫描码转换成 keyboard_event_t 结构,然后调用 hand_kboard 放入当前焦点 TTY 的输入队列。鼠标消息同样经过 hand_mouse 处理。这种消息化设计使得输入源与消费端解耦,后续若要支持触摸屏或远程输入,只需增加对应的消息产生者即可。

当前的消息轮询模型效率低下,未来要解决这些问题。

图形处理

图形处理模块是 IO 系统的上层延伸,负责鼠标光标绘制、窗口图层管理、基本绘图原语以及双缓冲刷新。其设计围绕图层管理器与底层图形输出接口展开,实现了从像素绘制到窗口交互的初步功能。针对不同显存布局,实现了两种主要的图形输出类,分别处理 ARGB 与 ABGR 两种字节序。基本绘图接口(画点、画矩形、批量画点)直接操作显存地址,通过帧缓冲基址加坐标偏移计算目标位置。对于 24bpp RGB 格式也有实现。矩形绘制中尝试了批量写入优化,例如在地址对齐时使用 32 位宽赋值代替逐字节写入。

图形子系统的图层管理。 LayerManager 管理多个 SheetTrait 对象,每个窗口或控件都是一个图层。我实现了双缓冲(enable_2buffer),通过后缓冲 sheet_buffer 减少显存直接操作带来的撕裂和闪烁。光标 Cursor 作为独立图层,可以响应鼠标移动事件并更新位置。此外还实现了简单的 Form 和控件(Label、TextBox),虽然功能简陋,但至少证明了窗口模型的可行性。双缓冲开启后,所有绘图操作先写入后缓冲(系统内存中的颜色数组),定期刷新函数检查“是否需要刷新”标志和“脏矩形”区域,将受影响的矩形区域通过真实的图形输出接口复制到显存。刷新频率由系统时钟控制。脏矩形坐标在刷新前会进行裁剪,防止越界。

图形服务函数运行在单独的任务中,从图形消息队列中取出消息,目前只处理鼠标消息并调用上述鼠标处理函数。这种设计将图形输入与主控制台服务解耦。

目前的局限性有:

  • 光标形状是固定尺寸的字符数组,无法动态加载不同光标样式(如箭头、手型)。
  • 每次鼠标移动或点击都遍历所有图层进行坐标命中测试,图层数量增加时开销线性增长。
  • 层管理器的事件回调仅实现了鼠标拖拽窗口(检测到左键按下时设置移动标志),但未处理窗口释放、边界限制、与其他控件的交互(如按钮点击)

任务管理与调度系统

调度系统是内核的核心组件之一,负责管理进程与线程的生命周期、优先级调度、同步与通信。经过多次重构,当前版本实现了一个 O(1) 优先级的混合调度器,据说是Linux 2.x用的是这个方案,区分实时任务与分时任务,并支持多核基础架构(多核部分尚未启用)。优势是基于位图的快速队列选择: 每个优先级对应一个就绪队列(双向链表)。调度器维护一个 32 位位图,每一位表示对应优先级队列是否非空。选择下一个任务时,通过 __builtin_ctz(数尾零指令)直接找到最高优先级的非空队列,然后取出队首线程。

系统区分 ProcessBlock(进程)与 ThreadBlock(线程)。每个进程至少拥有一个主线程,可额外创建更多线程。进程拥有独立的页表、文件描述符表、堆区边界、TTY 焦点及图形窗口引用;线程则持有运行上下文、栈空间、优先级、时间片及阻塞原因。这种分离使得资源管理更加清晰,也为未来引入多线程应用打下了基础。

系统提供了基于消息的进程间通信原语(msg_send / msg_recv),支持阻塞发送与接收。每个线程可记录等待的发送目标或接收来源,并挂起直到消息到达。

关于M态会不会发生抢占,这个与架构有关。x86是可以的,但是 riscv 的 ecall 好像会自动关闭中断,对此我决定手动打开中断。所以M态是会发生抢占的。

内存管理

内存管理模块承担了物理页分配、内核堆管理、页表映射及分段机制初始化等职责。该模块需要适配多种引导环境(BIOS、GRUB、UEFI)和不同架构(x86 32位、x86_64、RISC‑V),是系统底层的基础设施之一。

系统预留一个覆盖 0 至 4G 物理地址空间的位图.位图结构支持标记页的占用与释放、查询连续空闲页以及设置可用范围。然后实现了一个简单内存池,内核启动后从物理页分配器获取一块连续内存(如 16KB),将其加入内存池作为初始堆空间。后续的 new/malloc 调用均通过默认分配器转发至该内存池。这种设计将内核自身的动态内存需求与物理页管理解耦。

尽管当前内存管理模块尚不完善,它已经成功支持了内核启动、多进程创建、图形缓冲区分配等基本功能。能够正确解析 GRUB 的内存映射并分配出可用的页帧。

用户堆功能当前尚未实现。

消息机制

消息机制是进程间通信与硬件事件通知的核心基础设施。该模块分为两个层次:一是全局系统消息队列,用于处理来自中断的异步事件(键盘、鼠标、定时器、xHCI、帧缓冲刷新);二是基于线程阻塞的同步进程间通信(IPC),支持任意进程间的消息发送与接收,并能够将硬件中断转化为特定线程的消息。

系统维护一个全局的环形队列,专门存放由中断处理程序产生的硬件事件(如鼠标、键盘、定时器、xHCI、刷新)。独立的任务不断从此队列中取出消息并分发给对应的驱动或图形子系统。进程间的普通 IPC 则通过发送与接收系统调用实现,使用另一套基于线程控制块的阻塞队列机制。这种分离保证了硬件事件不会被进程间通信阻塞,降低了中断响应延迟。

同步 IPC 基于线程阻塞与唤醒。 发送线程调用发送接口时,若接收线程当前正阻塞在接收接口上且匹配发送者或允许任意来源,则立即复制消息数据并唤醒接收线程;否则发送线程自身被阻塞,挂入接收线程的发送等待队列,同时标记阻塞原因为“等待发送”,并记录目标线程。接收线程调用接收接口时,若其发送队列非空且与指定的发送者匹配,则取出队首消息,复制数据后唤醒发送线程;否则接收线程阻塞,标记“等待接收”,记录期望的发送者。这种设计实现了内核中的同步消息传递,无需额外的共享内存。由于发送方和接收方可能位于不同的进程(拥有不同的页表),消息数据不能直接用指针访问。模块提供了跨页表复制函数,根据两个进程的页表将源虚拟地址转换为物理地址(或直接映射地址),再复制到目标虚拟地址。该函数在 RISC-V 与 x86 上分别适配了分页机制,并支持降级到内核页表。

发送接口在阻塞前会检测发送等待链中是否存在循环等待(例如 A 等待 B,B 等待 C,C 等待 A)。若检测到潜在死锁,函数返回错误而不阻塞,避免了系统挂死。虽然检测算法简单(仅遍历单链),但对于单核场景下有限的通信深度已足够。

文件管理与虚拟文件系统

文件管理模块构建了一个虚拟文件系统(VFS)层,用于统一管理不同底层文件系统,并为进程提供标准的文件操作接口(打开、读写、关闭、删除等)。

系统维护一个全局根目录项,所有文件和子目录均以双向链表形式组织在父目录项之下。路径解析时通过线性查找子目录项,命中则直接返回,未命中时触发底层文件系统的搜索回调,并在返回后动态创建新的目录项和索引节点加入缓存。这种“按需建立”的缓存策略减少了内存占用。

系统实现了一个特殊的设备文件系统,并默认挂载在 /dev 目录下。该文件系统在初始化时自动创建 /dev/tty0 至 /dev/tty3 四个设备节点,其内部处理字段存储 TTY 索引号。读写操作通过 IPC 消息转发给对应的 TTY 服务进程,将文件接口转换为进程间通信。

目前只适配了 FAT12/16/32 几种文件系统。

系统调用与处理程序

采用了一种通用简单的设计方案,限制是每个系统调用的参数数量不能大于3.

支持 INT/GATE/SYSCALL/ECALL 指令导致的系统调用。

End

我的操作系统开发之旅才刚刚开始,未来还有很长的路要走。从最初对Rust内存安全性的懵懂,到如今能在三种架构间对比分页与陷入行为,这五次训练营的经历让我逐渐摸到了系统编程的门槛。然而,每一次深入都揭示出更多未知的领域——异步运行时在内核中的整合、多核间的缓存一致性协议、实时调度算法的确定性验证,这些课题仍在远方等待。RISC-V的开源特性降低了实验门槛,但生态的成熟度尚需时日;x86的庞大兼容层虽令人敬畏,却也暴露出历史设计的沉重代价。前方的道路并非坦途,但正因如此,每一次编译通过、每一个页表正确遍历、每一次异常被精准捕获,都成为继续前行的理由。或许,操作系统的魅力就在于它永远无法被彻底“完成”

Dosconio,
21, April 2026.

前言

这是我第二次报名这个训练营但是相当于是第一次参加了,因为第一次报名是在2024年的秋冬季,那时候我刚大一刚刚接触到计算机,感觉很有意思就报名了但是那时候连使用Github对我来说都极其困难且刚进入大学没有适应繁多的课程,
我便决定先放弃,潜修一年有了些许微波的基础后再来试一试。我总是喜欢去了解一些东西的底层,所以我对操作系统算是有一总执念吧,让我再次来到这里。但由于本人比较愚笨,学习速度极慢,再加上早八到晚十的课程,所以进度相较于其他优秀学院慢了许多,但贵在坚持嘛……

Read more »

lab1: print_with_color

  • 本实验比较较为简单, 修改 arceos/exercises/print_with_color/src/main.rs 文件, 为 println 添加 ANSI 转义码即可. ANSI 转义码用于在终端中控制光标位置、文本颜色、文本样式等, 其以 ESC (Escape) 字符开头.
  • 初次在 Rust 中使用 \033[34m[WithColor]: Hello, Arceos!\033[0m 形式的 ANSI 转义码, 发现其不奏效; 但是在 C/C++ 中却是正常. 查阅资料后得知, \033 是 C 语言的传统写法, 在 C/C++ 中使用较多. 在 Rust 中, 应该使用 \x1b, 即 \x1b[34m[WithColor]: Hello, Arceos!\x1b[0m. 本是上来说, \033\x1b 是同一内容的不同表示.

lab2: support_hashmap

  • 本实验直接使用 hashbrown 来支持 hashmap. 在 arceos/ulib/axstd/Cargo.toml 中添加 hashbrown 依赖, 在 axstd 中添加 collections 模块, 并在 arceos/ulib/axstd/Cargo.toml 中声明 pub mod collections;. 在 collections 模块中, 使用 pub use alloc::collections::*;, 将 alloc::collections 的内容(比如 BTreeMap)导入当前的模块, 使用 pub use hashbrown::*; 将第三方库的 HashMap 等导入当前模块, 此时可以通过 axstd::collections 使用 HashMap(hashbrown 提供)、BTreeMap(alloc::collections 提供).
  • 还有一点要注意, 应该在启用 alloc feature 时才会声明并挂载 collections (#[cfg(feature = "alloc")]), 若不加以控制, print_with_color 则会出现报错. 原因在于, 若 collections 的编译不被 alloc feature 控制, 其中的 BTreeMap 等会依赖全局内存分配器, 但是因没有启用 alloc feature, 故而找不到全局内存分配器. 应该启用 alloc feature 时才会编译 collections.

lab3: alt_alloc

  • 此实验是实现简单的内存分配器, 在 arceos/modules/alt_axalloc/src/lib.rs 中具体实现.
  • arceos/exercises/alt_alloc/Cargo.toml 声明依赖 axstd: workspace = true 表明 axstd 是工作区成员; features = ["alt_alloc"] 使 axstd 开启 alt_alloc 特性. 当 axstdalt_alloc 特性被启用时, 其会继续将此传递下去, 还会根据 [features]alt_alloc = ["arceos_api/alt_alloc", "axfeat/alt_alloc"] 启用 arceos_api 中的 alt_alloc 特性和 axfeat 中的 alt_alloc 特性.

lab4: ramfs_rename

  • 实验要求在 arceos/axfs_ramfs 中实现 rename 操作, 但 arceos 原本依赖远程仓库的版本, 故需要调整.
  • 可以采用 patch 方式让工程临时使用 arceos/axfs_ramfs 的本地组件仓库. 为方便起见, 直接在 arceos/exercises/ramfs_rename/Cargo.toml 中将 axfs_ramfs = { version = "0.1", optional = true } 中的 version = "0.1" 替换为 workspace = true, 多次运行发现还是提示 Operation not supported. 然后对代码进行详细分析, 发现应用 ramfs_rename 会依赖 axfs(间接) 和 axfs_ramfs, axfs 还会依赖 axfs_ramfs, 此时 ramfs_rename 依赖的 axfs_ramfsaxfs 依赖的不同. 故还需将修改 arceos/modules/axfs/Cargo.toml, 替换 version = "0.1"workspace = true.
  • 具体实现 rename 功能, 只需修改 arceos/axfs_ramfs/src/dir.rs. 阅读测试用例后发现, 文件重命名只在同一个目录下操作, 故在具体实现中只考虑此情况, 并未做特殊处理.

lab5: sys_map

  • 初次运行 sys_map 时, 发现运行结果与课件中展示的内容有很大出入, 课件显示 not implement, 而本地运行则会显示 Cannot load app!. 经排查, 发现未设置 musl-libc, 导致无法编译出 mapfile 可执行文件.
  • 实现 sys_map 时针对测试用例做了简化处理, 未考虑复杂的情况. 实现 sys_mmap 系统调用主要有 5 个步骤: 获取当前进程的地址空间, 并在此查找空闲内存区域, 再准备参数执行内存映射, 再读取文件内容到缓冲区 buf 并将其写入映射的内存区域, 最后返回映射起始位置. 实现过程中, 务必查找空闲的内存区域再映射, 否则会出现 Mapping error: AlreadyExists.

lab6: simple_hv

  • 运行此实验代码, 发现QEMU会卡死. 经过仔细排查, 是由于 payload 构建失败导致的. 在外层 Makefile 中定义的 RUSTFLAGS 会被默认传递给 make -C ./payload 递归调用, 这会干扰编译 payload (尤其是 skernel). 故在 payloadMakefile 中添加 unexport RUSTFLAGS 来避免 RUSTFLAGS 变量的传播.
  • 正常运行后, 根据 panic 指示修改实现, 使测例通过即可. 在异常处理分支中, panic 改为 ax_println, 还要调整客户机的 spec, 即 ctx.guest_regs.sepc += 4;. 注意, 还要设置 A0 寄存器的值为 0x6688, A1 寄存器的值为 0x1234.

对 rcore 早有耳闻,曾报名了 23 年秋冬的训练营,但是当前初学 rust、初学 riscv 等缺少了一种毅力,25 年秋冬重新报名参加,有所学、有所获。

虽然我是虚拟化方向的研究生,但是对于操作系统中的很多概念等停留在理论阶段,尤其在接触到了 rcore 之后,发现本科学的操作系统只是浮在表面的那一层,如今接触 rcore,做了一些实验,进一步加深了对操作系统的敬仰。

虽然走到了这里,但是对于 rcore 的很多非实验的部分的理解还不够,还需要好好地梳理,这里对 stage-3 arceos 实验进行总结。

前言

实验涉及 unikernel、宏内核、hypervisor,但是明显感觉到 stage3 实验更多得是对组件化操作系统的上手。

其中很多实验并不像 rcore 中的那样复杂,比如 print_color、simple_hv,可能一点点代码就能通过测例,更重要的是理解其组件化的思想,可以快速构建异构内核的能力,unikernel、宏内核、hypervisor,把他们的共同点抽离出来,封装成一个个组件,可以实现快速的内核定制。

下面说一下遇到的一些问题:

hashmap

起初,我通过 hashbrown 引入第三方的实现,发现并未涉及视频中提到的随机数,通过查找资料以及翻阅 std 下的 hash_map 实现。最终决定使用 hashbrown 下的 HashMap::with_hasher,并选择 foldhash 下的 FixedState::with_seed 提供一个哈希计算,其中 seed 通过 axhal 下的 random 生成。

ramfs_rename

最开始,发现怎么改动都不生效,arceos 的实验使用了 workspace,这个在之前我从未接触,最终发现是 ramfs_rename 对应的这个内核的 Cargo.toml 文件依赖的不是 workspace 中的 axfs_ramfs 导致。
除此之外,还有修改 axfs 下的 Cargo.toml。

sys_mmap

从文件中映射到内存中,分三步走,在虚拟地址空间找一个空闲虚拟页,借助 uspace.find_free_area,借助 uspace.map_alloc 进行预填充的分配,即完成虚实映射,然后将文件读到改页。
如果使用刚刚分配的虚拟地址可以读,但是会出现异常,比如权限不足,需要转到内核虚拟地址进行读写,这里通过 phys_to_virt 将物理地址转到内核到虚拟地址,随后通过 sys_read 这个 api 完成文件的读取。

其他

自我感觉对于 rcore 的掌握还不到家,目前仅仅是通过了测例而已。

rcore 的文档和代码常读常新,能加深操作系统的理解。

引言

听闻rCore是基于Rust实现的操作系统内核,我对此产生了兴趣并报名参加了训练营。经过三个阶段的系统学习,作为Rust语言的初学者,我掌握了Rust的基础语法特性,并深入复习了操作系统的相关概念。本文将记录我在完成第三阶段ArceOS的学习过程中,对几个较为感兴趣部分的实现理解与思考。

rename

ArceOS的虚拟文件系统(VFS)中rename功能的设计充分体现了职责分离和安全性的设计理念。该部分的核心思路是VFS层通过最长匹配原则 (find_fs_for_path) 确定路径所属的文件系统,并妥善处理跨文件系统重命名和挂载点重命名等边界情况。

VFS在此充当调度者的角色,负责将请求转发给正确的底层文件系统。而底层文件系统 (如ramfs) 则负责执行实际的重命名操作,通过Rust的读写锁机制确保重命名操作以及覆盖情况的原子性和并发安全性,这种分层设计不仅职责清晰,而且使VFS具有良好的灵活性和可扩展性。

mmap

sys_mmap实现了Linux风格的内存映射机制,其核心流程围绕地址空间管理和文件/匿名映射展开,首先检查映射长度是否为零,并将长度强制对齐到4KB页大小。若指定了addr参数则要求其必须是页对齐的。

当设置MAP_FIXED标志时,系统必须使用指定的固定地址,并校验该地址是否在合法的地址空间范围内。否则系统会在进程的地址空间中查找一块足够大的空闲区域 (通过find_free_area函数) 作为映射的起始虚拟地址。

对于匿名映射MAP_ANONYMOUS,系统仅在地址空间中分配并映射物理页,不涉及文件操作。对于文件映射,系统先执行map_alloc建立映射关系,然后通过sys_lseek调整文件偏移量,接着同步读取部分数据到缓冲区,最后通过aspace.write将数据写入新映射的虚拟地址,完成初始数据的填充。

整个设计的重点在于利用地址空间aspace抽象实现统一的虚拟内存管理和权限控制,这让我深刻理解了操作系统中抽象层的重要性。

Hypervisor

Hypervisor位于操作系统和物理机之间,将CPU、内存、磁盘、网卡等硬件资源切分成独立份额,再封装成多台虚拟计算机,每个虚拟机运行各自的操作系统和应用程序,彼此之间互不干扰。

在ArceOS的simple_hv练习中,需要实现了一个最小化的RISC-V虚拟机监视器,核心逻辑集中在vmexit_handler函数中,当客户机触发异常或SBI调用时,CPU自动陷入该函数,由宿主机快速处理并模拟所需功能。

VirtualSupervisorEnvCall是SBI调用的集中分发点,处理客户机OS的各种请求,包括printf输出、关机请求、时钟设置和SBI扩展探测等。其中putchar实时回显到宿主终端、SetTimer重写stimecmp寄存器,而SRST_SHUTDOWN在打印”Shutdown vm normally!”后停机。

整套代码虽然不足百行,却完整串联了异常捕获→寄存器改写→SBI模拟→时钟注入→正常关机的hypervisor核心流程,为学习更深层次的多核虚拟化、扩展页表(EPT)、IOMMU等高级虚拟化技术打下了基础。

引言

这篇博文记录了我在 LearningOS ArceOS 训练仓库中的调试历程:从基础的 ramfs_renamealt_alloc 练习,到让 CI 里的 simple_hv 能稳定加载 skernel2,再到阶段性验收脚本 verify_lab1.sh。过程中频繁与 GitHub Actions 环境差异、虚拟机退出机制等问题斗智斗勇,现总结如下。

实验环境

  • 仓库与分支:LearningOS/2025a-arceos-Shl1015CS 的 main(练习)与 lab1(挑战)
  • 宿主机:Ubuntu 22.04,rustup stable toolchain
  • QEMUqemu-system-riscv64

任务拆解与进展

1. print_with_color:确认基础输出能力

  • 目标:让 print_with_color 示例在串口中输出带 ANSI 颜色的 “Hello, Arceos!”。
  • 方法:直接在 main.rs 中使用 println!("\x1b[1;31m..."),并确保 axstd 打印宏可用。
  • 测试:运行 ./scripts/test-print.sh,脚本通过检测转义序列与纯文本,判断是否既有颜色又有正确文案。

2. support_hashmap:验证 alloc+集合

  • 目标:在内核环境下跑 support_hashmap 练习,完成 Memory tests run OK!
  • 关键点:提供稳定的堆内存来源,让 alloc::collections::HashMap 能频繁插入/删除。
  • 测试:./scripts/test-support_hashmap.sh 构建磁盘镜像、运行练习并抓取最后一行输出,确保内存测试通过。

3. sys_map:实现用户态 mmap

  • 目标:补全 exercises/sys_map 的系统调用接口,使用户态程序能够通过 mmap 映射文件,并读回 “hello, arceos!”。
  • 难点:
    • 在内核侧为匿名/文件映射分配虚拟地址,维护 VmArea 元数据;
    • fd 指向磁盘文件时,需将 payload(payload/mapfile_c/mapfile)写入 disk.img 并供客体访问;
    • 处理 munmap 与页对齐要求。
  • 测试:./scripts/test-sys_map.sh 会先执行 make payload && ./update_disk.sh ...,再以 BLK=y 运行练习并搜索 “Read back content: hello, arceos!”。

4. ramfs_rename:递归重命名与特殊路径

  • axfs_ramfsDirNode::rename 仅支持同级结点,需要实现跨层级、处理 . .. 的逻辑。
  • 解决方案:在 modules/axfs/src/root.rs 中实现“复制 + 删除”策略,保证旧结点被正确删除并保留内容。
  • 同时排查 impl_vfs_dir_default! 宏覆盖自定义实现的问题,最后确认修改入口正确。

5. alt_alloc:早期堆分配器

  • 要求实现 modules/alt_axalloc/src/bump_allocator.rs 中的 EarlyAllocator
  • 重点在于字节对齐、页分配接口实现与统计函数(total_bytes/used_bytes/available_bytes)。
  • 通过 cargo test -p alt_axalloc./scripts/test-alt_alloc.sh 验证,确保早期分配器可用于内核初始化阶段。

6. simple_hv:CI 环境无法加载 skernel2

这是整个阶段最耗时的部分,核心需求:让 GitHub Actions 中的 ./scripts/test-simple_hv.sh 通过。

7. verify_lab1.sh:阶段验收

  • 命令:cd arceos && ./verify_lab1.sh | tee tmpa.txt
  • 该脚本会重新生成镜像、构建 labs/lab1,并在 QEMU 中运行效验。
  • 当前阻塞点:lab_allocator 中的 LabByteAllocator 尚未实现,导致 panicked at labs/lab_allocator/src/lib.rs:20:9。后续计划完善 bump allocator 以便完成 lab1。

第一次参加操作系统的训练营,确实学到了很多之前在书本上没有学到的知识,也锻炼了自己的动手能力。

rustlings

主要是语法学习,因为之前学过rust的基础语法,所以这次简单复习一遍就很容易通过了,不过增加的数据结构和算法题还是有一定难度,需要多多debug。

rcore

rcore其实和本科的教材讲的操作系统其实是一回事,只是用rust实现。所以看文档就感觉很亲切,练习题也比较简单。

arceos

由于之前没有接触过组件化操作系统,第一次看到还是有许多不理解的地方。不过教学视频里面的图示都很清晰,对照着老师的教学也很容易听懂。练习题主要是要多使用gdb,搞清楚各个功能的函数调用链,功能本身的实现倒不是很复杂。

挑战题目

目前刚好处在论文开题的阶段,由于时间原因还没来得及做,希望能在截至日前抽出时间尝试一下。

学习心得

二阶段 rcore

再一次参加了OS训练营,从第一次的跌跌撞撞到第二次的懵懵懂懂再到第三次的游刃有余,操作系统虽然很难,但也正是因为困难才有意思。

整个内核的调用关系在心里更清晰了,在实现需求的同时,也力求代码的整洁和可读性。

  1. ch3 user通过syscall调用内核,本质上就是对目标的物理地址进行赋值,把不安全的操作放到内核来做
  2. ch4 加入了MMU,user拿到的地址就不是物理地址了,需要进行一些转换。在分配和释放内存的时候也就是修改页表的一个过程,和修改数据库没什么区别。
  3. ch5 实现了简单的进程调度算法,从FIFO变成了stride 调度算法,也把fork 和 exec拼一起形成了spawn。
  4. ch6 对inode进行操作,实际上各种方法都被包装的很好了,调用关系也很清晰。
  5. ch8 这里弄清楚创建线程的位置和那些变量的增减时机一切都迎刃而解。

三阶段arceos

代码量总体更少,确定了实现范围后很快就可以完成。

在实验上

  1. [print_with_color] 可以很好的熟悉调用关系
  2. [alt_alloc_bump] 一个bump内存分配算法 如果有时间可以尝试一下挑战题
  3. [ramfs_rename] 相比于二阶段这里的node抽象程度更高,老节点挂在新路径上即可实现
  4. [hashmap] 实现collections中的hashmap的替换,可以自己新建一个collection,在里面实现hashmap
  5. [sys_map] 三阶段代码量最大的一道题,不过很多方法和调用都在main中用过了,先拿到用户栈的地址,在里面找一块内存,之后把程序读入缓冲区并写到目标位置
  6. [simple_hv] 处理一下未处理的异常,之后执行下一条命令

总结

操作系统是一个很神奇的东西,每一次学习都有新的收获,在里面能找到很多数据库,web开发共通的思想。操作系统导论也是一本很好的书,虽然我还没看完内存部分,但是通过结合实践,理解的更深刻了。

学习操作系统就像爬山,每一步都需要扎实,每一步都很困难,但每一步都离顶峰更近,踏一步就有一步的收获。