0%

2023秋冬季开源操作系统训练营总结

一些碎碎念

我是一个普通的大三学生,目前就读于厦门大学计算机科学与技术系。不得不吐槽一下学校的课程设置,从本学期(也就是大三秋季学期)开始才开设计算机组成原理、计算机网络、汇编语言等计算机专业课程,至于操作系统、编译原理、计算机系统结构等等直到下个学期才会开设。当然也许学校的课程安排也有他的道理,现在慢慢意识到以前觉得又难又没有意义的课,比如说模拟电子技术、数字电子技术、计算方法等,这些课在现在的学习中或多或少都有涉及到,然后学起来甚至有点亲切感()。

前言

言归正传,相比于计算机专业的其他课程,我最感兴趣的应该就是操作系统了。虽然说也说不出来原因,也许是因为操作系统四个字看起来比较高级(?),在平常没事做的时候就自学了一遍学校的课程,通读过操作系统导论现代操作系统两本书,但是一切都仅局限于理论层次上,一直真正实际动手设计一个操作系统。这里不得不提到MIT的6s081,曾经试着做了一点点lab,但是因为各种原因,只开了个头就弃坑了。一次偶然的机会了解到了清华大学开源操作系统训练营,遂欣然报名。

一阶段

一阶段的任务主要是熟悉一下Rust的语法,通过rustling对Rust有了个初步的认识。(虽然于我个人来说好像并没有多大作用…该看不懂的还是看不懂…) 也算蛮顺利地完成了,为之后的学习增加了很多的信心。

二阶段

二阶段相当的煎熬。
Alt text

你已经熟悉了Rust的基本语法啦,现在请你动手用Rust搓一个OS出来。

对,差不多就是这种感觉。

整个的学习过程就差不多是,道理都明白,但是无从下手。比如说这个函数要加在哪个文件里,这里要干什么,unwrap是什么,……….,rust-analyzer真是帮了很大的忙就是说。
然后不出意料地卡关了,而且还适逢期中考试周,研究了两三天没什么成果后就弃坑去复习期中考试的内容了。考试结束后又重新捡起来看,发现之前悟到的那一点点东西也差不多全忘了。。。

在群里潜水看大佬们的讨论,才发现原来有 rCore-Tutorial-Guide-2023A 这个详细的指导书存在… 整个知识结构比适用于本次训练营的精简版清晰了不要太多。于是从头把到第五章为止的指导书阅读了一遍,感觉之前很多疑惑的问题在心里都有了答案(虽然也理解的不是很透彻)。
至于编程题的代码,啊作为Rust的初学者真是相当的头疼…于是到处借鉴了大佬们的代码,在大佬们的大框架下结合自己的想法进行修改,感受就是:哦原来这个在rust里应该这样写。

勉勉强强算是把二阶段完成了,回头思考二阶段的三个lab真是有轻舟已过万重山的感觉,一切都显得那么合理,但是让我从零开始写真是相当困难的一件事。也许原因是对Rust还不够熟悉吧,下一阶段的学习目标是把Rust系统的学习一遍。

三阶段

个人觉得达不到二阶段的考核要求,因此对于三阶段不抱什么希望,而且就算是参加了感觉也够呛能完成…

总结

感谢清华大学提供的这样一个学习机会,相比于我本科学校的纯理论教学,rCore和uCore属于是降维打击了,本次训练营的学习开拓了我的视野。有机会的话我会继续参加明年的训练营,希望在明年的时候能够以一个强大的自己来完成训练营的训练项目。

第三阶段总结报告

第一周

完成了三阶段的第二周的练习,我要诚实的说这对我来说并不容易

先来说说第一周的练习吧 第一周的练习 现在可能做完了所以觉得不难

实际上hashmap源码的删减就让我头疼 由于rust的语言特性实现hashmap似乎要比其他语言难不少 其他也就感觉还好

练习三需要实现一个内存分配算法 在这里我遇到了一些问题 在邝劲强同学的帮助下(他给予了我一些指导)完成了

后面的练习也都较为顺利的完成

第二周

涉及到arceos的外部应用的引用,实验嘛 代码什么的都提供了 很顺利的就完成了,然而到了练习的阶段,完全没思路 看着和我同为大二的新认识的同学顺利的推动着进度 但是我却没进展 心里十分焦急,

尝试了很多次 创建了不知道多少副本 不是跑不通就是panic 内存访问有问题,在百思不得其解下 我得到了王格格同学的指导 她细心的指导了我最后一点没想明白的地方(app.bin 如何被arceos调用),懂得了这一点内容,我后面的练习推动就能正常进行了,完成了联系一和二,接下来的3和4也都比较简单,然后是到了练习5 这里一开始我持续的panic 最后发现好像是我写的脚本不太严谨,总之忙了一段时间还终于是写出来了 练习6与练习5的关系比较大 没费什么功夫就写出来了。

总之 万事开头难 这样看来可能练习1&2才是最难

收获

在做arceos的练习与实验的过程中 让我更立体的理解了os的设计 而且arceos Unikernel是单内核的 与linux和windos都不同,组件化的设计,没有参加这次训练营我大概不会了解到的。

认识了很多rust底层开发的语法 (大部分是跟着chatgpt老师学的,虽然有的时候它笨笨的)

学习了怎么去编写简单的脚本 shell ,makefile …… (ps:也正是缺少了这部分知识让我第二阶段的学习遇到了许多困难)

明白了一丢丢汇编代码 如何直接以地址的方式访问数据

期望

最终的任务 尝试让arceos支持linux的多应用

我的实力。。。 我真的不觉得我能写出来。。。。

但是我会去试试的 实际上已经学到许多东西了 可喜可贺

感谢

首先当然是各位老师以及助教

其次是帮助我的各位同学 希望开源社区能在大家的共同努力下越来越好

前言

先对朋友 Kami 表示感谢,他在对转型底层开发路线为我提供引导并介绍了本期 OS 训练营。从专科到本科,从通用开发到底层开发,从零基础尝试学习并通过 Test。慢慢地不断前进。

在本期训练营中,我所添加代码量相对总体只占很少一部分。已有框架和大部分功能,完成 Test 更多考验的是思路。同一个问题使用相似的方法不断重复错误实现或纠结文档中的具体颗粒细节让我走了很多弯路。看似死胡同的路线中,及时停下尝试切换思路或寻找更多的资料补全知识脉络会让思维更清晰。而这也是我从本期训练营获得最大的收获,解决问题的思维比不断努力解决问题本身更重要。

