0%

序言

过去一个月,由于我对 ArceOS 的架构理解较少,为了快速掌握 ArceOS 的架构,Unikernel宏内核,我的主要目标改进 oscamp,为其完善对 x86_64 的支持。同时还做了对内核组件 x86_rtc 说明文档和测试的完善。
最直接的收获有两点:

  • Unikernel 思想 —— “用户态就是内核的一部分” 的最小可信边界,让我重新审视传统多进程操作系统里“内核/用户”硬隔离的成本。
  • 宏内核工程学 —— 模块划分、内核线程、系统调用网关、设备驱动归一化,这些都在 Starry/ArceOS 的设计里有了“先行者版本”。

工作记录

第一周

为了快速掌握 ArceOS 的架构,Unikernel宏内核,我选择了改进 oscamp,为其完善对 x86_64 的支持这一项工作。并开始了对 x86_64 的学习。

第二、三周

主要是做代码工作,以下是一些总结
RISC‑V 是通过 scause + stval;x86‑64 要区分 Exception Class(#PF/#UD)与 IRQ Vector,且栈布局不一样。
x86_64 的支持这一项工作需要完成以下的功能实现:

  1. 改进 context.rs,保存相关的寄存器,并完善 context_switchenter_uspace
    • 保存/恢复的寄存器集对齐 SysV x86‑64 调用约定:RBX RBP R12‑R15 + CR3 + RFLAGSFPU/AVX 延后到 lazy fp 任务。
    • context_switch(old, new) = 保存旧任务栈顶 → 恢复新任务栈顶 → iretq;为支持 SMP,加了 core::arch::asm!("swapgs") 保证每 CPU 的 GS 基址切换。
    • 进入用户态 (enter_uspace):手动构造 iretq 帧:SS|RSP|RFLAGS|CS|RIP,再写 CR3 = user_pml4; 关中断→加载帧→开中断→ iretq
  2. 改进 trap.S
    • IDT 256 项0x20 时钟、0x80 软中断、0x0E #PF……全部指向统一的 trap_entry;硬中断通过 APIC 自动切到 IST[0] emergency stack 防止内核栈溢出。
    • trap.rs 根据向量号派发到 handle_page_fault / handle_irq / handle_syscall
  3. 改进 syscall.rssyscall.S
    • SYSCALL/SYSRET 而非 INT 0x80;入口先 swapgs 用 GS 保存/恢复用户栈。
    • 按 SysV ABI RAX=nr,RDI RSI RDX R10 R8 R9 传六参 —— 在汇编里把寄存器序列化到栈,统一传给 x86_syscall_handler()
    • 退出路径:恢复通用寄存器 → swapgssysretq

第四周

对内核组件 x86_rtc 说明文档和测试的完善。
repo

未来展望

  • Transparent HugePages:复用前期完成的巨页 API,引入 khugepaged 合并线程。
  • vDSO:把高频 clock_gettime 胶水放到用户态,加速 Sys‑API。

第一阶段 rust基础

rustlings比较顺利,已关注rust好几年了,也一直跟进rust的发展和变化,用了大概几天的时间突击完成

第二阶段 rcore

这是我第一次接触操作系统,印象还都停留在原来的书本上,概念上,都是理论,没有真正接触操作系统是什么样的,所以,这个机会能让我了解探究操作系统内部真正的原理和运行逻辑,解开我心中的多年的疑惑,很是开心到起飞。

总体印象最深的就是操作系统内核是以什么形态存在的,上下文如何切换,进程空间如何形成,页表是如何实现的,跳板页又是怎么回事

第三阶段 ArceOS

这个和我预期不一样,没有沿着rcore继续走,这是一个全新的设计,一时间,有点慌乱,不能和rcore相关的思路很自然的顺承下来,显然这个更具有前瞻性,为此我也花了好多时间来梳理这个逻辑和rcore都关联起来。这个模块化设计的理念很突出,相互之间的关联及细节,需要仔细的研读和体会。

第三阶段的具体case

color

对println!() 把颜色表示直接加入后,可以显示颜色,
如在当前文件写了一个宏print_with_color!,让后让println!去去调用此宏,就得不到正确结果,提示找不到这个宏,查了资料,和 $crate有关,宏的暴露方式有关,引用路径,由于时间关系,后面调研

HashMap

开始的时候,从rust官方移植,这个太痛苦了,依赖太多了,最后放弃。
自己写个简单的,只是利用了官方的
use core::hash::Hasher;
use core::hash::Hash;
这两个hash算法,主体采用了最简单的线性插入算法

bumpallocator

需要仔细理解题意和上下文,思路选用:
申请一直从可用空间起始向前申请,
释放如果全部的空间都释放了,就把下一个可用空间调整到开始

rename

采用递归的方式,一层一层的先找到所在当前目录
增加当前目录的,更改命名的方法,查找相应的文件,删除后,再插入一个,因为当前存储使用的是BtreeMap,不能更改index

hv

这个耗费了我好多时间,主要是因为运行例程会卡住,后来发现可能是qemu的版本问题,升级到9.2后,还是一样卡住,当时环境为windows11,wsl2,卡在这里,不同的情况卡的还不一样:

1
Write file 'payload/skernel2/skernel2' into disk.img

最后没有办法,找一台空闲机器,实在不行,我就安装裸机linux系统,所以先尝试装了另一种虚拟机,virtualbox7.18,也花了些时间,配置好环境后,默认的qemu为6.x时,还是会出错,不过不是卡主的问题,是会触发异常访问,升级到9.2.x后,按照预期执行了,难道说,wsl2在某些情况就是不行,我真是难过。

之后遇到提交github,出错,musl.cc被block,

下载不下来:
wget https://musl.cc/riscv64-linux-musl-cross.tgz

下载不下来,终于等来了替代方案

wget https://musl.cc.timfish.dev/riscv64-linux-musl-cross.tgz
最后,终于得以解决

sys_mmap

参数和返回值我理解出现偏差,

1
2
3
4
5
6
7
8
fn sys_mmap(
addr: *mut usize,
length: usize,
prot: i32,
flags: i32,
fd: i32,
_offset: isize,
) -> isize

参数addr为0时要特别注意,需要寻找一空间,还有地址和size的对齐。返回值,当开始时认为0是成功,为负时,返回失败原因,后来再三确认失败返回0,成功返回地址,isize作为地址返回,有点不符合直觉?

Next

期待第四阶段

前三阶段总结

主要收获

在第四阶段中,更多的时间留给了自由探索.虽然起初缺少具体的目标有些令人摸不着头脑,不过跟随老师的引导,也一步步确立了整个阶段的目标:基于 uring^1 机制实现异步系统调用.尽管最后只实现了基于 uring 的异步 IPC 机制,一路上走来也有许多收获.

Rust 的异步机制

虽然有一些 Rust 异步编程经验,但尚未从更底层的角度了解过 Rust 的异步模型.在第一周中,通过动手实现一个简易的异步运行时 local-executor,认识到 Rust 的异步原语 Future 是如何与运行时交互的.以及深入到内存安全层面上,了解了 Rust 中如何通过 pin 语义来巧妙的保证自引用结构体的安全性.尽管这并不是一个完美的模型——几乎所有涉及到 pin 语义的数据结构和函数都需要 unsafe 代码,而这些 unsafe 代码所需的诸多安全性保证又着实有些令人头大.因为,Rust 提供了静态安全性,而编译器会基于这些安全保证进行比较“激进”的优化.所以,Rust 中的 unsafe 要比其他生来便“不安全”的语言更加不安全,对开发者的要求也更高.

在第三周的探索中,又了解到一个之前从未考虑过的问题——Future 的终止安全性^2.而这对于实现基于共享内存的异步通信机制来说尤其关键,稍有不慎就会引发难以察觉的漏洞.在后来着手实现异步通信机制的时候,又对这个问题进行了更深入的思考,并在现有方案的基础上提出了另外几个可行的思路

原子类型和内存排序

尽管曾了解过原子类型和内存排序相关的知识,但从未真正彻底搞懂过,直到在第二周的探索中发现了一本优秀的电子书 Rust Atomics and Locks^3.这本书从抽象的并发模型深入到具体的硬件细节,比较全面的介绍了几种原子操作和内存排序的设计初衷以及对应的汇编层面实现.结合这本书和自己的思考,又经过悉心整理最终形成了一篇比较详实的学习笔记.尽管在实践时还不能完全掌握各种内存排序的选择,通过翻看笔记以及参考相似场景下现有项目的做法,也都能找到一个安全正确的选项.

基于 uring 的异步通信

经过两周的调查和学习,最终在第三周完成了基于 uring 的异步通信框架 evering,同时利用 GitHub Pages 部署了它详细的设计文档

evering 最重要的两个数据结构是用来管理消息队列的 Uring 和用来管理操作生命周期的 DriverUring 的实现借鉴了 io_uring 的做法^4,但结合 Rust 的特性做了一些简化.比如,io_uring 支持 IOSQE_IO_LINK 来要求响应侧顺序处理请求.而在 Rust 中,每个异步请求都被封装为 Future,故可以利用 .await 来在请求侧实现顺序请求.Driver 的实现则借鉴了 tokio-uringringbahn.但相比后两者,evering 提供了更灵活、通用的异步操作管理机制.

不过,目前 evering 相对简陋,仅支持 SPSC,因此请求侧或响应侧只能在单线程上使用.也许未来可以实现 MPSC 的队列,以便于更好的与现有的异步生态(比如 tokio)兼容.

基于 evering 的异步 IPC

经过三周的铺垫,第四周正式开始实践跨进程的异步通信.在第三周中,基于 evering 实现了简易的跨线程异步通信 evering-threaded,而对跨进程来说,主要的难点就是内存的共享.好在 Linux 提供了易于使用的共享内存接口,基于 shm_open(3)memfd_create(2)mmap(2) 可以轻松在不同进程之间建立共享内存区.而 ftruncate(3p) 配合缺页延迟加载机制,使得程序启动后仅需一次初始化就能配置好可用的共享内存区间.不过,目前 evering 只能做到基础的“对拍式”的通信方式.而近期字节跳动开源的 shmipc 则是一个相对成熟、全面的异步通信框架,这对未来 evering 的改进提供了方向.

基于 evering 的异步系统调用

由于时间相对仓促,加之备研要占用大量的时间,遗憾的是,在第四阶段并没有完成最初的目标——实现基于 uring 的异步系统调用.与 用户线程 <-> 用户线程 的通信相比,用户线程 <-> 内核线程 的通信要额外处理内核任务的调度和用户进程的生命周期管理.即如何处理多个不同用户进程的请求,以及用户进程意外退出后对应内核任务的清理.而就共享内存而言,由于用户对内核单向透明,这看起来似乎比 IPC 的共享内存更容易解决.

用户态线程与协程的调度

去年的夏令营中,embassy-preempt 实现了内核中线程和协程的混合调度.那么用户态的协程能否被内核混合调度呢?在实现异步系统调用的前提下,当用户态线程由于内核尚未完成调用处理而让权(通过 sched_yield(2) 等系统调用)时,实际上,内核可以获知该线程应何时被唤醒.这就与 Rust 协程中的 Waker 机制非常相似,而用户态的让权又与 .await 很类似.基于这些,那么可以将一个实现异步系统调用的用户线程转换为一个用户协程.此后,内核就充当了这个协程的运行时和调度器的角色.

而相比用户态的线程,使用协程的一个显著优点是,对用户任务的唤醒实际上相当于执行一次 Future::poll.这意味着,当用户主动让权时,它不需要保存任何上下文——用户任务的唤醒本质上变成了函数调用,而主动让权表示该函数的返回.如此便能够进一步减少用户和内核切换的开销,以及系统中所需执行栈的数量.当然,当用户协程被抢占时,它便回退成了类似线程的有独立执行栈和上下文的存在.

总结

经过近两个月的学习,对操作系统和异步编程的许多方面都有了一些相对清晰的认知.非常感谢夏令营中各位老师的付出和历届同学的努力,学习的过程中让我切身的感受到操作系统发展到现在那段波澜壮阔的历史,以及在不断推陈出新的技术潮流中一点微不足道的参与感.尽管最后没能完成目标有些遗憾,不过,这也为将来再次参加夏令营留下了充足的理由 :P

序言

非常高兴能参加到开源操作系统训练营第四阶段的学习,跟大家一起进步。经历完这四个阶段,自己有非常大的收获,让我理解了操作系统内部的运行机制,通过组件化的管理来实现更现代化的操作系统,最终在操作系统中支持异步机制提高操作系统的性能。

在每周的学习过程中,非常感谢向勇老师给与了很多的指导和鼓励,成为了我前进的支柱。同时也要感谢周积萍学长给与了很多帮助和支持以及建设性的意见,让我遇到困难时不迷茫。在第四阶段的过程中,也从其他同学那学到了很多,能便捷的获取到学习资料和代码,当有疑惑了也有人能够理解你,跟你一起深入到技术中进行讨论,那种感觉真是舒服。

每周工作

对于一个程序员来说,时间总是不够的,在四周时间里,我主要做了以下几个方面的工作:

第一周 - 回顾

当我第一周(2025-06-01)加入训练营的时候,发现去年的学长大佬们已经早早的开始了自己的课题。我就从基础慢慢开始,这周主要是把文档中的例会视频都看了一遍,基本了解了协程异步的作用以及目前是如何把异步应用到操作系统内核当中去的。

第二周 - 定目标

在看例会视频的时候,我发现有学长讨论 rust 异步的函数着色问题,碰巧我之前在写 rust 的时候也遇到了这个问题,很多时候一套逻辑代码,要分别实现一个同步版本一个异步版本,从一些学长的代码中看确实是分开写的,比如赵方亮学长的仓库,在 2025-06-07 例会讨论中,有同学在参加 “大学生操作系统大赛” 的时候也遇到了此问题,最近在学习 zig 这门语言,说是能解决这个问题,我便想借着这个机会深入研究下。也是对于函数着色问题的一些自己的尝试,于是便确立了如下目标:

  • 长期目标:实现低侵入式的异步协程框架,服务于操作系统内核
  • 本期目标:实现简单的异步协程运行时 (zig)

后续在调研的过程中,发现 rust 的异步机制,是基于 Future 来实现的,这是一种无栈协程,跟我之前理解的 Go 语言的那种有栈协程还不一样。

第三周 - 学习和实验

后续的两个周,主要是学习文档中的异步协程资料,编写实验代码,验证自己的想法。

第四周 - 代码结合

  • 异步协程运行时 xasync 框架代码编写
    • 基本完成跑通简单测例
    • 同时初步解决了函数着色问题

完成上述四周的工作后,基本实现我在开源操作系统训练营本阶段的目标,符合预期。

xasync 异步协程运行时

使用者角度

我在设计 xasync 异步协程的时候,借鉴了 zig 协程 的设计思路,感觉 zig 协程更容易让使用者理解和减轻负担,那么从使用者的角度出发,什么样的协程用起来才是最舒服的,我认为尽量保持一套代码,只通过一些简单的标记就可以实现同步和异步的切换,是更加友好的协程框架实现方式。下面是我理解的伪代码。

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
var is_async = true // 如果关闭后,底层会走阻塞逻辑

fn read(file) {
if (is_async) {
scheudle(future:run(sys_read(file))) // 生成 future,交给 executor 和 eventloop 调度处理
suspend()
} else {
sys_read(file)
}
}

fn long_time_action() {
read("large file")
sleep(100)
}

fn other_action() {

}

fn main() {
let frame = xasync(long_time_action) // 使用者也可以用 xasync 来标记上层代码是异步的
// xawait(frame) // 需要等待的时候才等待

other_action()
}

通过上面的注释,可以仔细看下调用流程,这是我个人期望的协程框架使用方式的理解。

架构设计

总体设计图

上面是架构设计图,分为前后两部分把 有栈协程无栈协程 结合起来,其中红线理解为前进 蓝线理解为返回,比方说协程切换的前进返回、Future poll 前进和状态返回、协程调度的前进和唤醒的返回等。

目前图中描述的是有三个协程(绿色线程)在需要的场景下不断让出执行权,在异步任务结束后能随时切换到具体的任务上继续执行。这种机制在需要等待返回结果的情况下尤为重要。后续会优化成协程池方便使用。

下面从测例的角度简单剖析下实现代码和原理。

Future

测例代码

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

const Counter = struct {
const Self = @This();
num: u32,
max: u32,

fn init(num: u32, max: u32) Self {
return .{
.num = num,
.max = max,
};
}

fn doCount(ctx: *Context) Result {
const counter = @as(*Counter, @ptrCast(@alignCast(ctx.payload)));
if (counter.num < counter.max) {
std.debug.print("counter num = {}\n", .{counter.num});
counter.num += 1;
return .wait;
} else {
return .{ .done = &counter.num };
}
}

fn doNextCount(result: ?*anyopaque, ctx: *Context) *Future {
var counter = @as(*Counter, @ptrCast(@alignCast(ctx.payload)));

const num = @as(*u32, @ptrCast(@alignCast(result)));
const value = num.*;

counter.num = 0;
counter.max = value + 5;

return run(Counter.doCount, counter);
}
};

test "counter-chain-done" {
const allocator = std.testing.allocator;

var executor = Executor.init(allocator);
defer executor.deinit();

var counter = Counter.init(0, 5);
const fut = runWithAllocator(allocator, Counter.doCount, &counter).chain(Counter.printNum); // 这里支持链式调用

executor.schedule(fut);

executor.run();
}

上面的代码是把一个 Counter 计数器,改成了异步机制,当 num > max 的时候才会终止运行。在实现的时候利用 zig uinon(enum) 的特性,尽量做到了零成本抽象。

支持组合

还支持了 Then 组合操作,因为在封装 Future 代码的时候可能要把原有的阻塞代码拆成多个 Future 逐步执行。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
test "counter-chain-counter" {
const allocator = std.testing.allocator;

var executor = Executor.init(allocator);
defer executor.deinit();

var counter = Counter.init(0, 5);
const fut = runWithAllocator(allocator, Counter.doCount, &counter).chain(Counter.doNextCount); // 这里支持链式调用

executor.schedule(fut);

executor.run();
}

后续还会在 Future 上进行扩展支持 Join 等更多组合操作。

Executor

Executor 中有两个队列:

  • ready_queue: std.ArrayList(*Future) - 调度队列,供调用者放入 Future 任务
  • futs: std.ArrayList(*Future) - 执行队列,实际调度器处理的 Future 任务

Future 先是进入到调度队列,如果调度开始执行后,会从调度队列取出任务放入执行队列,这时候执行队列中可能还有其他未完成的任务,当 Future 结束后会从执行队列中移除,如果执行队列中的所有任务都是等待状态,则 Executor 处于 idle 状态,等待 event_loop 唤醒,具体使用方式在上面的测例代码中已体现。

目前调度策略比较简单,而且没有经过任何优化,后续会不断完善。

Coroutine(绿色线程)

目前已经支持协程间的切换,下面的代码是非对称协程的实现方式:

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
var base_coro: Coroutine = undefined;
var count_coro: Coroutine = undefined;
var count: i32 = 1;

fn addCount() void {
count += 1;
base_coro.resumeFrom(&count_coro);
count += 1;
base_coro.resumeFrom(&count_coro);
count += 1;
base_coro.resumeFrom(&count_coro);
}

test "simple counter suspend and resume coroutine" {
const allocator = std.testing.allocator;

base_coro = try Coroutine.init(allocator, null);
defer base_coro.deinit();
count_coro = try Coroutine.init(allocator, addCount);
defer count_coro.deinit();

try std.testing.expect(1 == count);

count_coro.resumeFrom(&base_coro);
try std.testing.expect(2 == count);

count_coro.resumeFrom(&base_coro);
try std.testing.expect(3 == count);

count_coro.resumeFrom(&base_coro);
try std.testing.expect(4 == count);

std.debug.print("all finished\n", .{});
}

这个测试就是用协程的方式去执行 addCount 所在的 count_coro 协程,在 addCount 中也可以随时切换调用者协程 base_coro,执行原有逻辑。

还支持了函数参数的传递,在上下文切换的时候,不是两个函数的切换,是通过一个中间函数 call,它会根据汇编传过来的参数指针地址,转换成具体的 *Coroutine,再从其中拿出 func_ptr 和 args_ptr,就相当于中间层转发了一下。从下面的代码看目前参数类型都是定死的,有点牵强,目前够用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn call(coro_ptr_int: u64) void {
const coro: *Coroutine = @ptrFromInt(coro_ptr_int);

std.debug.print("current coro address: 0x{x}\n", .{@intFromPtr(coro)});

if (coro.frame.func_ptr != null) {
if (coro.frame.args_ptr != null) {
const func_ptr = @as(*const fn (*const anyopaque) void, @ptrCast(coro.frame.func_ptr.?));
const args_ptr = coro.frame.args_ptr.?;
func_ptr(args_ptr);
} else {
const func_ptr = @as(*const fn () void, @ptrCast(coro.frame.func_ptr.?));
func_ptr();
}
} else {
std.debug.print("the func pointer is null\n", .{});
}
}

Eventloop

事件响应机制也就是 eventloop (reactor 模型),其实是所有异步协程实现的底层支持,我甚至认为就算不用异步,只用事件机制和回调的方式也能做到高性能。这一部分在本期训练营并没有深入的去学习,目前只是实现了一个大概。如果这层封装好了,做成一层统一的抽象去处理 epoll、io_uring、iocp、kqueue 以及中断信号量等,也将会有很大的收获,给自己挖个坑,明年把这部分填上。

eventloop 的核心代码就是用一个循环,不停的调用系统需要等待的函数,等待系统给出响应,这里用的是 epoll_wait,这些系统提供的函数其实在操作系统里面都有自己的实现,一般性能都比较高,而且可以阻塞也可以非阻塞。当系统给出响应后,再触发回调去唤醒 Executor。

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
pub fn poll(self: *Self, timeout_ms: i32) !usize {
try self.events.resize(16); // 预分配事件数组,先这么写

const n = std.posix.epoll_wait(self.epfd, self.events.items, timeout_ms);

for (self.events.items[0..n]) |event| {
const fd = event.data.fd;

if (self.callbacks.get(fd)) |callback| {
if (event.events & std.posix.system.EPOLL.IN != 0) {
var buf: [8]u8 = undefined;
_ = std.posix.read(fd, &buf) catch {}; // 这里目前只处理了 timer 的情况

if (callback.callback_fn) |func| {
func(callback.user_data);
}
}
}
}

return n;
}

pub fn run(self: *Self) !void {
self.running = true;

while (self.running) {
_ = try self.poll(100); // 100ms 超时
}
}
}

