0%

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的引用计数清零,整个结构体及所包含的所有内容,全部都会被回收,真是太方便啦。

2023 rCore 二阶段总结

非常感谢清华大学开放的 rCore 课程,一番体验下来后,我对 RISC-V 上的 OS 开发有了比较清楚的认知。
课程为了一步步引导学生理解 OS 的结构和功能,费了很大的功夫在 OS 从简单到复杂的渐进式的构建,以及
各种周边设施的建设上,再次表达对课程精心准备和无私开放的感谢!

用 Rust 写 OS 是一件非常有趣的事情,传统上基建通常使用 C 语言,虽然它非常容易与汇编相对照,抽象
能力和整洁程度却不大理想。作为一门比 C 年轻了 30 多岁的语言,Rust 融入了许许多多现代的理论和特性,
在我看来,对于写 OS 来说比较突出的一点是,它的类型系统带来了更强的表达能力,而同时 RAII 思想的贯彻
有时也带来了一些挑战。

Chapter 2

这一章介绍了经典的批处理系统。该系统区分特权级,但没有地址空间的保护,一个任务容易把其他任务的数据
写坏。

Chapter 3

这一章介绍了分时多任务系统,任务通过定时器触发的中断来切换。

Chapter 4

这一章增加了分页保护,内容一下子长了很多。

不知为何,rCore 提供的 os::timer::get_time_us 在这一章的实现有问题,而上一章却没有。整数
乘除的顺序有误使得误差较大。

在这一章额外做了一下 timer 设施的封装,模仿了 Rust 标准库的 InstantDuration

rCore 的地址空间管理,以及相应的 syscall 设计的比较简单,我觉得这里使用区间树,不用 RAII 风格,
这样可能 API 和实现会更方便合理。

Chapter 5

这一章围绕进程管理以及呈现给用户态的接口。

按实验的要求,在进程被调度时为它的 stride 增加一个 pass,可能更合理的选择是在进程被切出时增加?
因为进程可能主动 yield,没有跑完一个时间片。CFS 调度器的设计看上去和这一算法的原理相似。

实验中使用了 Rust 标准库中的二叉堆来管理就绪队列,开始思考 CFS 选择红黑树而不是二叉堆的原因。

大概前阵子是和朋友聊天时,有人提到这么一个用Rust写的教学内核,产生兴趣后找来看了才发现训练营的存在。

我用Rust已经有一段时间了,但多数时候是用来写应用层面的程序,比如web服务。我对操作系统的理解几乎停留在理论,也没有做过系统开发。得知有这么一个难得的机会,那自然不可错过了。

我的文章中不会着重讲述开发的流程,但会更多地记录一些我自己遇到的比较独特的问题和经历,以及我个人的感想——我自认为我的技术实力是不如许多人的,如果你想找技术上的攻略心得,去看看别人吧。 😋

在二阶段前

不巧的是,在一阶段开始之后不久,我就遇到了一些生活上的琐事。我很快发现自己没有多少时间可以投入到rCore中了。所幸,因为已经有Rust的经验,速通rustlings也只是一晚上的功夫。

只是,可想而知之后的二阶段恐怕没这么容易了。

ch3:来自Apple的诅咒

训练营的rCore(Guide)其实已经是rCore Tutorial的再度简化版,但即便如此,第一个任务ch3还是打得我措手不及。

此前我对操作系统的理解是泛泛的,大致知道操作系统具备哪些东西,最深入的理解也只是本科课程作业中用C编写的调度算法模拟演示。对于一个真正的操作系统,该从哪里开始都不知道,比如说,在no_std的世界中,stdout从哪里来?怎么在屏幕上渲染出日志?(遗憾的是rCore并没有回答这个问题,SBI完成了这部分)

要一次性消化这么多新东西并不容易,特别是当自己以为已经把Tutorial读得足够清楚了,一打开代码库时依旧觉得无从下手,不得不再次回到Tutorial啃。在这种状况下,我僵持了差不多一个星期才弄清楚ch3究竟应该怎样完成,或者说,代码究竟该写在哪。

然而,在功能完成后,我的代码出现了一个难以理解的错误:

1
Panicked at src/bin/ch3_sleep.rs:16, assertion failed: current_time > 0

有很多人遇到了get_time_ms和get_time_us的精度问题,但我很确定我的问题不属于此类——就算我的电脑再怎么快,get_time也绝对不可能是0。