个人介绍

​ 我本人是小学教育专业的初招定向师范生。小时候受到电影黑客帝国的感染,所以对于计算机从小就很向往。但在考完之后,由于自己天真以及父母的建议就来了师范。大学期间也由于是定向的,不能转专业。大一开始就自学计算机了,后面也参加了一些程序设计竞赛以及区域赛,在我们这个普通师范学校里面倒是打赢我们这边计算机相关专业的所有学生。但由于纯粹自己自学,学习方法上存在很大的问题,过于急功急利等原因实际上自己水平还是很差,最好的奖项也就只有蓝桥杯B组国二。在报名这个之前的项目方面的经历只写过csapp的lab,以及xv6敲了前三个lab。但也因为学习方法的问题,真正吸收到的东西很少,lab很多也是借鉴参考答案完成的。在这之前操作系统的基础只有从xv6前三个实验get到的一些东西。然后我本身对于业务方面的东西也不是很感兴趣,所以也没怎么学习这相关的。

对于这次训练营的体会

非常庆幸在当初在看群消息的时候,有群友提到了我们这个训练营。这几周的学习,我在操作系统方面还有个人的学习方法上改进了很多。在这之前我写lab的时候以及自己程序设计竞赛训练的时候,一开始的准备工作基本上都是先找到参考答案。然后在实际完成的时候会出现纠结于看不看答案的情况浪费时间,然后就是出现bug了,直接和参考答案逐行比对,而不是自己顺着代码思路找bug。这样子自己的编码和debug能力并没有提升上来,也没有体会到编程的乐趣。但这次rcore训练营没有参考答案。在第1个lab的时候,当时自己有点畏惧情绪,害怕出错。在完成了第三章的练习后,这种情绪才消减了。但在四章的学习过程中,我虽然之前写过xv6中对应的lab,但当时很多地方仍然是一知半解。在看文档的过程中,我陷入了细节的深渊。在很长的一段时间里面,我都是处于在看到这个函数的时候又想到某个问题,然后又去深究那个问题,然后在看那个问题的时候,又看到这个函数里面的细节不清楚,又去看这个函数的细节。如此反复。导致我的效率非常低下,且每一个问题都没有获得解答,看着每一个函数,脑海里面都是有点模糊,虽然脑海里面也有整体的大纲,但是对于细节来说很模糊。后面放下第四章,先去弄了第五章才意识到抽象出每个函数的接口,当看某个函数的逻辑时,对于它调用的函数里面的细节就可以暂时忽略,先有个大纲。

在实际写lab的过程中,第一个lab还比较顺利。第二个lab:在写munmap的实现的时候,因为我在实现mmap的时候是直接调用的insert_framed_area 添加了一个逻辑段,然后在memory_set中也正好看到删除以某个start_va开始的逻辑段的函数。当时就在想能不能借鉴这个函数,munmap的实现就直接删除与传入的start_va和end_va相匹配的段。但是转头一想,如果这个start_va和end_va要是在某个段中间或者是跨越多个段,那岂不是不行了。就在想办法解决这个问题。但是当时的我由于过去都是借鉴参考答案,所以当时的第一反应就是能不能找到参考答案看它有没有想这么多。有点害怕自己去尝试,然后这个感觉代码量也有点大。但后面发现测试样例很简单,就没考虑那么多。但是这里我还没有克服自己内心的障碍,因为我当时还是去借鉴了其他人的仓库(当时意外得知还可以这样干),来确定自己的想法。但是在实现第三个lab的时候:我算是克服了自己的这一内心障碍。当时自己写完之后,一make test就报time out了,当时的第一反应也是参考答案借鉴。但这次我坚持自己debug,最后花了一天半的时间找到了这个bug,仅仅只是一个地方少了一行代码。就是sys_spawn系统调用里面没有运行add_task函数, 导致一直运行的时候timeout. 就一行代码差不多两天半时间. 还是通过逐行和最开始能运行起来的ch5分支的代码进行比对才找到. 之前一直都找错方向了, 没有想到是测试样例调用了sys_spawn出的错误. 然后调bug的时候还遇到了一个问题, 当我直接用checkout回到ch5的初始提交记录的时候, 它在os文件夹里面make run的时候会报链接出错. 这个也花了好长时间去找原因, 但最后还是没有找到. 最后是通过直接重新clone仓库, 然后把初始ch5里面的文件复制过来才成功. 当找完bug,所以test全部通过的那一刻,内心确实是挺激动。但如果是放在以前的话,我花了一天半结果就这么一行代码,别说激动了,在自己内心没把自己pua死已经算好的了,也正是因为这种急功近利的心态,自己过去也是疯狂参考答案,但参考完答案之后,又由于不完全是自己写的,其实内心也还是会内耗,觉得这玩意没弄一样。但这次至少还是感觉挺有成就感,算是克服了内心的一重障碍,并且这种不借鉴答案的学习方式让我对于知识点本身的理解,以及自己内心的舒服程度上升了很多很多,至少不会内耗了。

这次rcore算是我真正意义上的入门lab。在完成这三个lab的过程中,我的完成速度再不断的加快,现在正在弄的第四个lab已经感觉比较得心应手了。之后也可以更加从容的面对其他lab。

然后rcore训练营,本身也是一份非常非常nice的操作系统学习手册,让我对于操作系统的各个部分的认识也上了一层楼。

rust 这门语言给我最大的感触就是它安全性,基本上所有可能出现的错误都帮你报错了,只要运行通过了很大程度上就不会有很大的问题,有也是逻辑上的问题了,不像c语言的指针,头大大的。然后它Option对于错误处理也非常nice。以及它的trait写起来很舒适,我在这之前并没有写过什么面向对象的项目,所以对于这个的认识可能也不是很深。

这次由于本专业要去小学实习,在加上自己的拖拉, 生病,以及一系列问题,就导致知道现在才完成了。希望还有机会能够第三阶段及格。加油.

总结报告

逐练习报告

练习1:获取镜像大小,使loader能够根据对应信息能够自动识别文件大小

main 函数中,固定设置 app_size = 32,这个显然是不合理甚至危险的。
请为 image 设计一个头结构,包含应用的长度信息,loader 在加载应用时获取它的实际大小。