整体组合 xasync

把上面各部分组合起来,看看能不能达到预期效果。

Timer

这部分注册一个 TimerHandle 到 event loop 当中。

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
const Timer = struct {
const Self = @This();

handle: TimerHandle,
completed: bool = false,
waker: ?*const Waker = null,

fn init(nanoseconds: u64) !Self {
const handle = try TimerHandle.init(&global_event_loop, nanoseconds); // 注册给 event_loop
return .{
.handle = handle,
};
}

fn deinit(self: *Self) void {
self.handle.deinit();
}

fn timerCompletedCallback(data: ?*anyopaque) void { // event_loop 回调
if (data) |ptr| {
const timer: *Timer = @ptrCast(@alignCast(ptr));
timer.completed = true;
std.debug.print("timer callback completed!\n", .{});
if (timer.waker) |waker| {
waker.wake(); // 唤醒
}
}
}

// future poll
fn poll(ctx: *Context) Result {
const timer: *Timer = @ptrCast(@alignCast(ctx.payload));
if (timer.completed) {
std.debug.print("poll timer is completed\n", .{});
return .{ .done = null };
} else {
timer.waker = ctx.waker;
return .wait;
}
}
};

Sleep

这部分把 Timer 包装成 Future

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
fn sleep(nanoseconds: u64) void {
std.debug.print("sleep comes in\n", .{});
if (!sys_is_block) {
const timer_ptr = global_runtime.allocator.create(Timer) catch unreachable;
timer_ptr.* = Timer.init(nanoseconds) catch unreachable;

const callback = EventCallback{
.callback_fn = Timer.timerCompletedCallback,
.user_data = timer_ptr,
};
timer_ptr.handle.setCallback(callback) catch unreachable;

const timer_fut = future.runWithAllocator(global_runtime.allocator, Timer.poll, timer_ptr).chain(struct {
fn thenFn(_: ?*anyopaque, ctx: *Context) *Future {
const timer = @as(*Timer, @ptrCast(@alignCast(ctx.payload)));
ctx.allocator.destroy(timer);
return future.done(null);
}
}.thenFn);

global_runtime.executor.schedule(timer_fut);

global_runtime.switchTaskToBase(); // 类似 suspend - 这个地方实现还有点歧义
// global_runtime.switchToExecutor(); // 如果需要等待返回结果则需要切换到 executor 等待其 resume 回来
} else {
std.Thread.sleep(nanoseconds);
}
}