在反复重写了几个不同的实现方式之后,我开始留意到一个惊愕的事实:“get_time为0”这个错误是否发生,在时间上不均匀。

有的时候随便改动一两行代码,第一次编译可以通过测试,但第二次之后就会遇上这个错误。

在探索到深夜时,我发现在get_time加上一个println,就能把这个错误的发生概率降到零。

Commit, push, CI… Run failed.

那,把这个println删了试试吧。删除之后,在我的机器上又不能通过测试了,但出于好奇,我还是把这个commit推到了远程。

这一次,测试通过了。

这是一个只会在我的机器上出现的错误。

我使用的是QEMU 8,当然也更换了最新的pre-release RustSBI,但这……我确实没看到有其他人遇到这种问题,也无法理解。

那么只剩下一种可能了……Apple Silicon。

ch4:困难模式

虽然已经在ch3得到了教训,我还是没有在第一时间更换Linux虚拟机。我想着,时间已经不多了,同样的问题,或者更诡异的问题出现之后再换虚拟机吧,还是想贪那两秒钟的编译时间。

幸好,那来自Apple的诅咒之后再没发生过。

然而ch4的难度直线上升,尤其是对我而言——我对页表一无所知。在ch4感觉到的压力,还有那种面对代码库不知所措的感觉,比ch3还要强烈得多。

这时我已经萌生了打退堂鼓的想法,ch4无论是知识本身的难度还是任务难度都让我有点招架不住。

不过,最终还是研究明白了ch4两个函数的实现方式,以及需要为它们补上的其他函数,虽然花的时间远比预期中长。

ch5:迟来的句号

ch4完成后,二阶段原定的期限只剩下两天了,我那时还不知道通过的要求已经放宽到了ch5,在周末啃了ch5文档一天之后,我还是决定离开了。

很遗憾,没能和你们一起走下去。

……吗?

在原定的期限一周之后,我发现还有人在冲击着二阶段。

那么,来画上一个真正的句号吧。

ch5的难度倒是下降了不少,大概也是因为已经对代码库更加熟悉了。在ch5上花的时间最多的地方竟然是在cherry-pick以前的代码上,花了不少时间手动处理合并冲突,直到能通过编译和通过BASE=1。至于实现两个功能倒是比较轻松愉快,毕竟,现在已经知道代码要加在哪了。

阶梯

回过头看,在这个仓库上写的代码,好像真的不多。但是,对于一个未曾了解过操作系统本质的人,可以说是打开了新世界的大门。此刻,操作系统已经变得不再神秘,而rCore Tutorial同样也是值得反复精读的教材。

最后,也感谢老师们提供的资源以及帮助。

拖了一周总算把第二阶段的三个实验搞定了。训练营的第一阶段设计本意应该是为了让同学们学习下rust相关知识,一百道练习题也不算难,奈何自己太懒,都是在做题前看一下相关基础知识,做完题可能就忘记了,也未深入学习,因此在第二阶段阅读和编写代码时捉襟见肘。第二阶段是让完成rcore相关的实验,完成实验重要的是理解相关操作系统的概念以及整体代码框架和一些程序接口,而实验中要自己写的内容并不算太多,好多时候都是调用现有接口。如果想对操作系统和一些代码细节有更深一步的理解的话,仅靠这次实验是不够的,有时间的话还是要多看看源码和操作系统相关知识。
最后感谢这次开源操作系统训练营项目的所有贡献者们,训练营的一二阶段已使我收获颇丰,希望能在第三阶段中收获更多知识。

实验1

在实验1中,我们需要实现一个sys_task_info的系统调用,用于获取进程的信息。首先,我们要能够返回task运行的总时间,即第一次执行时间到当前时间的时间差,单位为ms,要实现这个功能,我在TaskControlBlock结构体中添加了我需要的开始时间戳,然后在第一次run该task的时候,记录开始时间戳,然后在sys_task_info中,通过current_task,找到对应的TaskControlBlock,然后计算时间差,返回即可。第二步,能够统计task的sys_call的调用次数,同上,我在TaskControlBlock结构体中添加了我需要的sys_call调用次数,然后在每次调用sys_call的时候,增加该系统调用的调用次数即trap_handler中抓取到系统调用的中断处理时处理即可,然后在sys_task_info中,通过current_task,找到对应的TaskControlBlock,然后返回即可。

实验2