一开始想要尝试类似于rcore中在链接脚本的特定位置保存数据并且通过build.rs脚本获取对应镜像大小然后再写入到链接文件中去的方法。

后面想到这个方法会因为相互依赖问题而不容易解决,最原初的设想在于类似rCore的处理方案,在原初的链接脚本中保存一个位置用于存储应用程序大小,后面在loader中对于这个段的数据进行获取,但是这部操作实际上是需要在链接的时候完成的,即我还不知道最终生成的二进制文件的大小就要将其写入进去。(后面想了下可能还可以在外面手动修改这个段,但是有点麻烦了就没有使用)。

在与邝劲强同学进行沟通的时候,发现其实可以用相对来说更加简单的方法来实现这个效果,即通过一个脚本实现了对于镜像文件大小的获取以及自动将代表对应文件大小的bin文件加载到对应的位置。这个地方特别感谢邝劲强同学分享的有关 dd 会相互覆盖,可以使用 seek 跳过一段的信息。

1
2
3
4
5
6
7
8
9
10
APP_BIN_SIZE=$(stat -c%s "./hello_app.bin")
echo $APP_BIN_SIZE
printf "%04x" $APP_BIN_SIZE | xxd -r -p > ./size.bin

# Using Zero to fill the block to 32M(32Times, one times for 1M)
dd if=/dev/zero of=./apps.bin bs=1M count=32
# Add origin app into the end of the file (not cover)
dd if=./size.bin of=./apps.bin conv=notrunc       bs=1B seek=2
dd if=./hello_app.bin of=./apps.bin conv=notrunc  bs=1B seek=4
mv $BASE_DIR/hello_app/apps.bin "$BASE_DIR/apps.bin"

练习2: 拓展image头结构,使之可包含两个应用。

通过对于 hello_app 的简单拷贝,可以实现第二个应用,这一步主要是在一开始的基础上拓展了头结构,但是由于这里没有提供对于更多应用程序的支持以及通过函数返回了文件大小埋了个坑(后面会提到)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Input: The Name of Directory
# Output: The size of the binary image
function generateBinrary() {
  # Create Binrary
  echo $1
  cd "$BASE_DIR/$1"
  echo `pwd`
  cargo build --target riscv64gc-unknown-none-elf --release
  # remove symbol information
  rust-objcopy --binary-architecture=riscv64 --strip-all -O binary ../../target/riscv64gc-unknown-none-elf/release/$1 ./$1.bin

  # return size
  return $(stat -c%s "./$1.bin")
}

cd $BASE_DIR
hex1=$(printf "%02x" $hello_app_size)
hex2=$(printf "%02x" $hello_ebreak_size)
hex="$hex1$hex2"
echo -n $hex | xxd -r -p > size.bin

练习3:批处理形式运行两个单行代码应用 nopwfi

这里有个小问题就是需要修改原始代码中给出的 -> !options(noreturn) 标志,以避免出现没有办法返回的情况。

这个地方实际上实现的批处理并不是真正意义上的批处理,我在复用之前代码的时候出错了,这个问题在 lab2.5-commit 中得到了修复。

以下是当时提交的实现,基本上就是对之前的代码的直接复用。

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
let apps_start = PLASH_START as *const u8;
let byte = unsafe {
  core::slice::from_raw_parts(apps_start, size_of::<u16>())
};
let app_size_1 = u8::from_be_bytes([byte[0]]);
let app_size_2 = u8::from_be_bytes([byte[1]]);
println!("size 1: {app_size_1}, size 2: {app_size_2}");

println!("Load payload ...");

println!("sizeByte: {byte:?}");

let read_only_app1 = unsafe {
  core::slice::from_raw_parts(apps_start.offset(size_of::<u16>() as isize),
  app_size_1 as usize
)};
let read_only_app2 = unsafe {
  core::slice::from_raw_parts(apps_start.offset((size_of::<u16>() + app_size_1 as usize) as isize),
  app_size_2 as usize
)};

// println!("content: {:?}: ", code);
println!("Load payload ok!");

println!("Copy app ...");
let load_start = LOAD_START as *const u8;

// load app 1
let load_app_1 = unsafe {
  core::slice::from_raw_parts_mut(load_start as *mut u8, app_size_1 as usize)
};
let load_app_2 = unsafe {
  core::slice::from_raw_parts_mut(
    load_start.offset(app_size_1 as isize) as *mut u8,
    app_size_2 as usize)
};

// Copy App Data From ReadOnly Areas
load_app_1.copy_from_slice(read_only_app1);
load_app_2.copy_from_slice(read_only_app2);

练习4:实现 SYS_TERMINATE

这个练习没什么好说的,直接添加 arceos_api 然后再加到符号表里面实现一下功能就可以了

1
2
3
4
fn abi_terminate() -> ! {
    println!("[ABI:TERMINATE]: Shutting Down !!!");
    arceos_api::sys::ax_terminate();
}

练习5:改造应用 hello_app

在完成实验五的时候主要出现了几个超出意料而且有点摸不着头脑的情况,包括但不限于a7寄存器被修改,莫名其妙的镜像文件截断,主要的修复细节可以参考 批处理修复多道程序支持返回值错误修复拷贝错误修复寄存器错误修复 等具体 commit

本任务的主体部分实际上与之前的exec2.4没有什么特别的差异,主要的难点在于puts的实现实际上调用了迭代器的功能,同时这还牵涉到由于字符串实现中&str的链接问题。

1.由于在操作中使用了字符串,但是字符串被放置在rodata段,在默认链接情况下rodata段实际上位于.text段前面,这会违背我们以装载地址作为应用的起始运行地址的想法(jalr LOAD_ADDRESS)【在起始部分有一大串0】

因此,需要手动实现一个类似的链接脚本(本处直接采用了rCore之前使用的链接脚本进行替代),添加build.rs编译脚本,给cargo添加了rust-args参数以选择我们自己做的链接脚本对于hello_app包进行链接。此处特别鸣谢赫森同学在群里分享的有关信息。

2.在修改了链接脚本之后仍然会出现神奇的报错信息,经过修改 MakeFile 运行 debug 之后发生产生这个情况的原因在于程序的二进制文件被异常截断了,同时出现很神奇的文件大小不增反减的情况(如下图)。