fn delay() void {
std.debug.print("delay comes in\n", .{});
sleep(5 * std.time.ns_per_s);
}

main

整合完毕后对于使用者来说,代码如下:

1
2
3
xasync(delay);
// xawait(); // 需要等待的时候开启
std.debug.print("hello xasync\n", .{});

运行效果

不等待完成

1
2
3
4
5
6
7
delay comes in
sleep comes in
hello xasync - 注意这里,没有等待 timer 异步执行结束,而是直接返回
timer callback completed!
poll timer is completed - 注意这里,timer 结束了
main will quit
event loop quit

等待完成

1
2
3
4
5
6
7
delay comes in
sleep comes in
timer callback completed!
poll timer is completed
hello xasync - 注意这里,虽然底层是异步协程执行,但是这里等待 timer 执行完毕才打印
main will quit
event loop quit

总结

从目前执行效果和 API 的调用方式看符合预期,基本达成了本期的目标:实现简单的异步协程运行时 (zig),按照这种方式解决函数着色问题是有希望的。

虽然本期目标基本达成,但是中间学习的过程中还是有很多技术细节没有完全搞懂,有些学习资料没有完全看完,后续还要继续努力。

后续规划

  • 参数和返回值的类型支持且能自动推导
  • 支持线程池 thread pool
  • eventloop 完善
  • 是否后台调度支持用户配置 - 现在需要改代码来实现
  • 支持 rust 调用
  • 封装 asyncio
  • 集成到 arceos/rcore 中
  • 性能对比测试