实验2使用 Rust 的全局的动态内存分配器的实现 buddy_system_allocator 完成在内核中使用堆空间。 基于 RISC-V 硬件支持的 SV39 多级页表实现地址空间、物理页的分配与回收。开始划分内核地址空间及应用地址空间,实现应用程序不在需要关注应用起始地址及存放位置。相比上一章节的中断和系统调用处理过程添加了跳板页面,来避免在用户模式进入内核模式或内核模式退回用户模式时导致的地址不一致。
lab2 实验中 lab1 实验的相关内容需要重新实现,因为内存分页后在用户态传递进来的参数地址是虚拟地址,内核的访问地址映射和物理地址一致,无法通过虚拟地址对传递进来的参数赋值,所以需要将虚拟地址转换为物理地址,才能完成赋值。 sys_mmap 的实现参考系统中 insert_framed_area 的实现,添加逻辑校验给定的地址中是否包含已经映射的地址即可。sys_munmap 根据 sys_mmap 的实现反推即可实现。

实验3

在实验3中,ch3 中我学习了多道程序的放置与加载,任务切换,如何管理多道程序。然后在练习中为了实现 sys_task_info 我在原本 TaskControlBlock 的基础上添加了 task_syscall_times 数组和 first_start_time 字段来记录获取 taskinfo 所需信息。在 syscall 中调用自己封装的 add_cuurent_task_syscall_times 来实现对 task_syscall_times 记录更新。而对于 first_start_time,我在程序第一次运行时更新来记录,使得在调用 sys_task_info 时能够准确获得程序的运行时长。

感想

第一次接触结构如此清晰的OS课设,从历史演变的角度一步步搭建操作系统,每章的讲解图文并茂,将内核的功能与结构十分直观地剖析了出来。在遇到问题时老师们的讲解也帮助了我很多。

前言

最近正好在学习Rust,然后在rustcc上看到了相关的信息就想着来参加一下。记录一下整体的学习过程。

第一阶段总结

因为断断续续学习过rust相关的语法,而且以前学校上过C++的课程,所以对于一些概念上的东西理解还是比较容易上手,但是因为工作了好几年,平时的工作代码量不高。所以第一阶段做一些概念上的test还可以。到了第二阶段刚开始有点捉襟见肘。

第一阶段总结

学校的操作系统以理论偏多,而且当时确实没有特别好的实验指导,记得当时还花重金买了一本《操作系统真相还原》,但是由于一些原因没有做下去。这次正好补上。
环境为了方便,就直接选择了docker,但是docker有个问题就是gdb调试的时候每次都要打开一个窗口,要有一点docker和tmux的基础。

Chapter 1

第一章讲述了怎么写一个 Bare-metal 应用,涉及到了一些内存布局、汇编、链接器之类的知识,而且上学时候还是有好好学习的,所以压力不大,对于一些不懂的概念可以结合BlogOS,但是要注意一些细节。

Chapter 2

第二章,主要引入了特权级的概念,以前也知道这个,但是具体的是怎么实现的还真没有相关的了解。重点在于串起来整个应用的启动流程,先通过汇编指令在固定位置加载内核代码。内核代码中完成应用初始化后,最后再通过一个trap上下文来跳到应用态。整体因为不涉及地址空间所以还是比较简单。

Chapter 3 & Lab 1

第三章,引入了分时多任务,出现了任务切换的概念,核心在于任务切换是在两个 Trap 控制流间跳转,达到切换应用的目的。同时也引入了时钟中断,时钟中断主动在trap中处理切换。还有一个点就是用户态的时候,S的时钟中断是无法屏蔽的。

实验1,这个实验其实是个人感觉前三个实验最难的,当时想了好几天,一直想精确的统计应用启动的时间,甚至想过通过寄存器的值来区别应用是否被切换了,但是没有成功,最后通过和群里的同学交流后,知道了不需要很精确。就直接在 TCB 里面加启动时间字段,初始化为0,在切换的时候在加一个启动时间的判断,注意第一个应用的启动时间可以直接在first_task中设置。syscall个数通过数组计数即可。

Chapter 4 & Lab 2

第四章,引入了页表。但这个其实当时上学理解的比较透彻,所以还是比较容易的,内核只有一个内核应用空间,内核空间创建的时候就对整个剩余可用的其中几个空间做了一个恒等映射,设置完之后启动页表硬件设置,然后平滑迁移的过程以及跳板的设计是比较精妙的,还有就是初始化为0的静态/全局变量也默认在.bss段。