这跟我之前exec2.2实现的那个函数有直接的关联,或者说主要的问题就处在我使用的这个函数这里。

1
2
3
4
5
6
7
8
9
10
11
# exec 2.2 实现的函数
function generateBinrary() {
  # Create Binrary
  echo $1
  cd "$BASE_DIR/$1"
  cargo build --target riscv64gc-unknown-none-elf --release
  rust-objcopy --binary-architecture=riscv64 --strip-all -O binary ../../target/riscv64gc-unknown-none-elf/release/$1 ./$1.bin

  # return size
  return $(stat -c%s "./$1.bin")
}

王瑞康同学指出我的脚本代码中实际存在缺陷,问题主要出在我这个地方是通过bash函数的返回值来实现对于镜像文件大小的获取,但是实际上这个地方由于bash函数返回值限制在0~255位, 当添加了&str之后的镜像文件大小实际上达到了将近4110,则会面临取模截断的情况,在这样情况下4110 % 256 = 13 最终导致了实际上只有13个字节的数据被成功加载到内存中来,最终导致了代码出现异常的截断情况。

针对于这个情况,主要修复的方案在于通过echo + result=${},外加事先转换成为十六进制。

1
2
3
4
5
6
7
8
9
10
11
12
# Input: The Name of Directory
# Output: Hex Size of Binary
function generateBinary() {
  # echo "====================" $1 "===================="
  # Create Binrary
  cd "$BASE_DIR/$1"
  cargo build --target riscv64gc-unknown-none-elf --release
  # remove symbol information
  rust-objcopy --binary-architecture=riscv64 --strip-all -O binary ../../target/riscv64gc-unknown-none-elf/release/$1 ./$1.bin

  echo "$(stat -c%s "./$1.bin" | xargs printf "%04x")"
}
  1. 在调试完成上述问题之后发现,在当前情况下仍然出现了 panic,经过调试之后发现,主要问题出现在 a7 寄存器被擦写。

