前言
这是我第二次报名这个训练营但是相当于是第一次参加了,因为第一次报名是在2024年的秋冬季,那时候我刚大一刚刚接触到计算机,感觉很有意思就报名了但是那时候连使用Github对我来说都极其困难且刚进入大学没有适应繁多的课程,
我便决定先放弃,潜修一年有了些许微波的基础后再来试一试。我总是喜欢去了解一些东西的底层,所以我对操作系统算是有一总执念吧,让我再次来到这里。但由于本人比较愚笨,学习速度极慢,再加上早八到晚十的课程,所以进度相较于其他优秀学院慢了许多,但贵在坚持嘛……
arceos/exercises/print_with_color/src/main.rs 文件, 为 println 添加 ANSI 转义码即可. ANSI 转义码用于在终端中控制光标位置、文本颜色、文本样式等, 其以 ESC (Escape) 字符开头.\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 是同一内容的不同表示.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.arceos/modules/alt_axalloc/src/lib.rs 中具体实现.arceos/exercises/alt_alloc/Cargo.toml 声明依赖 axstd: workspace = true 表明 axstd 是工作区成员; features = ["alt_alloc"] 使 axstd 开启 alt_alloc 特性. 当 axstd 的 alt_alloc 特性被启用时, 其会继续将此传递下去, 还会根据 [features] 的 alt_alloc = ["arceos_api/alt_alloc", "axfeat/alt_alloc"] 启用 arceos_api 中的 alt_alloc 特性和 axfeat 中的 alt_alloc 特性.arceos/axfs_ramfs 中实现 rename 操作, 但 arceos 原本依赖远程仓库的版本, 故需要调整.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_ramfs 与 axfs 依赖的不同. 故还需将修改 arceos/modules/axfs/Cargo.toml, 替换 version = "0.1" 为 workspace = true.arceos/axfs_ramfs/src/dir.rs. 阅读测试用例后发现, 文件重命名只在同一个目录下操作, 故在具体实现中只考虑此情况, 并未做特殊处理.sys_map 时, 发现运行结果与课件中展示的内容有很大出入, 课件显示 not implement, 而本地运行则会显示 Cannot load app!. 经排查, 发现未设置 musl-libc, 导致无法编译出 mapfile 可执行文件.sys_map 时针对测试用例做了简化处理, 未考虑复杂的情况. 实现 sys_mmap 系统调用主要有 5 个步骤: 获取当前进程的地址空间, 并在此查找空闲内存区域, 再准备参数执行内存映射, 再读取文件内容到缓冲区 buf 并将其写入映射的内存区域, 最后返回映射起始位置. 实现过程中, 务必查找空闲的内存区域再映射, 否则会出现 Mapping error: AlreadyExists.payload 构建失败导致的. 在外层 Makefile 中定义的 RUSTFLAGS 会被默认传递给 make -C ./payload 递归调用, 这会干扰编译 payload (尤其是 skernel). 故在 payload 的 Makefile 中添加 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,把他们的共同点抽离出来,封装成一个个组件,可以实现快速的内核定制。
下面说一下遇到的一些问题:
起初,我通过 hashbrown 引入第三方的实现,发现并未涉及视频中提到的随机数,通过查找资料以及翻阅 std 下的 hash_map 实现。最终决定使用 hashbrown 下的 HashMap::with_hasher,并选择 foldhash 下的 FixedState::with_seed 提供一个哈希计算,其中 seed 通过 axhal 下的 random 生成。
最开始,发现怎么改动都不生效,arceos 的实验使用了 workspace,这个在之前我从未接触,最终发现是 ramfs_rename 对应的这个内核的 Cargo.toml 文件依赖的不是 workspace 中的 axfs_ramfs 导致。
除此之外,还有修改 axfs 下的 Cargo.toml。
从文件中映射到内存中,分三步走,在虚拟地址空间找一个空闲虚拟页,借助 uspace.find_free_area,借助 uspace.map_alloc 进行预填充的分配,即完成虚实映射,然后将文件读到改页。
如果使用刚刚分配的虚拟地址可以读,但是会出现异常,比如权限不足,需要转到内核虚拟地址进行读写,这里通过 phys_to_virt 将物理地址转到内核到虚拟地址,随后通过 sys_read 这个 api 完成文件的读取。
自我感觉对于 rcore 的掌握还不到家,目前仅仅是通过了测例而已。
rcore 的文档和代码常读常新,能加深操作系统的理解。
听闻rCore是基于Rust实现的操作系统内核,我对此产生了兴趣并报名参加了训练营。经过三个阶段的系统学习,作为Rust语言的初学者,我掌握了Rust的基础语法特性,并深入复习了操作系统的相关概念。本文将记录我在完成第三阶段ArceOS的学习过程中,对几个较为感兴趣部分的实现理解与思考。
ArceOS的虚拟文件系统(VFS)中rename功能的设计充分体现了职责分离和安全性的设计理念。该部分的核心思路是VFS层通过最长匹配原则 (find_fs_for_path) 确定路径所属的文件系统,并妥善处理跨文件系统重命名和挂载点重命名等边界情况。
VFS在此充当调度者的角色,负责将请求转发给正确的底层文件系统。而底层文件系统 (如ramfs) 则负责执行实际的重命名操作,通过Rust的读写锁机制确保重命名操作以及覆盖情况的原子性和并发安全性,这种分层设计不仅职责清晰,而且使VFS具有良好的灵活性和可扩展性。
sys_mmap实现了Linux风格的内存映射机制,其核心流程围绕地址空间管理和文件/匿名映射展开,首先检查映射长度是否为零,并将长度强制对齐到4KB页大小。若指定了addr参数则要求其必须是页对齐的。
当设置MAP_FIXED标志时,系统必须使用指定的固定地址,并校验该地址是否在合法的地址空间范围内。否则系统会在进程的地址空间中查找一块足够大的空闲区域 (通过find_free_area函数) 作为映射的起始虚拟地址。
对于匿名映射MAP_ANONYMOUS,系统仅在地址空间中分配并映射物理页,不涉及文件操作。对于文件映射,系统先执行map_alloc建立映射关系,然后通过sys_lseek调整文件偏移量,接着同步读取部分数据到缓冲区,最后通过aspace.write将数据写入新映射的虚拟地址,完成初始数据的填充。
整个设计的重点在于利用地址空间aspace抽象实现统一的虚拟内存管理和权限控制,这让我深刻理解了操作系统中抽象层的重要性。
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_rename、alt_alloc 练习,到让 CI 里的 simple_hv 能稳定加载 skernel2,再到阶段性验收脚本 verify_lab1.sh。过程中频繁与 GitHub Actions 环境差异、虚拟机退出机制等问题斗智斗勇,现总结如下。
main(练习)与 lab1(挑战)qemu-system-riscv64print_with_color:确认基础输出能力print_with_color 示例在串口中输出带 ANSI 颜色的 “Hello, Arceos!”。main.rs 中使用 println!("\x1b[1;31m..."),并确保 axstd 打印宏可用。./scripts/test-print.sh,脚本通过检测转义序列与纯文本,判断是否既有颜色又有正确文案。support_hashmap:验证 alloc+集合support_hashmap 练习,完成 Memory tests run OK!。alloc::collections::HashMap 能频繁插入/删除。./scripts/test-support_hashmap.sh 构建磁盘镜像、运行练习并抓取最后一行输出,确保内存测试通过。sys_map:实现用户态 mmapexercises/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!”。ramfs_rename:递归重命名与特殊路径axfs_ramfs 的 DirNode::rename 仅支持同级结点,需要实现跨层级、处理 . .. 的逻辑。modules/axfs/src/root.rs 中实现“复制 + 删除”策略,保证旧结点被正确删除并保留内容。impl_vfs_dir_default! 宏覆盖自定义实现的问题,最后确认修改入口正确。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 验证,确保早期分配器可用于内核初始化阶段。simple_hv:CI 环境无法加载 skernel2这是整个阶段最耗时的部分,核心需求:让 GitHub Actions 中的 ./scripts/test-simple_hv.sh 通过。
verify_lab1.sh:阶段验收cd arceos && ./verify_lab1.sh | tee tmpa.txtlabs/lab1,并在 QEMU 中运行效验。lab_allocator 中的 LabByteAllocator 尚未实现,导致 panicked at labs/lab_allocator/src/lib.rs:20:9。后续计划完善 bump allocator 以便完成 lab1。第一次参加操作系统的训练营,确实学到了很多之前在书本上没有学到的知识,也锻炼了自己的动手能力。
主要是语法学习,因为之前学过rust的基础语法,所以这次简单复习一遍就很容易通过了,不过增加的数据结构和算法题还是有一定难度,需要多多debug。
rcore其实和本科的教材讲的操作系统其实是一回事,只是用rust实现。所以看文档就感觉很亲切,练习题也比较简单。
由于之前没有接触过组件化操作系统,第一次看到还是有许多不理解的地方。不过教学视频里面的图示都很清晰,对照着老师的教学也很容易听懂。练习题主要是要多使用gdb,搞清楚各个功能的函数调用链,功能本身的实现倒不是很复杂。
目前刚好处在论文开题的阶段,由于时间原因还没来得及做,希望能在截至日前抽出时间尝试一下。
再一次参加了OS训练营,从第一次的跌跌撞撞到第二次的懵懵懂懂再到第三次的游刃有余,操作系统虽然很难,但也正是因为困难才有意思。
整个内核的调用关系在心里更清晰了,在实现需求的同时,也力求代码的整洁和可读性。
代码量总体更少,确定了实现范围后很快就可以完成。
在实验上
操作系统是一个很神奇的东西,每一次学习都有新的收获,在里面能找到很多数据库,web开发共通的思想。操作系统导论也是一本很好的书,虽然我还没看完内存部分,但是通过结合实践,理解的更深刻了。
学习操作系统就像爬山,每一步都需要扎实,每一步都很困难,但每一步都离顶峰更近,踏一步就有一步的收获。
第一阶段由于原先有C/C++的编程基础,基础语法上手还是比较快的,但在rust语言在内存管理方面的特性还是需要适应一段时间。但学习rust的经历中,也是能学到很多,能看到很多其他编程语言的影子,但也有rust自身的特性,如内存管理方面。多方面多语言融会贯通,收获颇丰。
由于在操作系统方面是0基础,在进入第二阶段的学习之前,我先去学习了Mit的xv6课程,有了这方面的基础,后续的学习会简单很多。
在实验上第一次尝试用rust写内核,体验挺奇妙的,在内存管理上给人与C语言完全不同的体验。
作业难度比上一阶段简单一些,但模块化的设计思路很巧妙,体验比较新奇。
在实验上
总的来说,训练营的学习收获还是挺多的,也尝试了用rust编写内核,这是我从未涉猎的领域,希望在后续的学习中能学的更多,后续希望能写出自己的内核,尝试在操作系统中加入自己的想法。
在该实验中,我原来是在__print_impl中进行实现(因为不懂宏),后来在实验二中输出pass后测评脚本没找到support_hashmap pass,问ai是因为我的实现在行末有个换行导致没识别上,最后还是选择修改格式化输出宏。
在axstd中实现了collections并添加了HashMap。第一次的时候没有将原collections中的数据结构导入到新的collections中,卡了一小段时间。
最开始是使用递归形式写的,没有注意数据量,运行一半卡死了,最后修改成循环写法,修改后才顺利通过。
自感难度不大。
在修改dependencies卡了半天,一直没有调用自己实际写的(对rust的依赖之类的还是不太熟悉,经常不知道该怎么导入)。最开始把问题想太简单了,后续修改才真正pass。
最开始直接使用sys_read将内容写入用户内存空间的地址,没想到内核态没有实现处理这种缺页问题,看着Page Fault一脸懵逼,排查半天发现在read这一行,后续用ai排查才知道是这个原因,最后先写入缓冲区,再将缓冲区写入用户地址空间。
自己写的时候开了个头就没想法了。(可能是因为答案就在边上)
本来以为会很难,看视频的时候云里雾里的,实际写起来发现还好。
自我感觉第三阶段的任务要比第二阶段简单一些,可能是第二阶段刚开始接触kernel,加上代码量比较大,当时一个实验要卡好久,第三阶段感觉主要是底部实现已经比较完善了,所以好写一些。
经过这两个阶段对kernel的学习,我感觉我在文件系统方面还是有一点云里雾里,可能是第二阶段学习的时候比较仓促,不太扎实。同时rust编程方面也不算非常熟悉,看懂没有太大的问题,但是实际上手写总是因为语法问题而有卡顿,同时在包,模块的引用,cargo.toml的编写修改还是不太熟悉(以前就简单学过c和cpp,不太关注这种问题)。
离训练营结束还有一个多月,希望可以解决这些问题,继续学习,拿下通过学员证书,尽量争取一下优秀学员证书。
总体上 我对arceos的认识可以概括为接口,框架,算法。
traitfeature,对同一个框架采取不同的实现通过在arceos/exercises/print_with_color/src/main.rs 中 use axstd::println, 并且在print_with_color中没有实现axstd.rs 或者是axstd/mod.rs于是便到arceos/print_with_color/Cargo.toml中寻找
发现这一行
1 | axstd = { workspace = true, optional = true } |
意思是当前目录的配置直接继承了根目录的配置, 于是便到arceos/Cargo.toml中去求证
在members中,发现了以下这一行
1 | "exercises/print_with_color", |
说明确实继承了根目录的配置
因此直接在arceos/Cargo.toml中寻找axstd
发现了
1 | axstd = { path = "ulib/axstd" } |
于是便到ulib/axstd中寻找最后在arceos/ulib/axstd/src/macros.rs
中找到了
1 | macro_rules! println |
直接修改具体的实现即可
根据提示修改axhal能够修改ArceOS的颜色 到axruntime中找到打印符号的逻辑,使用的是ax_println那么直接修改该功能的实现,便实现了符号颜色的改变
ulib/axstd/Cargo.toml的 `[dependencies]中加入1 | axhal = { workspace = true } |
random函数的调用axstd/Cargo.toml中, 我加入了1 | [dependencies.xxhash-rust] |
Optino<(key, value)>键值对,全部初始化为None,表明下标处没有存储元素core::slice::from_raw_parts将输入的键hash得到一个下标,如果该下标对应的键值对是None,None为止。我并没有实现删除和查找的功能,只是通过了测试用例,实现的并不完善。
对一段连续的内存左侧用以字节分配,右侧用于页面分配即可实现
通过这个实验的学习,了解到了初始阶段的内存分配器可以通过直接操作物理地址来支持早期的库和函数
通过find_free_area找到用户空闲的虚拟地址空间,建立地址映射,将通过sys_read得到的文件内容写入物理地址中,最后返回虚拟地址的开头。
执行
1 | riscv64-linux-gnu-objdump -d ./target/riscv64gc-unknown-none-elf/release/skernel2 |
1 | ffffffc080200000: f14025f3 csrr a1,mhartid |
通过这两行可得出指令csrr a1,mhartid长度为4
1 | ffffffc080200004: 04003503 ld a0,64(zero) # 40 <_percpu_load_end+0x40> |
这两行可知指令ld a0,64(zero) 长度为4
因此需要在vmexit_handler中对Exception::IllegalInstruction和Exception::LoadGuestPageFault的处理中,将sepc的值加上4。
然后通过观察Exception::VirtualSupervisorEnvCall的处理分别设置寄存器a1和寄存器a0的数值即可。
通过对hypervisor 的初步学习 加深了程序就是状态机的理解,程序的状态有寄存器和内存,对最简单的Guest OS的初始化无非就是对寄存器和内存的初始化
折腾了半天发现rename一直不会是当前工作区的实现
于是就在arceos/exercises/ramfs_rename/Cargo.toml中将
1 | axfs_ramfs = { version = "0.1", optional = true } |
修改为
1 | axfs_ramfs = { workspace = true, optional = true } |
然后发现还是不行
发现由于rename是被axfs模块中的rename调用的,就将axfs的Cargo.toml也修改了
1 | axfs_ramfs = { workspace = true, optional = true } |
然后就可以了
实现上来说就是需要通过split_path分离出路径,必须在相同的路径下才能修改名字