答疑和思考

为什么要用 zig 写

  • 没有任何原因,个人偏好,peace & love.

实现代码在哪里

中断驱动的协程异步网络 - 在ArceOS中实现

一、背景

时间过的飞快,转眼间,训练营四阶段已经迎来尾声。
我由于去年已经参加过一届训练营,所以本次我提前一个月晋级到了四阶段,与向勇老师确定了我的课题方向:“中断驱动的协程异步网络”。
在后续周会的沟通中,向老师也给了我更具挑战性的目标:“在VisionFive2开发板上验证我的工作”。

二、历程

在四阶段,我主要完成了以下几个里程碑:

Rust异步运行时

因为我之前曾经在工作中编写过面向 WASM 平台的 Rust 异步运行时,所以第一个里程碑并没有花掉我太多时间。
为了给自己增加一些挑战,我还额外实现了常见的异步运行时辅助数据结构与函数,如:mutexspawn, block_on, join 等。

异步版本的网络操作

在完成 Rust 异步运行时后,我便开始着手实现异步版本的网络API。
我为所有网络操作中包含block_on的函数都编写了异步版本,并实现了网络操作的异步化。其中包括:

  • socket.recv_async()
  • socket.send_async()
  • socket.accept_async()
  • socket.connect_async()

PLIC中断驱动的Future唤醒

实现以上异步网络操作后,网络异步仍然是以Poll驱动的,而不是以真实的物理世界事件驱动的。
为了实现真正的异步网络,我需要实现一个中断驱动的Future唤醒机制。
在 RISC-V 架构中,PLIC 是中断控制器,可以用于实现中断驱动的Future唤醒机制。
我为 ArceOS 移植了 PLIC 驱动,并实现了 VirtIO-Net 设备的 PLIC 中断驱动的 Future 唤醒机制。

将ArceOS移植到VisionFive2开发板

在完成以上工作后,距离训练营结束仍然有一个半月的时间。这次组会中,向老师给了我一个更具挑战性的目标:将 ArceOS 移植到 VisionFive2 开发板,并在 VisionFive2 开发板上验证我的工作。我也就正式开始了移植工作。
开发板板载了 u-boot 和 OpenSBI v1.2,我可以直接从 u-boot 启动 ArceOS。
幸好我手中还有 jTag 调试器,向老师也给我提供了其他前辈同学的移植经验。
感谢萧络元同学的代码仓库,我很快便完成了 ArceOS 的移植。
VF2 开发版与常见 RISC-V 开发版有些许不同,例如:
* S 态可用内存起点为 0x4000_0000 而不是 0x8000_0000
* OpenSBI v1.2 支持 SBI v1.0 规范,但未实现 Console Extension。所以日志打印时需要使用 Legacy Console API。
以及 u-boot 启动与 Qemu 启动也有些差异,现代镜像打包时更多采用 itb 格式,同时将 dtb 打包进 itb 中。
总的来说,移植工作并不算太难,但是当系统无法加载又没有任何日志输出的时候,还是需要些裸机的调试手段,例如直接通过汇编调用串口打印,或者 sbi call 字符打印来确定程序执行到了哪里,是否进入内核代码。

为VisionFile2启用中断驱动的异步网络

在完成 ArceOS 的移植后,我便开始着手为 VisionFive2 开发板编写网卡驱动,启用中断功能,复现我在 Qemu 中完成的功能。这是本次训练营中我遇到的最大挑战。
首先,启动真实的网卡设备异常复杂,需要按顺序依次启动不下10个时钟信号与复位信号。
然后,VF2 平台使用的网络设备为 dwmac-5.2,参考 Linux 内核中驱动实现时,其历史悠久,代码量庞大,支持功能多且复杂,兼容设备多,还需要兼容多个平台和历史版本,硬啃 Linux 内核驱动代码基本上不太现实。
在必要时候还需要配合 PHY 芯片的同步配置,与设备寄存器交互通过 MMIO 映射,而与 PHY 芯片交互则需要通过 MDIO 总线,Clause 22/45 协议。

同时还需要学习众多的 MMIO 寄存器偏移量,与每个 bit 对应的功能含义,了解 GMAC/DMA/MTL/PHY 相关寄存器。
也学习了 ring buffer 的实现,以及如何使用 ring buffer 实现网络数据收发。

最终我还是没能在 VF2 平台开启中断功能,根据与厂家工程师的交流,PHY 芯片中断引脚并未接入 PLIC 中断控制器,所以无法使用 PLIC 中断驱动网络功能。

遗憾未能亲眼见到自己的工作在真实硬件上运行,但是通过本阶段的学习,我还是学到了海量的知识。

三、总结

通过本次训练营,我弥补了我知识体系中的许多空白,包括:

  • PLIC 中断控制器
  • 网卡驱动
  • U-Boot 运作原理
  • 真实硬件是如何运作的

四、致谢

感谢向勇、陈渝老师,以及所有帮助过我的老师和同学。

2025 春夏季开源操作系统训练营 学习总结

1
2
3
4
5
// 实验环境
Mac mini Apple M4
MacOS 15.4.1
// 备注(在国外的同学可以忽略)
在配置实验环境时,一定要更换Homebrew、rustup和Cargo的镜像源。

第一阶段:Rust编程

由于参与过 http://opencamp.cn/ 的其他 Rust 训练营,本阶段相当于是复习阶段。然而笔者对很多 Rust 语法还是理解的不够,需要通过更多实际项目来加深学习。