针对于这个问题,在之前的练习四多次调用同一个函数的时候也回出现类似的错误,当时进行修复的主要是在函数调用末尾添加了unsafe { core::arch::asm!("ld a7, {}), sym ABI_TABLE},在每一个可能使用到 a7 寄存器的地方手动添加一次寄存器,但是在使用迭代器的情况下,这种简单的处理方法并不奏效。根据群里面王瑞康同学分享的clobber_abi("C"),在应用程序调用asm的时候添加了这个操作就可以保持寄存器不变。

练习5:代码重构

练习五的时候由于接受了赫森和邝劲强同学有关于当前脚本不能很好的适应多道程序加载的建议(裱糊匠操作太多),对于原先的脚本进行了重构,重构后仅需要以字符串形式在脚本中添加对应的应用名称,就可以自动将应用生成二进制文件放到一起。

1
2
3
4
5
// 注:这个地方实际上搞错了PFLASH的具体位置,但是懒得改了
# PFLASH 32M ]
# PFLASH 32M ] [ NUM_OF_IMAGE ]
# PFLASH 32M ] [    u16:2B    ] [ BYTE_LIST:4B*NUM_OF_IMAGE ]
# PFLASH 32M ] [     2B + 4B * NUM_OF_IMAGE                 ] [  ] [  ] [  ]

这段主要实现的交过类似于前文中所实现的,具体包括生成应用程序脚本并且通过objcopy去除其中非必要的信息,通过echo的方式传递对应的数据。
用下文中的for循环包裹上面的这个函数,实现了将对应的app_name, app_size等数据保存在bash一维数组中的效果(方便后文调用)

1
2
3
4
5
6
7
for name in "${app_names[@]}"; do
  echo name: $name
  app_size=$(generateBinary $name)
  app_sizes["$name"]=$app_size
  link+=${app_size}
  NUM_OF_IMAGE=$(expr $NUM_OF_IMAGE + 1)
done

根据上面获取到的对应size信息,自动生成有关于num和size的数据,并且分别生成十六进制二进制文件合并到原先的PFALSH空间中。

1
2
3
4
5
6
7
8
cd $BASE_DIR
printf "%02x" $NUM_OF_IMAGE | xxd -r -p >num.bin # NOTE: not allow app > 255
echo -n "${app_sizes[@]}" | xxd -r -p > size.bin
echo "size.bin size: $(stat -c%s "./size.bin")"

dd if=/dev/zero   of=./apps.bin              bs=1M count=32
dd if=./num.bin   of=./apps.bin conv=notrunc
dd if=./size.bin  of=./apps.bin conv=notrunc bs=1B seek=2

然后,根据上文中保存的对应数据,自动生成其他的,涵盖包信息的二进制文件,并且逐个合并到前面的空间中。

1
2
3
4
5
6
7
8
start_offset=$((2 + 4 * $NUM_OF_IMAGE)) # NUM_OF_IMAGE:2B + IMAGE_SIZE:4B * NUM_OF_IMAGE
echo "start_offset" $start_offset
for ((i=0; i<${#app_names[@]}; i++)); do
  app_name=${app_names[i]}
  app_size=${app_sizes[i]}
  dd if="$BASE_DIR/$app_name/$app_name.bin" of=./apps.bin conv=notrunc bs=1B seek=$start_offset
  start_offset=$((start_offset + app_size))
done

在loader中,主要做出了如下改变:

首先,根据当前的二进制布局,获取app_num作为总应用程序数量研判标准,获取对应各个应用程序sizes的区域

let byte_num = unsafe { core::slice::from_raw_parts(apps_start, size_of::()) };
let app_num = u8::from_be_bytes([byte_num[0]]);

保存操作如下:

其中需要特别注意到的是由于rust中的自动union操作(忘记叫啥名了,就是自动对齐),会对于u8,u16连续的情况下在u8之后补零

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Gain Each App Size
let mut apps: [APP; MAX_APP_NUM] = [APP::empty(); MAX_APP_NUM];
let byte_apps_sizes = unsafe {
    // NOTE: BC Rust Internal structure autocomplete will fill vacancy, thus u16 rather than u8
    core::slice::from_raw_parts(
        apps_start.offset(size_of::<u16>() as isize),
        app_num as usize * size_of::<u16>(),
    )
};

let mut head_offset = size_of::<u16>() + app_num as usize * size_of::<u32>();
for i in 0..app_num {
    let i = i as usize;
    apps[i] = unsafe {
        APP::new(
            apps_start.offset(head_offset as isize),
            u16::from_be_bytes([byte_apps_sizes[i * 2], byte_apps_sizes[i * 2 + 1]]) as usize,
        )
    };
    head_offset += apps[i].size;
}

并且将其存储到结构体数组中:
结构体实现如下:

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
#[derive(Clone, Copy)]
struct APP {
    pub start_addr: *const u8,
    pub size: usize,
}

impl APP {
    pub fn new(start_addr: *const u8, size: usize) -> Self {
        Self { start_addr, size }
    }
    pub fn empty() -> Self {
        Self {
            start_addr: 0xdead as *const u8,
            size: 0,
        }
    }
}

impl core::fmt::Debug for APP {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        if self.size == 0 {
            return Ok(());
        }

        f.debug_struct("APP")
            .field("start_addr", &self.start_addr)
            .field("size", &self.size)
            .finish()
    }
}

最后通过一个for循环实现重复装载应用程序到指定地址

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
// LOAD APPLICATION
for i in 0..app_num {
    println!("====================");
    println!("= START OF APP {i} =");
    println!("====================");
    let i = i as usize;
    let read_only_app =
        unsafe { core::slice::from_raw_parts(apps[i].start_addr, apps[i].size) };
    let load_app =
        unsafe { core::slice::from_raw_parts_mut(load_start as *mut u8, apps[i].size) };
    println!(
        "Copy App {i} data from {:x} into {:x}",
        apps[i].start_addr as usize, LOAD_START
    );

    load_app.copy_from_slice(read_only_app);

    trace!("Original App: ");
    trace!("{i}: {read_only_app:?}");

    trace!("Load App:");
    trace!("{i}: {load_app:?}");

    register_abi(SYS_HELLO, abi_hello as usize);
    register_abi(SYS_PUTCHAR, abi_putchar as usize);
    register_abi(SYS_TERMINATE, abi_terminate as usize);

    println!("Executing App {i}");
    let arg0 = b'c';
    unsafe {
        core::arch::asm!("
        la      a7, {abi_table}
        li      t2, {run_start}
        jalr    t2",
            clobber_abi("C"),
            run_start = const LOAD_START,
            abi_table = sym ABI_TABLE,
        )
    }

    println!("APP {i} FINISH !!!")
}

练习6:应用虚拟空间

基本上没什么好说的,对于base2.5的代码进行简单复用就可以实现。

实验一 & 实验二

实验问题

练习 1:
main 函数中,固定设置 app_size = 32,这个显然是不合理甚至危险的。
请为 image 设计一个头结构,包含应用的长度信息,loader 在加载应用时获取它的实际大小。
执行通过。
练习 2:
在练习 1 的基础上,扩展 image 头结构,让 image 可以包含两个应用。
第二个应用包含唯一的汇编代码是 ebreak 。
如实验 1 的方式,打印出每一个应用的二进制代码。

解题思路

  1. 确定镜像头的内容
    因为实验一二要将多个应用的二进制代码加载到 pflash 区域,所以为了更好的兼容和抽象,镜像的头应该包括:
  • 应用程序的个数
  • 每个应用程序的大小
  • 每个应用程序的起始地址
    因此,最终确定的应用程序头如下:
    1
    2
    3
    4
    5
    struct AppHeader {
    apps_num: usize,
    app_size: Vec<usize>,
    app_start: Vec<*const u8>,
    }
  1. 确定镜像的加载办法
    关于镜像的加载实际上在老师的示例代码中已经给出了,需要特别注意的是:pflash 要求的镜像大小必须是 32M,所以需要先生成一个 32M 的全零文件,再将二进制文件 dd 进去。

解题方案

  1. 首先确定镜像头的
    在外部形成 pflash.img 镜像,包含应用的个数,每个应用的大小,每个应用的二进制文件
    这使用一个脚本文件生成,镜像的生成办法在代码仓库:https://github.com/Gege-Wang/hello_app

在 loader.rs 中建立一个 Appheader 结构体,并且从 pflash.img 中将 AppHeader 还原出来。
根据AppHeader 的信息加载每个应用。

pflash
apps_num
app1_size
app2_size
appn_size
app1.bin
app2.bin
appn_bin

需要解决的问题

在脚本文件中并没有指定二进制文件的顺序,这不合理,需要后期重构。
关于 rust 中各种类型的转换需要更加熟悉一些。

实验三

实验问题

批处理方式执行两个单行代码应用,第一个应用的单行代码是 nop ,第二个的是 wfi 。

解题思路

这个问题的关键是:如何执行第一个应用并且在第一个应用执行完成的时候开始执行第二个应用。 我在编译 nop 的时候发现应用大小是 4 ,然后内容是 [1, 0, 0, 0],但是我瞄到群里先做出来的人的截图,都不是这个二进制,我感到很困惑。后来我开始想第一个应用结束之后怎么返回到 load.rs 然后继续执行。jalr t2 之后会保存下一条指令的地址,所以在应用一执行完之后返回就能回到 load.rs 这条指令。虽然我用这个方法能够做出来这个问题,但是却为后来的问题埋下了巨大的麻烦,事实证明,从应用返回这个方法需要非常多需要考量的地方,甚至可以说直接从应用返回是非常不可取的。

解题方案

使用一个循环依次执行每个应用程序,在程序执行完之后加上 ret 指令返回。

需要解决的问题

或许要在外部实现一个批处理的程序,而不是由应用程序自己返回。

实验四

实验问题

本实验已经实现了1 号调用 - SYS_HELLO,2 号调用 - SYS_PUTCHAR,请实现 3 号调用 - SYS_TERMINATE 功能调用,作用是让 ArceOS 退出,相当于 OS 关机。

解题思路

虽然说这个好像是里面最简单的一个,但是我还是找了好几个来完成。我不太清楚 exit 和 terminate 是什么区别,其实我刚开始是用了 axtask 里面的 exit()来完成的,不知道退出号是什么,就那么随随便便退出了。感觉没什么底气,又开始搜索 terminate,还真的是有一个 axhal/misc 里面有一个terminate(),打印log 看起来比较正常了。

解题方案

仿照 abi_hello 和 abi_putchar 实现就可以了。

实验五

特别感谢 @PeizhongQiu 的技术支持。

实验问题

按照如下要求改造应用 hello_app:

  1. 把三个功能调用的汇编实现封装为函数,以普通函数方式调用。例如,SYS_PUTCHAR 封装为 fn putchar(c: char) 。
  2. 基于打印字符函数 putchar 实现一个高级函数 fn puts(s: &str) ,可以支持输出字符串。
  3. 应用 hello_app 的执行顺序是:Hello 功能、打印字符串功能、退出功能。

解题思路

封装成函数不困难,打印字符串也不困难,最困难的是第三个要求,完成这三项功能。
这里有一个重要的逻辑需要搞清楚。 ABI 是 Arceos 实现的,封装的函数是 应用 实现的。调用封装函数也是 应用 实现的。 整个的处理逻辑却是在 Arceos 的 load.rs 中实现的
从每一次 ABI 调用返回到 应用 的时候都需要返回到应用上一条指令的状态,这里有两个目的:一个是保存一些固定寄存器的值,比如 a7,另一个是保存封装函数的返回地址 ra。就像宏内核中系统调用需要保存上下文一样,ABI 调用的时候依旧需要保存上下文,这貌似是因为不是一个普通的函数调用,普通函数调用编译器会保存栈帧,不过这里需要自己保存,而按照怎样的规则保存是应用和操作系统约定的规范。

解题方案

我意识到要保存上下文,却没有想到一个好的办法, @PeizhongQiu 告诉我使用 clobber_abi(“C”),能够保存上下文,并且在返回的时候恢复,这是一个非常好的消息,能够不费吹灰之力之力的完成后面的工作。不过这条命令做的事情也是需要进一步掌握的。

需要解决的问题

要清楚保存上下文发生的细节,才能完成应用程序二进制兼容的事情,这个问题很重要。

实验六

实验问题

  1. 仿照 hello_app 再实现一个应用,唯一功能是打印字符 ‘D’。
  2. 现在有两个应用,让它们分别有自己的地址空间。
  3. 让 loader 顺序加载、执行这两个应用。这里有个问题,第一个应用打印后,不能进行无限循环之类的阻塞,想办法让控制权回到 loader,再由 loader 执行下一个应用。

    解题思路

    这里做完基础练习就没做什么改动就过了,为 APP 建立独立的页表,实现初始化和切换函数。在练习五的基础上进行加上独立的页表就可以了。

特别说明

所有的代码实现都在仓库https://github.com/Gege-Wang/arceos,欢迎交流并提出指导意见。

实验总结

非常开心参加 ArceOS unikernel 第三阶段的训练,在基础学习部分的训练大部分都是老师在指导书里给了详细的说明,石磊老师非常有耐心,助教老师也很给力,训练群里每个同学都很积极,让我感觉充满了动力。

ArceOS Unikernel 总结报告

不知不觉,ArceOS Unikernel 课程的学习已经过去了一半。回顾过去这两周,我深感学习过程中的磨砺与成长。感谢石磊老师的细心指导,从基础概念入手,让我们逐渐掌握 Unikernel 的奥秘。谁曾想到,Unikernel 的设计竟如此强大。每次回顾这个过程,我都会重新阅读陈渝老师的著作《像开发应用一样开发操作系统》,从中汲取启示。我对操作系统可以如同应用一般,通过组件化思维实现感到震惊。过去曾听说过鸿蒙分布式部署等新闻,也阅读过一些关于鸿蒙的相关文章,但亲身参与组件化操作系统的学习,完全是不同于以往的体验。

课程总结

第一周练习主要包含以下内容:

  • 首先,通过彩色打印 “Hello World” 和支持 Hashmap 数据类型的示例,快速熟悉 ArceOS 架构
  • 接着,实现 early 内存分配算法,深入了解 ArceOS 的内存分配流程
  • 最后,将协作式调度算法 FIFO 改造为抢占式调度算法,为第一周的学习画上圆满句号

通过本周学习对 ArceOS 的整体架构、内存分配及调度算法有更深入的了解。

Read more »

Arceos unikernel summary

Exercise 1

实验中 payload 中的 app.bin 文件是通过一系列工具组合编译处理打包而来的,在实验一中,只是用 objdump 工具将 elf 文件中的一些调试信息和元信息删除掉,并通过 dd 命令生成一个32M 的空文件并将应用放在空文件的开始位置,在 arceos 中 loader app 运行的时候,将镜像写入给 qemu 的 pflash 供 loader 访问。参考练习提示:“可以为镜像设置一个头结构”,并考虑到后面的练习会有在一个镜像中写入多个 app 的操作,可以想到这个头结构是在 “镜像制作” 过程中被结构的,也就是可以在 dd 命令生成的空 32M 镜像上做文章,而不需要深入到 hello_app 的编译和链接过程。所以可以考虑写一个脚本,计算编译处理过后的 bin 文件的大小,并将其写入到 32M 的空文件头部,作为这个镜像的头结构,并在后续镜像内存中写入 app 的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import struct

# 定义头部结构
header_format = '>Q'
header_size = struct.calcsize(header_format)

# 读取原始应用程序二进制文件
with open('test.bin', 'rb') as f:
app_data = f.read()
app_size = len(app_data)

# 计算应用程序大小
print(app_size)

# 创建新的二进制文件
with open('test_app.bin', 'wb') as f:
# 写入头部(包含应用程序大小)
f.write(struct.pack(header_format, app_size))

# 附加原始应用程序数据
f.write(app_data)

这是一个简单的 py 镜像处理脚本,对于一个单应用而言,头结构只需要存储一个整数,注意这里的头结构的数据存储的大小端顺序要和 loader 中解析 头结构 的代码要一致。这里选择大端序存储的 8 字节无符号整数,这样生成的镜像开头就会先存储一个 64 位的数据作为这个 app 的长度,可以用 xxd -ps 命令查看:

1
2
root@08e03dc057f5:~/phease3/test_app/test4# xxd -ps test_app.bin
000000000000000e411106e422e00008730050100000

可以看到前 8 字节保存的数据为 app 的大小:14,在 loader 里面首先根据大端序读取镜像头,就可以知道 app 的实际大小了。

Exercise 2

练习二需要在镜像中存在两个应用,完成练习一后思路便明朗了起来,直接在原先的 py 脚本上处理即可:

1
2
3
4
5
6
7
8
9
10
11
12
- header_format = '>Q'
+ header_format = '>QQ' #表示头结构存储了两个64位无符号整数(大端序)

+ with open('nop.bin', 'rb') as f:
+ app0_data = f.read()
+ app0_size = len(app0_data)

- f.write(struct.pack(header_format, app_size))
+ f.write(struct.pack(header_format, app_size, app0_size))

f.write(app_data)
+ f.write(app0_data)

在 loader app 中,解析出两个 app 的长度之后,即可计算出每个 app 的起始地址和长度,并打印相关数据即可。

Exercise 3

按照练习二的思路,制作一个包含两个 app 的镜像,loader 复制解析每个 app 的长度和将 pflash 上的 app data 拷贝到内存。并分批次的将 app 的数据拷贝到内核可执行地址空间,来逐个运行 app。hello_app 通过编译器处理后,在 arceos unikernel 中,运行 app 等价于函数调用,而编译器在编译 app 的时候已经处理好了调用栈以及 ra 寄存器等,所以只需要 jalr 到 app 的开头,等待程序运行无误后便可将控制权转交给 loader ,运行在实验二的基础上,需要修改内联汇编的代码,将最后一行 j . 删掉以保证程序正常运行。大致运行过程为从 loader app 将 hello_app 的内存载入到可读可写可执行的内存区域并跳转到该 app,运行完成后回到 loader app 接着这一流程载入并运行下一应用。

Exercise 4

根据 arceos 的框架设计

arceos arch

ax_terminate 功能在 axhal 模块被定义并通过 arceos_api 的 arceos_api::sys::ax_terminate 函数暴露给 app,loader app 要实现 terminate 调用需要引入 arceos_api crate,定义一个 abi_terminate 函数,在函数内部调用 arceos_api::sys::ax_terminate ,并在 abi_table 注册这个调用即可。这个实验实现较为简单,但是在这一步的时候 loader app 内部的 main 文件涵盖的内容太多了,所以就想到了模块化,新建了 parse 和 abi 文件,将处理镜像头结构的代码转移到 parse,abi 相关调用转移到 abi 文件。

Exercise 5

在做练习五的时候,首先先尝试多次 abi 调用,将实验五的 main 函数的代码独立出来成为 putchar 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
fn putchar(c: char) {
let arg0 = c as u8;

unsafe { core::arch::asm!("
li t0, {abi_num}
slli t0, t0, 3
add t1, t0, a7
ld t1, (t1)
jalr t1",
abi_num = const SYS_PUTCHAR,
in("a0") arg0,
) }
}

接着在 main 函数中多次调用 putchar 函数,制作成镜像后在 arceos 中运行,会报 LoadPageFault 错误。在进行了艰难的 debug 后,发现 a7 寄存器在第一次调用后,其值就失效了,猜测也许是在 print 调用中等过程导致这个寄存器的值发生了更改,而内联汇编编译过后 app 也并没有自动保存这个调用者寄存器,最终在尝试下,发现可以先将 abi_table 的地址先取出来作为一个全局的静态变量 ABI_TABLE_ADDRESS 中,然后在 putchar 函数中,先 ld 这个值的符号, 将这个值存储在一个临时寄存器中,再进行后续计算:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fn putchar(c: char) {
let arg0 = c as u8;

unsafe { core::arch::asm!("
li t0, {abi_num}
slli t0, t0, 3
ld t2, {abi_table}
add t1, t0, t2
ld t1, (t1)
jalr t1",
abi_num = const SYS_PUTCHAR,
abi_table = sym ABI_TABLE_ADDRESS,
in("a0") arg0,
) }
}

通过反汇编命令:

1
cargo objdump --release --target riscv64gc-unknown-none-elf --bin hello_app -- -d

反汇编发现 rust 若按照第一种方式,编译器会将代码编译为两次在一个寄存器中取值,然而这个值会在 abi 调用后失效。在交流群里询问才得知需要在内联汇编上加上一行 clobber_abi(“C”),这时编译器会帮你自动加上某个 abi 的 function call 的 caller-saved 的寄存器会在内联汇编被使用的提示,从而会保证程序上下文寄存器的值在函数调用前后都有效。具体的 ref: https://doc.rust-lang.org/reference/inline-assembly.html.

在测试完多次 abi 调用没有问题后,封装了一个 puts 函数:

1
2
3
fn puts(s: &str) {
s.bytes().for_each(|c| putchar(c as char))
}

Exercise 6

我们看一下练习六的要求

  1. 仿照 hello_app 再实现一个应用,唯一功能是打印字符 ‘D’。
  2. 现在有两个应用,让它们分别有自己的地址空间。
  3. 让 loader 顺序加载、执行这两个应用。这里有个问题,第一个应用打印后,不能进行无限循环之类的阻塞,想办法让控制权回到 loader,再由 loader 执行下一个应用。

对于第一个要求:我们可以在练习二的基础上制作一个包含两个 app 的镜像,并可以在 loader 中读取 pfalsh 中的 app data。对于第三个要求顺序加载执行,我们的 parse 模块会解析每个 app 的起始地址和长度,按照练习三中提到的执行流程即可,且我们删掉了汇编中的 j . ,以及在练习六种我们加上了 clobber_abi("C") 来保证寄存器在调用前后都有效,以保证程序正常返回到 loader,对于第二条要求,在第三条顺序执行加载的情况下可以得到保证。

ArceOS & Unikernel

为了解决目前已有的 OS 存在的一些问题,比如内存安全问题、组件耦合问题、开发门槛高等,ArceOS 应运而生。其设计目标与理念是能够设计出面向智能物联网设备,安全、高性能、应用兼容性高的操作系统。

其中存在一个 ArceOS 的实现 —— Unikernel,其核心思想就是将应用和OS设计为一体,OS 以库的形式存在。

对于每个应用,选择一系列以实现的组件构成的 OS 来适配(组件化),也就是对于不同的应用而言可能会存在不同形态的 OS。

Unikernel 是单应用、单地址空间、单特权级的形态。

下面简单总结一下第三阶段基本任务的附加题部分的思路。

实验一

本实验实现了一个 app 名为 loader 的外部应用加载器,然后实现了两个外部应用,熟悉了编译生成和封装二进制文件的流程。

本实验的难点在于头结构的设计以及封装命令的设计。

头结构我设计成如下模式:

app_num : u8

app_size : u16 , app_content

读取部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
let start = PLASH_START as *const u8;
let apps_num = unsafe {
let num = core::slice::from_raw_parts(start, 1);
num[0]
};
...
let mut start_now = PLASH_START + 1;
...
let size: &[u8] = unsafe { core::slice::from_raw_parts(start_now as *const u8, 2) };

let apps_size = (((size[0] as usize) << 8) + size[1] as usize) as usize;
let apps_start = (start_now + 2) as *const u8;
let code = unsafe { core::slice::from_raw_parts(apps_start, apps_size) };

封装命令一开始我只是简单的固定二进制文件大小后修改其中的参数,后面由于实验经常会修改外部应用导致大小极易变化,我就修改为自动计算封装,如下:

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
# 编译外部应用1
cd hello_app
cargo build --target riscv64gc-unknown-none-elf --release
rust-objcopy --binary-architecture=riscv64 --strip-all -O binary target/riscv64gc-unknown-none-elf/release/hello_app ./hello_app.bin
cd ..
# 编译外部应用2
cd hello_app2
cargo build --target riscv64gc-unknown-none-elf --release
rust-objcopy --binary-architecture=riscv64 --strip-all -O binary target/riscv64gc-unknown-none-elf/release/hello_app2 ./hello_app2.bin
cd ..
# 拷贝全0 32M 到 apps.bin
dd if=/dev/zero of=./apps.bin bs=1M count=32
# 封装 app_num
app_num=2
printf "$(printf '%02x' $app_num)" | xxd -r -p | dd of=apps.bin conv=notrunc bs=1 seek=0
# 当前的该封装的位置
start=1
# 封装第一个外部应用
app_size=$(stat -c %s ./hello_app/hello_app.bin)
printf "$(printf '%04x' $app_size)" | xxd -r -p | dd of=./apps.bin conv=notrunc bs=1 seek=$start
start=$(($start+2))
dd if=./hello_app/hello_app.bin of=./apps.bin conv=notrunc bs=1 seek=$start
start=$(($start+$app_size))
# 继续封装第二个外部应用
app_size2=$(stat -c %s ./hello_app2/hello_app2.bin)
printf "$(printf '%04x' $app_size2)" | xxd -r -p | dd of=./apps.bin conv=notrunc bs=1 seek=$start
start=$(($start+2))
dd if=./hello_app2/hello_app2.bin of=./apps.bin conv=notrunc bs=1 seek=$start
start=$(($start+$app_size))
# 移动封装完成的二进制文件到 ./arceos/payload 下
mkdir -p ./arceos/payload
mv ./apps.bin ./arceos/payload/apps.bin

实验二

本实验较为简单,在原来的基础上修改外部应用的汇编指令以及 loader 改用批处理的方式加载外部应用即可。

实验三

本实验设计了一个可供外部应用调用的 ABI 接口,SYS_TERMINATE 直接调用 axstd::process::exit 即可,ArceOS 已经完成好了相应的接口。

实验四

本实验实现了对 ABI 接口函数的调用。

但是本实验极容易出现奇怪的问题。

因为本身需要使用到传入的 ABI 接口地址,还需要在外部应用内实现相应的调用,所以需要熟悉一些汇编指令。

就拿我本身写的时候而言,当时在写 puts 函数的时候,调用了字符串切片的 chars 函数,但是由于这个函数用到迭代器的原因,出现了奇怪的寄存器问题,导致调用 ABI 函数返回时 RA 寄存器失效。后来改换为 as_bytes 才成功。

然后是我帮同学解决的问题,他存在地址未映射的问题。后来我发现,是他是冒用 in(reg) 和 out(reg) 导致的,传入了 ABI 接口的地址导致调用函数的时候接口地址丢失,在第二次调用的时候就会访问未映射的地址。

实验五

本实验较为容易,只需要初始化地址空间以后,对于每一个外部应用调用 RUN_START 前切换地址空间即可。

外部应用不能进行阻塞,我们需要修改 _start 函数的返回值为 (),如下:

1
unsafe extern "C" fn _start(abi_entry: usize) -> ()

总结

第三阶段项目一的基础部分让我们熟悉了 Unikernel 的框架结构,并熟悉了调用一个函数的具体封装步骤。

实现了彩色输出,移植了 HashMap,实现了一个基础的内存分配器,完成了 dtb 文件的解析和输出,修改原有的协作式调度算法fifo为抢占式调度算。

还基本实现和理解了外部多应用、ABI接口函数和多地址空间的 Unikernel模式。

2023秋冬季开源OS训练营第二阶段总结报告

首先感谢各位老师和助教,为我们呈现了这样一部优秀的Rust-OS教程。我本人是北航计算机学院大四学生,大二上过使用C语言编写的MOS操作系统课程。第二次系统性地学习操作系统,让我对OS的基本概念、本质原理、Rust编程能力等有了很大的进步和提升。下面对我在各个Lab中的收获进行总结。

Lab1

Lab1实际上引入了一个分时操作系统。该Lab较为简单,让我重新回顾了产生分时机制的原因以及具体实现。将任务分成多个时间片来执行,应用程序以为自己独占了整个CPU,是本章要理解的重点。

Lab2

这章引入了地址空间的抽象,应用程序以为自己独占了整个物理内存。我在本章学习中首先回顾了内存管理的知识,由于大二操作系统课程是基于MIPS汇编,且不区分内核地址空间和用户地址空间,因此又花了些功夫学习riscv页表机制、跳板页面机制。

Lab3

该章对进程的管理中规中矩,特色在于使用Rust语言编写,熟练了我对Rust的编程能力。

Lab4

本章主要介绍了一种文件系统的实现。我大二学习的OS属于微内核操作系统,将文件系统作为了一个用户态进程,其他进程请求文件系统服务时是通过进程间通信IPC机制实现的。而rCore的文件系统则融入到内核中,分层性感觉更强,也让我对inode有了清晰的理解,原来就是文件系统底层辨识文件的标识。

Lab5

还没时间做,想先进行第三阶段hypervisor的学习。

对Rust-OS的体会

用Rust语言编写OS,让我体会最深的不是安全性,而是方便。以前用C写OS,要专门编写释放内存的函数,而用Rust,只需要一条drop命令,甚至不需要drop,待Arc的引用计数清零,整个结构体及所包含的所有内容,全部都会被回收,真是太方便啦。