实验二,没有理解在指针指向的数据跨页的情况。个人理解即使数据跨页,变量在分配的时候虚拟地址是连续的,物理地址足够的情况下也应该是连续分配的,test没做相关的考虑。

mmap和unmap分配内存主要在于增加限制条件的判断,最开始没有注意启动位置要对齐,导致用例不过,其他的没有什么问题。

Chapter 5 & Lab 3

第五章,引入了进程,了解到了 “idle task”。任务切换每次都要到 idle 控制流然后又从idle控制流通过调度算法切换到新任务。

实验三 参照fork和exec的代码即可。

结语

参加本次训练营真的受益匪浅,把os的理论和实践结合起来真的很有意义。因为平时还要工作,已经超过了第二阶段上课时间一周才做完前三个实验,后面努力做完剩余两个实验。感谢本次训练营的老师,能提供这么优秀的课程和资源!!!

前言

我是一名工作了五年的社畜,先后用过php、go语言开发应用端的程序,尽管接触过一些操作系统的知识,但都是二手资料,只了解个大概。

我对问题都会追本溯源,想了解得更深一点,这样才能更好的总结归纳。

工作中用的很多工具以及知识底层都涉及到操作系统,只有了解了操作系统内核才能更好的理解整个计算机体系结构。

国庆节前在网上冲浪时,无意中看到Rust语言中文社区有关于开展开源操作系统训练营的公告,了解后,竟然是免费教程,太让人惊喜。

机不可失失不再来就立马报名参加了。

第一阶段rustlings 100道题

这一阶段的题相对比较简单,我花了一周的时间就刷完了。但也发现了自己的盲点,比如高级生命周期这块,是从没了解过。

顺带一提,我承接了队长的工作,解答队员不懂的知识点,队员都很争气,基本都没这么问我,都能完成rustlings100道题,到第一阶段截至,8名队员有7名晋级,
还有一名太忙了,没时间做,也不参加后续的了。

第二阶段 r-Core内核

第零周

也就是完成rustling之后,我开始摸索搭建实验环境的搭建和看第零章的内容,到第一周开始,环境已经搭好和看到第一章

第一周

发现第三章才是第一个实验,奋起直追,主要的时间都发在阅读文档中,我看的是详情书,看到周六才到第三章末。

周六才是完成实验,实验不是很难,主要在于理解两个异常控制流的转换,all_trap和switch,理解特权级切换硬件都干了什么,软件如何衔接。

最终在周日完成了第一个实验。

第二周

第四章比前面三章要理解的东西都多,而且也很难。可能对于我来说,操作系统这块内容比较陌生,没有一个概念,做起来就有点畏手畏脚。

如何将用户态的一个变量赋值,这个变量还可能分散到两个实际不相连的物理页,实在想不到如何下手。群里的大佬支招,让我把结构体的字段拆开分别赋值,
这个方法可行,但不通用,每个结构体字段都不一样,每个都要根据它的字段分别赋值,很麻烦。

最终我想到的方法是,写一个通用的方法,根据用户态的指针查到对应物理页面,在内核态实例化一个和用户态一样对象,转化成字节数组,将字节数组拷贝到物理页面中。
这个方法收到拷贝应用数据的方法的启示,我不用管对象是什么,只需要当成字节数组来看,逐页拷贝就行。

只要理解了虚拟地址空间和物业页帧之间的关系,mmap和munmap都是比较好实现。

第三周

前面两周只做到了实验2,接下来的一周虽然很快看完了第五章,但是完全没有时间完成实验,只能到周末完成。

实验3相对于实验2算比较简单的,前面抽象的概念都已经展开,实验3只不过增加了fork、exec之类的系统调用,用于生成新的进程。

spawn只需要获取pid和从elf中获取堆栈等信息,组装成一个taskControlBlock,并加入到任务队列即可。

stride算法的实现需要扩充两个字段stride和priority,并在每次进程切换时stride加上pass,pass= big_stride/priority。之后是修改任务管理器的fetch方法,
实现fetch能获取最小stride进程,我将VecQueue改成LinkedList,为了好从中间去掉某个节点。

总结

  • 在这5周内,学习到很多知识,以前很模糊的点现在已经清晰了,感谢训练营给了这个机会
  • 因为是打工人,上班完全没法摸鱼,学习的时间不是很多,很多时候都是在地铁上,休闲时间里来完成实验。但这五周都过得很充实,尽管也很忙
  • 接下来就要进入第三阶段,希望能学到更多的知识