第二阶段:OS设计实现

本阶段主要参考指导书完成实验 https://learningos.cn/rCore-Tutorial-Guide-2025S/

1. Apple Silicon 相关问题

在第零章,指导书如是说:
经初步测试,使用 M1 芯片的 macOS 也可以运行本实验的框架,即我们的实验对平台的要求不是很高。但我们仍建议同学配置 Ubuntu 环境,以避免未知的环境问题。

笔者在实验中确实遇到了一些问题,但都并非无法解决,具体情况如下:

1.1 安装 qemu (可能需要 sudo)

1
2
brew install qemu
qemu-img --version

如果显示qemu-img version 9.2.3则说明安装成功。

1.2 载入rustsbi-qemu.bin时卡死

@lurenjia1213 修复了该问题,需要 Clone 下面的项目,重新编译,然后拷贝生成的rustsbi-qemu.bin 并覆盖实验项目中的文件。
https://github.com/lurenjia1213/rustsbi-qemu/tree/main

1.3 qemu 模拟器无法正确退出

在ch3中,qemu模拟器无法正确退出,需要拷贝ch2中的./src/boards/qemu.rs到ch3。通过对比ch2和ch3的区别来修复问题,从而可以学习正确退出模拟器的方法。

ch4 通过同样的方法在 BASE=1 的测试中还是无法正确退出,需要以后解决。

1.4 MacOS 没有 timeout 指令导致测例无法通过

安装 coreutils, 用 gtimeout 来替代。

1
2
3
brew install coreutils
// 需要将下面这条指令写到保存环境变量的那个文件中
alias timeout=gtimeout

2. 学习心得

由于没有系统学习过操作系统的理论知识,学的比较慢,需要参考指导书和学习资源中的视频才慢慢掌握。虽然操作系统相关的内容学的不够扎实,但是对于 rust 的语法慢慢熟悉了起来。抱着学习 rust 的心态还学习到了操作系统的底层运行,很有收获。

第三阶段:项目基础阶段 - 组件化操作系统

  • 尽管是第一次接触操作系统,在完成第二阶段以后,第三阶段给笔者的感觉不是很陌生(至少知道大概都在干什么)
  • 课程视频和课件也很详细的给出了任务和学习目标,但是第三阶段的项目相比第二阶段要大很多
  • 有时候不知道要去哪里干什么,只能通过给出的题目反向查找相关的内容。

1.遇到的问题和解决思路

  • 在UniKernel部分, 笔者在 MacOS 下面完成了任务,到了宏内核的部分涉及到了交叉编译的部分,似乎 MacOS 变得复杂了起来。折腾了一段时间之后,还是在一台 Linux 服务器上用 docker 完成了后续的任务。
  • 这是笔者第一次使用 docker,遇到不懂的就让 Chatgpt 来生成指令,但是还是遇到了很多问题:
    1. hub.docker.com 被墙,根本创建不了 Linux 容器, 后来找到 nvcr.io/nvidia/pytorch:25.05-py3 解决了问题: https://catalog.ngc.nvidia.com/orgs/nvidia/containers/pytorch
    2. 以为 root 之后就万事大吉,但是在运行容器的时候要加上 --privieged, 不然 mount 指令无法正确的找到目录的位置
    3. 运行容器的时候千万不要加 -rm, 不然停止容器的时候就永远地消失了

      2. 学习心得

      由于上述笔者遇到的问题,反反复复搭环境搭了 3 次, 对各种 Linux 指令,以及各种组件的作用有了更深的理解。相比第二阶段,第三阶段的练习难度反而有所下降,但是 ArceOS 本身的内容是相当多的,需要更加系统的学习。 希望能在第四阶段有所收获。

rust基础的总结

一.基本数据类型与所有权

所有权系统核心规则

  1. 移动语义(Move)
    1
    2
    3
    let s1 = String::from("hello");  // 堆分配
    let s2 = s1; // 所有权转移
    // println!("{}", s1); // 错误!s1 已失效
  2. 借用规则
    • 任意时刻:一个可变引用 多个不可变引用
    • 引用必须始终有效(悬垂指针禁止)
  3. 生命周期标注
    1
    2
    3
    fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
    }

Slice 类型

  • 无所有权引用
    1
    2
    3
    4
    let s = String::from("hello world");
    let hello: &str = &s[0..5]; // 字符串切片
    let a = [1, 2, 3, 4];
    let slice: &[i32] = &a[1..3]; // 数组切片

二.Crate 与模块系统

Crate 类型

类型 文件扩展名 特点
二进制 Crate main.rs 可执行程序
库 Crate lib.rs 可复用代码库

模块可见性规则

1
2
3
4
5
6
7
8
mod front_of_house {
pub mod hosting { // pub 使模块公有
pub fn add_to_waitlist() {}
}
}

// 使用绝对路径访问
crate::front_of_house::hosting::add_to_waitlist();

使用外部 Crate

1
2
3
# Cargo.toml
[dependencies]
rand = "0.8.5" # 语义化版本
1
2
3
4
5
6
// main.rs
use rand::Rng;

fn main() {
let num = rand::thread_rng().gen_range(1..101);
}

三. Option 与错误处理

Option 枚举

1
2
3
4
5
6
7
8
9
10
11
enum Option<T> {
Some(T),
None,
}

// 安全解包
let x: Option<i32> = Some(5);
match x {
Some(i) => println!("Value: {}", i),
None => println!("Missing value"),
}

Result<T, E> 错误处理

1
2
3
4
5
6
7
8
9
10
11
fn read_file(path: &str) -> Result<String, io::Error> {
fs::read_to_string(path)
}

// 错误传播简写
fn read_config() -> Result<String, io::Error> {
let mut file = File::open("config.toml")?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}

错误处理最佳实践

  1. 优先使用 Result 而非 panic
  2. 使用 ? 操作符传播错误
  3. 自定义错误类型实现 std::error::Error

四. Trait 与泛型

Trait 定义与实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
trait Summary {
fn summarize(&self) -> String;
}

struct NewsArticle {
headline: String,
location: String,
}

impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{} ({})", self.headline, self.location)
}
}

泛型函数

1
2
3
4
5
6
7
8
9
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}

Trait Bound 语法糖

1
2
3
// 以下两种写法等价
fn notify<T: Display + Clone>(item: &T) {...}
fn notify(item: &(impl Display + Clone)) {...}

生命周期进阶

生命周期标注必要性

  1. 结构体持有引用时必须显式标注生命周期,确保引用的有效性
  2. 方法实现中:
    • &self 参数隐含 &'a self 生命周期
    • 返回值关联结构体生命周期(通过生命周期消除规则第三项)
  3. 遵循Rust生命周期消除三规则:
    • 每个输入引用自动获得独立生命周期
    • 单个输入引用时所有输出引用与其生命周期对齐
    • 方法签名中 &self 使输出引用与结构体生命周期对齐

错误

1
2
3
4
fn dangling_reference() -> &str {
let s = String::from("temporary");
&s[..] // 错误!返回局部变量引用
} // s离开作用域被丢弃
1
2
3
4
5
6
7
8
9
10
struct ImportantExcerpt<'a> {
part: &'a str,
}

impl<'a> ImportantExcerpt<'a> {
fn announce_and_return(&self, announcement: &str) -> &str {
println!("Attention: {}", announcement);
self.part
}
}

五. 智能指针

常用智能指针对比

类型 所有权 线程安全 使用场景
Box<T> 单一 堆分配、递归类型
Rc<T> 共享 单线程引用计数
Arc<T> 共享 多线程引用计数
RefCell<T> 可变 运行时借用检查

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Box 用于递归类型
enum List {
Cons(i32, Box<List>),
Nil,
}

// Rc 共享所有权
use std::rc::Rc;
let a = Rc::new(5);
let b = Rc::clone(&a);

// RefCell 运行时借用检查
use std::cell::RefCell;
let c = RefCell::new(42);
*c.borrow_mut() += 10;

六.迭代器与闭包

闭包类型推断

1
2
let add_one = |x| x + 1;         // 类型自动推导
let print = || println!("hello"); // 无参闭包

闭包捕获模式

捕获方式 关键字 所有权
不可变借用 `
可变借用 ` mut
值捕获 move 转移

迭代器适配器

1
2
3
4
5
6
7
let v = vec![1, 2, 3, 4];

// 链式调用
let sum: i32 = v.iter()
.map(|x| x * 2) // 加倍
.filter(|x| x % 4 == 0) // 过滤4的倍数
.sum(); // 求和

自定义迭代器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Counter {
count: u32,
}

impl Iterator for Counter {
type Item = u32;

fn next(&mut self) -> Option<Self::Item> {
if self.count < 5 {
self.count += 1;
Some(self.count)
} else {
None
}
}
}

七.并发与异步编程

线程创建

1
2
3
4
5
6
7
use std::thread;

let handle = thread::spawn(|| {
println!("From spawned thread");
});

handle.join().unwrap();

通道通信 (mpsc)

1
2
3
4
5
6
7
8
9
use std::sync::mpsc;

let (tx, rx) = mpsc::channel();

thread::spawn(move || {
tx.send("Message").unwrap();
});

println!("Received: {}", rx.recv().unwrap());

共享状态 (Mutex)

1
2
3
4
5
6
7
8
9
10
11
use std::sync::{Arc, Mutex};

let counter = Arc::new(Mutex::new(0));

let handles: Vec<_> = (0..10).map(|_| {
let c = Arc::clone(&counter);
thread::spawn(move || {
let mut num = c.lock().unwrap();
*num += 1;
})
}).collect();

异步编程 (async/await)

1
2
3
4
5
6
7
8
9
10
11
12
async fn fetch_data() -> Result<String, reqwest::Error> {
reqwest::get("https://api.example.com/data")
.await?
.text()
.await
}

#[tokio::main]
async fn main() {
let data = fetch_data().await.unwrap();
println!("Data: {}", data);
}

八.常用集合类型

Vec 动态数组

1
2
3
4
5
6
7
8
9
10
11
let mut v = Vec::with_capacity(10);
v.extend([1, 2, 3]);

// 安全访问
if let Some(val) = v.get(1) {
println!("Second element: {}", val);
}

// 所有权注意事项
let first = &v[0];
// v.push(4); // 编译错误!存在不可变引用时禁止修改

HashMap<K, V> 哈希表

1
2
3
4
5
6
7
8
use std::collections::HashMap;

let mut scores = HashMap::new();
scores.insert("Blue", 10);

// Entry API 安全更新
scores.entry("Yellow").or_insert(50);
scores.entry("Blue").and_modify(|e| *e += 1);

阶段二,os基础

lab1

与上下文, 特权级有关的寄存器

  • sstatus:包含了处理器的状态信息,包括特权级别和中断使能状态。恢复 sstatus 的值确保在返回用户态时,处理器的特权级别和中断状态与陷阱发生前一致。
  • sepc:保存了中断或异常发生时的程序计数器值。恢复 sepc 的值确保在返回用户态时,处理器能够从中断或异常发生的地方继续执行。
  • sscratch:保存了用户栈指针。在切换到用户态之前,将用户栈指针保存到 sscratch 寄存器中,以便在用户态下使用。
  • sret根据sstatus中的SPP位指示切换为用户态。(寄存器中的一个位,0,u_mode;1_,s_mode,s_mode)
  • scause: Trap原因/种类
  • stvec: trap_handle地址

lab2

SV39

  • virtual page 39位, 38-12为虚拟页号
  • 页表项PTE: Reserver: 10, PPN2: 26, PPN1: 9, PPN0: 9, RSW: 2, DAGUXWRV

分页

  • MMU地址转换
  • kernel address space最高位为 “跳板”, app ks, guard page
  • app address space, 最高位为跳板, TrapContext, UserStack, GP, Framed

    跳板意义:

    satp, 切换后,地址映射不同, 例如:上下文切换的restore, 在更改satp指令后, 保证下一条指令在不同的地址映射下能被正确寻址,保证指令的连续执行

TrapContext新增字段

在进行特权级转换时, 需要相应的sp以及satp的token

  • pub kernel_satp: usize, 内核地址空间的 token
  • pub kernel_sp: usize, 当前应用在内核地址空间中的内核栈栈顶的虚拟地址
  • pub trap_handler: usize, 内核中 trap handler 入口点的虚拟地址

lab3

fork

  • 获得父进程的地址空间
  • sepc + 4
  • a0返回参数更改,父子进程不相同
  • 维护父子进程关系
  • fd, 死锁检测等

功能实现

  • stride算法:

    • 为TCB加上schedule块(struct), 同时预留了pass设置的接口
    • 为sys_set_priority加入了对priority的设置
    • 将TaskManager块改为了用binaryheap存储, 并为TCB分配了Ord特性,每次选取都会取stride最小的调度
  • 向前兼容

    • 重写mmap和munmap(用到了remove_area_with_start_vpn)
    • 重写了sys_get_time,用到了translate_va

lab4

文件系统

文件系统本质上是一堆块上的抽象, 在内存中有缓存块对其进行映射.

进程维护一个文件描述符表,可映射到对应的缓存块

1
2
3
4
5
6
7
8
pub struct BlockCache {
cache: [u8; BLOCK_SZ],
block_id: usize,
block_device: Arc<dyn BlockDevice>,
modified: bool,
}

//提供对应的接口调用

easy-fs磁盘布局

  • 超级块 (Super Block),用于定位其他连续区域的位置,检查文件系统合法性。

  • 索引节点位图,长度为若干个块。它记录了索引节点区域中有哪些索引节点已经被分配出去使用了。

  • 索引节点区域,长度为若干个块。其中的每个块都存储了若干个索引节点。

    1
    2
    3
    4
    5
    6
    7
    8
    #[repr(C)]
    pub struct DiskInode {
    pub size: u32,
    pub direct: [u32; INODE_DIRECT_COUNT],
    pub indirect1: u32,
    pub indirect2: u32,
    type_: DiskInodeType,
    }
  • 数据块位图,长度为若干个块。它记录了后面的数据块区域中有哪些已经被分配出去使用了。

  • 数据块区域,其中的每个被分配出去的块保存了文件或目录的具体内容。

lab5

在引入线程后, 调度机制本质上是在线程块上进行切换. 会区分主线程和子线程

  • 创建线程不需要要建立新的地址空间
  • 能够访问到进程所拥有的代码段, 堆和其他数据段
  • 专有的用户态栈

实现功能

  • 在ProcessControlBlockInner加入了对mutex和sem的死锁检查块(all[], ava[], need[])
  • 检测前对相应资源的need[] + 1
  • 实现is_safe检测函数, 对finish==false和need <= work的块, 回收allocation和finish=true,对标记flag=true, 当finish没有任何改变, 即本次循环flag==false时退出loop, 利用闭包all,检测finish所有线程是否全是true
  • 若为unsafe, 则回退need, 返回-0xdead
  • 若为safe, 则在down和lock之前drop(process_inner),防止线程堵塞无法释放资源, 在down和lock之后同时更新检查块中的矩阵
  • 为up和unlock加上检查块的更新

Mutex实现问题

  • Mutex1的lock里,会一直尝试获取锁, 具体逻辑为当无法获得锁时,直接阻塞,让出cpu,直到被唤醒, 再重新尝试获得锁, unlock中释放锁,并且唤醒一个线程去竞争这个锁.
  • Mutex的lock,在无法获得锁时,直接堵塞,在unlock时,只有等待队列为空才释放锁.
    - 这里的unlock本质是锁资源的转移, A不释放锁, 而是唤醒一个直接使用这个资源的B线程(它醒来后直接运行临界区后的代码)

第三阶段

一, 组件化内核基础与 Unikernel 模式

组件化内核介绍

Unikernel 模式

  • 特点
    • 应用与内核合一:编译为一个 Image,共享同一特权级(内核态)和地址空间。
    • 无用户态 / 内核态切换:简单高效,但安全性较低(应用可直接访问内核资源)。

核心组件

组件名称 功能描述 在实验中的作用
axhal 硬件抽象层,屏蔽不同架构差异(如 Riscv64/ARM) 初始化串口、内存等硬件,提供底层 IO 接口
axruntime 内核运行时环境,负责引导流程、内存初始化、任务调度框架 执行内核启动流程,调用应用层代码
axstd 内核态标准库,提供基础数据结构和工具函数(如 println!) 实现字符终端输出功能
arceos_api 内核公共接口,定义组件间通信协议 统一组件间调用规范

Unikernel 的启动链

  • 硬件启动:通过 OpenSBI(Riscv 固件)加载内核 Image 到内存。
  • 引导阶段(axhal)
    • 初始化 CPU 寄存器、MMU 分页(早期恒等映射)。
    • 建立内核栈,为 Rust 运行时做准备。
  • 运行时阶段(axruntime)
    • 初始化内存分配器、日志系统。
    • 调用应用层 main 函数,执行具体功能。

实验

1. 主函数 src/main.rs

1
2
3
4
5
6
7
8
#![cfg_attr(feature = "axstd", no_main)] // 若启用 axstd,不使用标准库的 main 入口
#[cfg(feature = "axstd")] // 根据 feature 条件编译
use axstd::println; // 使用 axstd 的打印函数

#[cfg_attr(feature = "axstd", no_mangle)] // 避免符号名被修改
fn main() {
println!("Hello, ArceOS!"); // 调用 axhal 提供的串口输出功能
}

2. 依赖管理 Cargo.toml

1
2
3
[dependencies]
axstd = { workspace = true } // 引入 axstd 组件,支持标准库功能
arceos_api = { workspace = true } // 引入内核公共接口

3. features 动态配置

  • 作用:通过编译参数控制组件的启用,实现 “按需构建”。
  • 示例
    • axstd 组件通过 feature = “axstd” 控制是否包含。
    • 实验中默认启用 axstd,因此能使用 println!。

println!

通过更改ulib下axstd,macros文件中的println!

hashmap

1
2
#[cfg(feature = "alloc")]
pub mod collections;

暴露自己写的collections

1
2
3
[dependencies.hashbrown]
version = "0.14"
default-features = false

用了官方库的core版本

二, 内存管理与多任务基础

1. 分页的两个阶段

阶段 目标 实现方式 关键组件
早期启用(必须) 快速建立基本映射,保证内核启动 1GB 恒等映射(虚拟地址 = 物理地址) axhal 中的 BOOT_PT_SV39 页表
后期重映射(可选,需 paging feature) 扩展地址空间,支持设备 MMIO 细粒度权限控制(如只读、可执行) axmm 中的 AddrSpace、PageTable

2. 算法

算法 原理
TLSF 两级 Bitmap + 链表管理空闲块
Buddy 基于 2 的幂次分裂 / 合并空闲块
Slab 为特定大小对象创建缓存池

3.

全局分配器:通过 #[global_allocator] 声明,实现 GlobalAlloc trait。

1
2
#[cfg_attr(all(target_os = "none", not(test)), global_allocator)]
static GLOBAL_ALLOCATOR: GlobalAllocator = GlobalAllocator::new();

任务数据结构 TaskInner

1
2
3
4
5
6
7
8
struct TaskInner {
id: TaskId, // 唯一标识
name: String, // 任务名称(调试用)
state: AtomicU8, // 状态(Running/Ready/Blocked/Exited)
kstack: Option<TaskStack>, // 任务栈(类似线程栈)
ctx: UnsafeCell<TaskContext>, // 上下文(保存寄存器状态)
// 其他字段:调度相关(如时间片、优先级)
}

协作式调度

FIFO 队列:任务按 “先到先服务” 原则执行,当前任务需主动让出 CPU(调用 yield_now())。

组件
组件 功能
axsync 同步原语(自旋锁、互斥锁)
axtask 调度接口(spawn/yield_now 等)

实现

EarlyAllocator实现要求比较低

byte
  • alloc
    • 注意每次分配内存时候的对齐
    • 预分配,检查是否与p_pos重叠
    • 为count++
      • 注意每次分配内存时候的对齐
      • 预分配,检查是否与p_pos重叠
      • 为count++
  • dealloc
    • 单纯的count–
    • count==0时,就可以重置b_pos了
page
  • alloc
    • 检查alignment是否有效
    • 获取分配的size进行对齐,同时检查是否越界
    • 更新数据
      • 检查alignment是否有效
      • 获取分配的size进行对齐,同时检查是否越界
      • 更新数据
  • dealloc
    • 不要求实现

三、调度,块设备,文件系统

时钟中断:

代码(Riscv64 中断初始化)

1
2
3
4
5
6
// axhal/src/platform/riscv64_qemu_virt/mod.rs
axhal::irq::register_handler(TIMER_IRQ_NUM, || {
update_timer(); // 更新系统时间
axtask::on_timer_tick(); // 触发调度器更新
});
axhal::arch::enable_irqs(); // 开中断

块设备驱动:

Trait:BlockDriverOps

1
2
3
4
5
trait BlockDriverOps {
fn num_blocks(&self) -> u64; // 磁盘总块数
fn block_size(&self) -> usize; // 块大小(512 字节)
fn read_block(&mut self, block_id: u64, buf: &mut [u8]) -> DevResult; // 读块
}

文件系统:

抽象

文件系统(FileSystem):如 FAT32、EXT4。

目录(Dir):存储文件 / 子目录元数据。

文件(File):存储具体数据,支持读写操作。

接口

1
2
3
4
trait VfsOps {
fn root_dir(&self) -> &DirNode; // 获取根目录
fn lookup(&self, path: &str) -> Option<FileNode>; // 解析路径
}

加载流程

块设备读取:通过 VirtIO Blk 驱动读取磁盘前 512 字节(引导扇区)。

解析 BPB:获取 FAT 表起始地址、簇大小等参数。

挂载文件系统:将 FAT32 的根目录挂载到 VFS 的 / 节点。

应用加载示例(U.8 实验)

1
2
3
4
5
6
// 从 FAT32 文件系统加载应用程序
fn load_app(path: &str) -> Result<Vec<u8>> {
let root = vfs.root_dir();
let file = root.lookup(path).ok_or("文件不存在")?;
file.read_to_end() // 读取文件内容到内存
}

实验实现

寻找ing

在axfs_ramf中实现

1
impl VfsNodeOps for DirNode{...}

文件时通过封装的BTreeMap管理的, 替换相应键值对即可

注意

1
fn rename(&self, src_path: &str, dst_path: &str)

src和dst_path路径层级不一样
我使用了split_path_to_end来获取最终的文件名

四, 地址空间管理

缺页异常处理

1
2
// 关键修改:init_user_stack的lazy参数设为false
let ustack_top = init_user_stack(&mut uspace, false).unwrap(); // 延迟映射

缺页异常处理流程

  • 异常触发:用户态访问未映射地址(如栈写入),CPU 陷入内核。
  • 处理逻辑:
    1. 通过handle_page_fault函数申请物理页帧(alloc_frame)
    2. 在页表中建立虚拟地址与物理页帧的映射(pt.remap)
1
2
3
4
5
fn handle_page_fault(...) -> bool {
let frame = alloc_frame(true); // 申请物理页
pt.remap(vaddr, frame, orig_flags); // 建立映射
tlb.flush(); // 刷新TLB
}

ELF 格式解析

关键段:
LOAD 段:包含代码段(R E标志)和数据段(RW标志)。
BSS 段:未初始化数据,ELF 文件不存储,内核需预留空间并清零。
加载逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
for segment in elf.segments {
if segment.type == LOAD {
let vaddr = segment.virt_addr;
let phys_frame = alloc_frame(segment.mem_siz);
map_virtual_to_physical(vaddr, phys_frame, segment.flags);
if segment.has_data {
copy_file_data(vaddr, segment.file_offset, segment.file_siz);
} else {
zero_memory(vaddr, segment.mem_siz); // BSS段清零
}
}
}

实验实现

得到aspace->分配内存->将文件信息写入

1
2
3
4
5
6
const MAP_SHARED = 1 << 0;    // 共享映射,对映射区域的修改会反映到文件中
const MAP_PRIVATE = 1 << 1; // 私有映射,对映射区域的修改不会反映到文件中
const MAP_FIXED = 1 << 4; // 必须使用指定的映射地址
const MAP_ANONYMOUS = 1 << 5; // 匿名映射,不与文件关联
const MAP_NORESERVE = 1 << 14; // 不保留交换空间
const MAP_STACK = 0x20000; // 用于栈分配

这里只处理MAP_PRIVATE,
同时addr.is_null(),可通过aspace.find_free_area寻找内存

五, Hypervisor

Hypervisor

1.1 定义

Hypervisor(虚拟机监控器)是运行在物理硬件与虚拟机之间的虚拟化层软件,允许多个虚拟机共享物理资源,每个虚拟机拥有独立的虚拟硬件环境(如vCPU、vMem、vDevice)。

1.2 核心功能

  • 资源虚拟化:模拟CPU、内存、设备等硬件资源
  • 隔离与调度:确保虚拟机之间资源隔离,并高效调度物理资源
  • 模式切换:在Host(Hypervisor)与Guest(虚拟机)之间双向切换

1.3 与模拟器的区别

维度 Hypervisor 模拟器(Emulator)
ISA一致性 虚拟环境与物理环境ISA一致 可模拟不同ISA(如x86模拟ARM)
指令执行 大部分指令直接在物理CPU执行 全部指令需翻译/解释执行
性能目标 高效(虚拟化开销低) 侧重仿真效果,性能要求低

1.4 虚拟化类型

  1. I型Hypervisor:直接运行在硬件上(如Xen、KVM),性能高
  2. II型Hypervisor:运行在宿主OS上(如VirtualBox),依赖宿主资源管理

二. Riscv64虚拟化扩展(H扩展)

2.1 特权级扩展

新增特权级:

  • HS(Hypervisor Supervisor):Host域的管理级,负责虚拟化控制
  • VS(Virtual Supervisor):Guest域的内核级,运行Guest OS内核
  • VU(Virtual User):Guest域的用户级,运行Guest应用

特权级关系:

1
2
物理机:M(最高) > HS > U
虚拟机:VS(Guest内核) > VU(Guest用户)

2.2 关键寄存器

  • hstatus:控制Host与Guest的模式切换
    • SPV位:指示进入HS前的模式(0:非虚拟化模式;1:来自Guest的VS模式)
    • SPVP位:控制HS是否有权限操作Guest的地址空间
  • vs[xxx]/hs[xxx]:分别用于Guest和Host的上下文管理
  • misa:标识是否支持H扩展(bit7=1表示支持)

3. 模式切换机制

3.1 从Host到Guest(run_guest函数)

1
2
3
4
5
6
// 保存Host寄存器状态
sd ra, (hyp_ra)(a0) // 保存返回地址
// 加载Guest寄存器状态
ld sstatus, guest_sstatus(a0)
// 执行sret指令切换到VS模式
sret

可参考guest.s

  • a0指向的guest_reg区域 与 当前reg的替换

    3.2 VM-Exit处理(以SBI调用为例)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    match scause.cause() {
    Trap::Exception(Exception::VirtualSupervisorEnvCall) => {
    let sbi_msg = SbiMessage::from_regs(ctx.guest_regs.gpr);
    if let Some(SbiMessage::Reset(Shutdown)) = sbi_msg {
    ax_println!("Shutdown vm normally!");
    // 清理Guest资源
    }
    }
    }

实现

根据结果硬编码,更改guest_reg的值

Stage 1

这个阶段主要是学习Rust语法,因为之前有报名过训练营,所以做起来比较顺手,把基础语法又复习了一遍。

Stage 2

这个阶段主要是阅读实验指导书和源码。实验指导书非常重要,如果没有看明白的话对做实验有很大影响,所以要细心耐心看。时间有限的话,看精简版指导书即可。
完成实验部分需要重点理解几个点:

  1. 任务切换机制,保存切换前后程序上下文
  2. 地址空间,多级页表机制
  3. 文件系统,操作与管理

Stage 3

这个阶段主要是看视频和PPT,并通过做6个实验来熟悉ArceOS的设计思想。相对于上一个阶段的实验会简单一些。

总结

之前有报名过两次训练营,但都没有坚持下来。对于如何平衡学习、工作和自我提升之间的平衡,是一个我现在以及将来都需要仔细思考的问题。

Prologue

这样的总结应该从何开始?我是从Bilibili刷视频偶然了解到与训练营相关的信息的。使用Rust语言编写操作系统的实践,我太喜欢这个方向了。因为我正学过一点Rust,也经学校老师的推荐看过CSAPP并完成了大多数的实验。其中我最喜欢的便是shlabattacklab────写一个shell!实在是有趣不过,如果再写一个操作系统呢?好吧,我应该没有与之匹配的实力,不过开源操作系统训练营就这样给了我一个类似的机会。报名人数破千!全程免费!还有什么好说的呢,杀😡。

Stage 1

110道Rustling编程题,并没有耗费我太多功夫,更多的是重新熟悉一下语法。我觉得,学习Rust不仅是学会如何使用一门编程语言,更是了解更多的编程范式。例如trait背后的组合大于继承;函数式编程对现代编程语言深刻的影响:默认不可变、闭包、HOFs、链式操作等;所有权与生命周期机制,这种RAII思想是C++首创的(但是opt-out)。Rust编译器就是你最好的老师,更别说还有满地走的各式AI(本总结经AI辅助完成),2025年的今天,学习Rust不应该再是一件难事🥳。

Stage 2

到了OS设计实现,主要是完成5道rCore操作系统大实验编程题。我是提前进入该阶段,所以全程并没有看过相关学习视频,而是跟随rCore-Tutorial-Guide文档完成的🤓。

这阶段最耗时的是lab2───地址空间和lab5───并发,这两个不管哪个太痛苦了😭。lab2是因为分页机制本身就相对复杂,层层抽象,读内核新增的代码就花了我很久时间(光论这一点文件系统其实不遑多让,不过到这里我的读代码能力已经得到显著锻炼了,所以带给我的痛苦远不及地址空间🥱)。而lab5,单纯是我因为技术路线的左右互博而无限拖缓了进度,我一直在对死锁检测资源的获取上究竟是现场构建还是跟随进程保存之间反复横跳。倒不如说,是因为我在实现这两个的时候遭遇多方掣肘,导致我不停怀疑我自己,不停的重构。使用Rust编程不就是戴着脚镣在跳舞吗?我现在水平还不够,只能写出不够优美的实现,但是我不会放弃的😡。

Stage 3

组件化操作系统,这大概是最各显神通的阶段了。我对这阶段的印象其实是一点草台味🤯,遇到各方面奇怪的问题,测试脚本死活不通过,各种不同的资料,到底要实现在哪里,我要怎么修改一个crate依赖的代码?我是个不撞南墙不愿意问别人的人,所以我全部都闭门造车自己解决了所有问题(真的吗?至少测例说我通过了)。但实际上,在讨论群里大家都很乐意回答别人的提问,每个人都有自己的“奇技淫巧”,应该让大家全都热烈讨论遇到的问题,才能让训练营变得更好😈。

Conclusion

写到这里我已经有点精疲力竭。我在参与前三个阶段的过程中收获颇多,不只是对整个组件化操作系统的认识。还有各种在学习过程中对工具的使用,helixZellij,这些工具,我很早就下载了,只是因为它是Rust重写的老工具。现在呢?我需要helix丰富的快捷键,我需要Zellij的分屏。我开始熟悉,正是我开始迈出一步,参加了这次,2025 春夏季开源操作系统训练营

完成了三阶段的任务,我也疲惫了,进入了一种拖延的状态。五月二十二号,新建文件夹,想要完成这篇总结报告。一直到今天,我终于又想重新出发了。希望到了第四阶段,我可以找到新的方向。

编程的乐趣:⭐️⭐️⭐️⭐️
挑战的难度:⭐️⭐️⭐️
开源训练营:⭐️⭐️⭐️⭐️⭐️