0%

练习 1

main 函数中,固定设置 app_size = 32,这个显然是不合理甚至危险的。

请为 image 设计一个头结构,包含应用的长度信息,loader 在加载应用时获取它的实际大小。执行通过。

思路:

  • 在制作bin文件的时候,先将应用的长度信息存入,在存入应用数据;从bin文件中读取的过程中,也按照同样的约定即可。
1
2
3
4
5
6
7
8
app_size=$(stat -c %s ./hello_app.bin)

printf "$(printf '%04x' $app_size)" | xxd -r -p | dd of=./apps.bin conv=notrunc bs
=1 seek=0

dd if=./hello_app.bin of=./apps.bin conv=notrunc bs=1 seek=2

mv apps.bin ../arceos-unikernel/payload/

练习 2

在练习 1 的基础上,扩展 image 头结构,让 image 可以包含两个应用。

第二个应用包含唯一的汇编代码是 ebreak

如实验 1 的方式,打印出每一个应用的二进制代码。

思路:

  • 使用1个字节记录应用数量,2个字节记录应用大小
  • app_num和app_size在烧录进bin文件时,先转换为十六进制,再转换为二进制写入bin文件,1个字节使用2个十六进制位表示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 创建二进制bin文件
dd if=/dev/zero of=./apps.bin bs=1M count=32

# 通用编译命令
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

# 应用数量
app_num=2
printf "$(printf '%02x' $app_num)" | xxd -r -p | dd of=apps.bin conv=notrunc bs=1 seek=0

# 第一个应用
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=1
dd if=./hello_app/hello_app.bin of=./apps.bin conv=notrunc bs=1 seek=3

# 第二个应用
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=9
dd if=./hello_app/hello_app.bin of=./apps.bin conv=notrunc bs=1 seek=11

mv ./apps.bin ./arceos-unikernel/payload/apps.bin

练习 3

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

思路

  • 制作bin文件的过程同练习二
  • 在代码中通过for来遍历二进制文件中的应用程序即可

练习 4

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

思路:

  • 仿造已实现的两个系统调用,通过调用std::process:exit方法来退出ArceOS

练习 5

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

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

思路:

  • 需要通过linker.ld定义hello_app的内存布局,ABI_TABLE的地址由操作系统通过a0寄存器传递给应用,之后再存入a7寄存器
  • 其中的一个系统调用如下,clobber_abi("C")表示按照C调用约定来修改寄存器,用于保持Rust代码与内联汇编之间的一致性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn putchar(c: u8) {
unsafe {
core::arch::asm!("
li t0, {abi_num}
slli t0, t0, 3
mv a7, {abi_table}
add t1, a7, t0
ld t1, (t1)
jalr t1",
abi_num = const SYS_PUTCHAR,
abi_table = in(reg) ABI_ADDR,
in("a0") c,
clobber_abi("C"),
)
}
}

练习 6

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

思路:

  • 参考练习5的思路,新增一个应用,通过for循环来执行两个应用程序

总结报告

第一周练习

练习1是输出彩色字体,实现并不难。

练习2是移植hashmap,我将标准库里的 hashmap 拷贝过来,先全部注释掉,再根据测试结果逐步的去解除注释

练习三和四是实现内存分配算法和解析 fdt,解析 fdt 使用了群友推荐的库。

练习5是抢占式的 fifo,只需要将 rr 里面的抢占的代码移植过来

第二周练习

练习1 && 2

镜像格式:

|app_num|app_size|app_data|app_size|app_data|…|

一开始使用dd写入的时候一直无法读取第二个app,后来通过 hexdump 去解析镜像文件发现了问题在哪,进行了相应的修改。

练习 3

需要去掉 noreturn,让函数返回

练习 4

练习3使用 axstd 里面提供的 exit 方法就可以完成这个实验

练习 5

练习4需要注意寄存器 a7 被 rust 修改,每次调用完后需要重新设置 a7 的值,或者第一次返回的时候就保存好入口地址,以后就使用这个入口地址,还需要加上clobber_abi("C")。遍历字符串的时候使用 bytes 来迭代。

练习 6

需要先读取 app_num 和 app_size。其他就移植练习5过来就行。

代码仓库:https://github.com/uran0sH/arceos

总结报告

很高兴参加第三阶段ArceOS Unikernel的课程学习与训练,
经过第三阶段的学习,让我对rust、unikernel、linux等都有了更深入的理解。经过这些实验与练习,提升了我对rust语言的理解,感受到rust的条件编译的强大。学习了ArceOS的层次结构、模块化设计理念,让我对Unikernel的概念有了更具象化的理解。十分感谢石磊老师的细心讲解、助教老师孙应娥的热心帮忙以及群里的同学们的讨论指导。

下面是逐个练习的总结与反思。

第一周练习

练习 1 &2

练习1

支持彩色打印println!。以apps/helloworld为测试应用。
要求:不能在helloworld程序本身改,要在下面的库或更深层次的组件修改

练习2(附加题)

支持HashMap数据类型。以apps/memtest为测试应用。
要求:在ulib/axstd中支持HashMap类型

预期输出:执行 make A=apps/memtest ARCH=riscv64 run

思路:练习1就是在println!宏的实现上添加带颜色的格式控制就可以了。
练习2根据提示是参考rust官方的hash表实现,但是官方实现内容太多了,错综复杂的依赖不好解决。根据老师在群里的提示,只需要实现测试用到的函数(new、iter、insert等)就好了,然后就顺利解决了。

我在参考官方库的时候,发现官方库实际是在 用hashbrown::hash_map,进行二次封装的。所以这个练习有一个偷懒的做法

1
use hashbrown::hash_map as HashMap;

练习3

练习3

为内存分配器实现新的内存算法early,禁用其它算法。early分配算法以apps/memtest为测试应用。

这题是我卡的最久的一个练习,我一开始以为需要涉及底层内存的分配(实际上是分配好底层的内存了),实际上为了简化练习,底层内存已经分配固定了,只是需要给应用分配地址。
因此我一开始走了很多弯路,我的原本的想法是将byteAllocator和pageAllocator进行结合,或者是使用byteAllocator和BitAlloc结合。导致这样的想法是为以为BitAlloc是会控制底层的内存分配(不得不说实在太蠢了)。

尝试了各种方法后,请教了王格格同学,知道了正确的做法:维护byte_pos和page_pos来进行内存分配就好了。

实际的细节中要注意内存地址的对齐,为在练习5中发现了在练习3中的问题。即内存地址的大小和地址都要进行对齐。

练习4

解析dtb(FDT的二进制格式),打印物理内存范围和所有的virtio_mmio范围。以apps/memtest为测试应
用。
当ArceOS启动时,上一级SBI向我们传递了dtb的指针,一直会传递到axruntime,我们就在
axruntime中执行解析并打印。

思路:需要查阅相关资料,了解DTB,了解FDT相关的内容。感谢群里的郝淼同学分享的hermit-dtb库,解析dtb很方便。在解析的过程中需要注意的点是内存地址和大小的字段在reg字段中,需要将reg一分为二。

练习5

练习5

把协作式调度算法fifo改造为抢占式调度算法。让测试应用通过

难点:主要是要理解arcos中的调度模块的代码,启用preemt的feature。

第二周练习

练习1&2

练习 1:

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

练习 2:

在练习 1 的基础上,扩展 image 头结构,让 image 可以包含两个应用。
第二个应用包含唯一的汇编代码是 ebreak 。
如实验 1 的方式,打印出每一个应用的二进制代码

练习1和练习2是关系紧密的,都是要给image添加文件头,可以直接做练习2。

我们需要知道应用的数量,以及每个应用的字节长度,所以很自然地可以想到,首先文件头第一个字段应该是 应用数量,然后接着是每个应用的应用大小,最后是每个应用的应用二进制内容

因此我设计的文件格式如下
| field| length|
| ———– | ———– |
| app_amount| 2 bytes|
| app0_size | 2 bytes|
| app1_size | 2 bytes|
| …… | 2 bytes|
| appn_size | 2 bytes|
| app0_bin | app0_size bytes|
| app1_bin | app1_size bytes|
| ……. | …….|
| appn_bin | appn_size bytes|

应用数量和应用大小字段长度都是2个字节,因此我用rust写了个小程序将文件头写入文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//参数是 需要合并的app文件名称
let mut args =env::args();
args.next();
let files_number:u16=args.len() as u16;

let mut output_file: File =File::create("new_apps.bin").expect("open fails");
output_file.write(&files_number.to_be_bytes()).expect("write file numbers fails");

for file in args{
let length:u16= fs::metadata(file.clone()).expect("open file fails").len() as u16;
println!("APP:{}:{}",file,length);
output_file.write(&length.to_be_bytes()).expect("writing file length fails");

}
args=env::args();
args.next();
for file in args{
let mut source_file=File::open(file).expect("Opening file fails");
let mut buffer=Vec::new();
source_file.read_to_end(&mut buffer).expect("reading file fails.");
output_file.write_all(&buffer).expect("writing file fails.");
}

相应的在arcos中的loader程序就要先读取前两个字节,获取app数量,然后再依次读取app长度和app的内容

要注意的细节:要维持文件大小在32M,否则qemu无法加载,文件大小在Makefile文件中写死了。

练习3

练习 3:

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

思路:如何把控制权重新回到arceos中,重新返回到loader中执行

练习4

练习 4:

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

思路:熟悉arceOS的代码,寻找相关的模块代码。我的想法是关机的调用实际上是应该控制硬件来进行操作的,可能跟sbi相关的,应该在硬件抽象层,所以为主要是看了axhal模块的代码。然后发现了相关的调用代码

1
2
3
4
5
6
7
8
9
/// Shutdown the whole system, including all CPUs.
pub fn terminate() -> ! {
info!("Shutting down...");
sbi_rt::system_reset(sbi_rt::Shutdown, sbi_rt::NoReason);
warn!("It should shutdown!");
loop {
crate::arch::halt();
}
}

所以只需要在loader中调用这个terminate函数就可以实现关机操作。

练习5

练习5

照如下要求改造应用 hello_app:

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

思路:将三部分调用的内联汇编代码都封装在函数中,在入口函数中顺序调用即可。

看样子不难的程序我又卡了很久很久,跟编译器斗智斗勇了很久。

1
2
3
4
5
6
unsafe extern "C" fn _start(_entry:usize){
ENTRY=_entry;
hello();
puts("ArceOS exercise 5.");
terminate();
}

理想的执行过程是依次调用hello、puts、terminate函数后退出。但是无论怎么尝试,程序只会执行第一个调用的函数后退出。我就很疑惑:能够执行第一个调用的函数,那说明我的函数封装应该没有问题,可是为什么无论调用多少次,只会执行一次。而且调用多少次都不会改变编译生成的文件大小。然后我又生成了汇编代码进行查看。

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
_start:
addi sp, sp, -32
sd ra, 24(sp)
sd s0, 16(sp)
sd s1, 8(sp)
sd s2, 0(sp)
addi s0, sp, 32
mv s1, a0
.Lpcrel_hi0:
auipc s2, %pcrel_hi(_ZN9hello_app5ENTRY17hc4afe4ceb535dcc3E.0)
sd a0, %pcrel_lo(.Lpcrel_hi0)(s2)
mv t2, a0
#APP

li t0, 1
mv a7, t2
slli t0, t0, 3
add t1, a7, t0
ld t1, 0(t1)
jalr t1

#NO_APP
ld a0, %pcrel_lo(.Lpcrel_hi0)(s2)
bne a0, s1, .LBB0_2
ld ra, 24(sp)
ld s0, 16(sp)
ld s1, 8(sp)
ld s2, 0(sp)
addi sp, sp, 32
ret

然后发现start内确实只有我第一个调用函数的汇编代码。难道是为我的代码被编译器给优化了?
然后尝试了各种方法,改成debug模式,修改优化等级,修改使用的寄存器等。比如在Cargo.toml中修改优化等级为0。

1
2
[profile.dev]
opt-level = 0

均没有任何效果。
然后为又重新审视了每个汇编代码块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
unsafe fn hello() {
core::arch::asm!("
li t0, {abi_num}
mv a7,t2
slli t0, t0, 3
add t1, a7, t0
ld t1, (t1)
jalr t1
",
abi_num = const SYS_HELLO,
in("t2") ENTRY,
clobber_abi("C"), // 告诉编译器 clobber 了通用寄存器
options(noreturn)
)
}

然后发现了问题所在:options(noreturn)是告诉编译器这段汇编代码没有返回,也就是说后面的代码永远也不会执行,因此编译器会把后面调用的函数代码全都丢掉。因此只会执行第一次函数调用。注释掉即可解决问题。

练习6

练习 6:

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

思路:需要为每个应用分配独立的地址空间,并在跳转到app之前切换到app的地址空间,返回时再重新切换回内核空间。

因此在切换到app的地址空间之前,需要先保存当前的satp寄存器的值,执行完app后重新加载。

2023秋冬季开源操作系统训练营第三阶段总结报告

练习一、二

练习要求

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

实现过程

根据练习一、二的要求设计 image 结构如下图所示。

Alt text

image 文件中的首 0-7 的字节用于存放 ‘app 数量’,用以告诉加载器整个 image 中包含有多少个 app。后续的内容以 ‘app 数量’ 个的 app 结构块组成,每个 app 结构块 的首个 32 字节存放当前 app 的块数量,第二个 32 字节存放不满足块大小的 ‘余量’。
‘app 块大小’ 默认定义为 1024 ,即一个块大小为 1024 字节。app 的 ‘块数量’ 则为 ‘app 二进制文件大小除以 app 块大小’。’余量’定义为不满足一个块大小的剩余长度,表示为 ‘app 二进制文件大小除以 app 块大小的余数’。
跟据上述的定义,将 app 二进制文件修改为以 ‘.app’ 后缀结尾的文件,依托于 rust 的构建脚本 build.rs 查找指定目录下的 app 文件,生成上述格式的 image 文件,同时使用 dd 命令完成文件合并,需要保证 * pflash 文件必须为 32MB 大小*。
后续在 loader 中完成对 image 的解析,并在 app 需要执行时将 app 字节码内容拷贝到指定地址后,跳转到相应地址执行 app 内容。 app 结构定义 如下:

1
2
3
4
struct App<'a> {
size: u32, size: usize,
code: &'a [u8], code: &'a [u8],
}

练习三

练习要求

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

实现过程

根据上一节的练习添加第二个应用,在第一个应用执行完成后并没有返回到 loader 执行下一个下一个应用,根据调试分析,在第一个应用中并没有返回到 loader 的相关代码,需要添加添加相关代码才能返回到 loader 执行下一个 app。在每个 app 的代码中添加 ‘ret’ 指令,并删除 ‘options(noreturn)’ 以及函数的 ‘永不返回’ 标识 ‘!’,以完成在 app 执行结束后跳转回 loader。同时在构建 image 的时候需要注意 app 的顺序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#![no_std]
#![no_main]

use core::panic::PanicInfo;

#[no_mangle]
unsafe extern "C" fn _start() {
core::arch::asm!(
"
nop
ret
"
)
}

#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}

练习四

练习要求

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

实现过程

仿照 SYS_PUTCHAR 的实现,调用 axhstd 提供的 exit 函数完成相关功能个即可。

练习五

练习要求

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

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

实现过程

根据实验文档,ABI_TABLE 的基地址保存在 a7 寄存器中,在进入 app 时将 a7 寄存器的内容保。同时将 ABI 函数的调用逻辑封装为函数 ‘abi_call’,需要注意使用 ‘clobber_abi(“C”)’ 完成对寄存器的保存,同时指定 app 的 链接文件,以指定字符串存放位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/// abi 调用函数
unsafe fn abi_call(addr: usize, abi_num: usize, arg0: usize) {
core::arch::asm!("
mv a7, {abi_addr}
mv t0, {abi_num}
slli t0, t0, 3
add t1, a7, t0
ld t1, (t1)
jalr t1",
abi_addr = in(reg) addr,
abi_num = in(reg) abi_num,
in("a0") arg0,
clobber_abi("C")
)
}

练习六

练习要求

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

实现过程

在练习五的基础上,仿照 ‘hell_app’ 实现另一个 app,同时需要避免 app 中的 ‘wif’ 和无限循环等阻塞操作,避免不能返回 ‘loader’ 执行下一个 app。

总结

本次实验和练习加深了我对操作系统的理解,同时也学习了组件化操作系统 unikernel 的相关知识、对 qemu 的使用更加熟练的同时也学习了 pflash 相关的知识。对 rust 的嵌入式汇编及相关知识的掌握更加深入。同时也感谢各位老师的对本次课程的知识讲解,也感谢各位同学在群里提出的的问题以及解决方案。

总结报告

第一周实验总结

exercise 1&2: 改变println!宏以及引入haspmap

这里我是直接查询chatgpt知道可以通过在println!加入特殊符号来改变颜色,于是改变了底层的println!实现了。

后者我则是有点找不着头脑,因为本身的rust基础就是训练营的第一周和第二周,而这种程度根本无法跟得上后续的项目级别的开发,二阶段也只是调用api,到了第三阶段才发现自己连cargo管理项目和lib.rs文件都不知道是什么作用,导致自己花了很多时间去找bug,后来通过在项目群里询问大佬才发现自己的问题,当然对于引入hashmap的操作,也是借鉴了石磊老师的注释大法,通过报错信息和一步步释放自己想要的代码来逐步达到目的,这让我节省了很多时间。

exercise 3&4 引入early算法和解析dtb文件

前者我是直接通过将buddy算法和bitmap算法两个内存分配和页分配的算法结合起来拼凑起来实现的。

后者我是通过使用群里大佬分享的库,以及询问邝劲强大佬关于库的使用方法,他给予了我一些提示,于是成功实现了这个练习。

exercise 5 将协作式调度算法fifo改造为抢占式调度算法

这里我是直接参考了round_robin.算法的实现进行修改,然后比较顺利的通过。

第二周实验总结

exercise 1&2

这里我使用了非常规的方法,直接使用了脚本来获取文件大小等信息

后者通过脚本将两个文件合并到一起,后来才发现这种做法是不可取的,少了在loader的一环,这也导致我后续练习5的bug找了很久。

exercise 3&4

这两个实验则相对于简单很多,第一个实验就是改一下汇编代码’nop’和’wfi’

后者就是仿照puthello和putchar然后加一个axstd::process::exit函数的调用就行了。

exercise 5&6

其实在我心目中,反而是第一个和第二个实验最难,我对文件那块不太熟悉,练习5和练习6的思路和策略我觉得想得很快,无非是因为要写内联汇编而已,这里我也是首先犯了没有保存a7寄存器的错误,后续发现不再是loadfault的错误而是IllegalInstruction的错误信息,一直找不到错误的重点在哪里,在此过程中,在助教和群友的帮助下,尝试了通过其他寄存器如s11来保存a7,通过全局静态变量来保存a7寄存器,通过在loader传入a0而非a7寄存器到helloapp的_start函数的第一个参数来保存abi表地址一共三个办法,得出一定不是

helloapp本身的问题,而是loader的文件,然后通过排查,发现是load_size的大小太小了,一直写得指导书的32,后面改大一点通过了

练习6意识到需要改正练习1和练习2没有在loader加入头结构和相关函数的问题,于是添加上去,实现了分别调用两个地址空间的应用。

第一周练习总结

第一周的 5 个练习的主要目的是了解 ArceOS 的基本代码组织结构,为后面的任务打下基础。

练习 1、2

练习 1 要求实现彩色打印 println!,这个任务我采用了修改 axstd::println! 来实现,这是出于架构兼容性的考虑,不应修改太过底层的代码。具体实现是利用了 ANSI 转义序列。

练习 2 要求支持 HashMap 并通过测试。之所以 Rust 核心库中没有 HashMap,是因为 HashMap 需要调用产生随机数的接口,而这个接口是架构相关的。因此,练习 2 的核心是实现 axstd 可调用的 fn random() -> u128 接口。

自底向上实现 random 的过程:

  • axhal::random::random():在 axhal 层实现架构相关的 random() 接口,它调用了 axhal::time::current_ticks()
  • arceos_api::random::ax_random():定义 ax_random 接口,通过 pub use axhal::random::random as ax_random; 将其直接实现为 axhal::random::random()
  • axstd 中可调用 arceos_api::random::ax_random()

练习 3、4

练习 3 要求实现内存分配器 early。ArceOS 中包含两个内存分配器 pallocballocpalloc 用于以页单位的内存分配,balloc 被指定为 Rust 的 #[global_allocator],这样我们就可以使用 Rust 中如 VecString 等这些堆上可变长的数据类型。这两个内存分配器都包含在一个全局内存分配器 GLOBAL_ALLOCATOR 中。

early 分配器既是 palloc 又是 balloc,因此这里遇到一个问题:GLOBAL_ALLOCATOR 中使用了一对智能指针分别指向了 pallocballoc 实例,但由于 early 分配器的特点,early 在全局只有一个实例,并且其中的 pallocballoc 还需要共享数据。

因此,我将 GLOBAL_ALLOCATOR 可能的类型分为两种:

  • SeparateAllocator:分离式内存分配器,pallocballoc 是两个,当 balloc 可用内存不足时,向 palloc 请求内存
  • AIOAllocator:一体式内存分配器,pallocballoc 是同一个,两个内存分配器中共享剩余可用内存

此外,我定义了 trait GlobalAllocator,与原来的 GlobalAllocator 类中实现的方法保持一致。而后,为 SeparateAllocatorAIOAllocator 分别实现 GlobalAllocator,然后在 toml 中增加关于 early 的选项,即可达成练习目标。

练习 4 要求通过 rust_main() 的参数 dtb 解析 dtb。这一练习的关键是找到一个合适的 crate,我使用的是 hermit-dtb = "0.1.1"

练习 5

练习 5 要求将 FIFO 算法改变为抢占式的。FIFO 算法原本维护了一个队列,靠进程主动放弃 CPU 触发调度。在抢占式中,需要将原本的 List 修改为双端队列 VecDequeue,这样在 remove_task(&mut self, task: &Self::SchedItem) 中,修改为先找到 task 的下标,然后根据下标将其从队列中删除;在 put_prev_task(&mut self, prev: Self::SchedItem, preempt: bool) 如果此时是可以抢占的,就将 prev 放入队尾,否则将其放在队头,相当于调度器选择的还是当前的任务;最后,将 task_tick 的返回值恒置为 true 这样每次时钟中断发生时都能进行调度。

第二周练习总结

第二周在一个个小练习后逐渐实现了一个可以加载二进制文件的 loader,并且加载的应用有独立的地址空间。

练习 1、2

实际上练习1、2的任务是实现一个简易的文件系统。我的设计如下:

1
2
3
4
5
6
7
struct ImgHeader {
app_num: usize,
}

struct AppHeader {
app_size: usize,
}

将 1 个 ImgHeader 放在 pflash 头部,而后紧接着app_numAppHeader 用于指示每个应用的大小。

生成二进制镜像,添加这写头部信息我是用 C 语言实现的,在此不再赘述。

练习 3

批处理的方式下,先加载第一个应用,然后运行;而后加载第二个应用,然后运行。这两个应用均被加载到一个固定的地址。

在 loader 中,使用 jalr 指令通过函数调用执行一个应用,因此,在进入应用程序的主函数时,ra 寄存器已经被加载为返回地址,想要从应用程序返回,只需要将函数签名返回值部分的 -> ! 改为 -> () 或者直接删除即可。这样,应用程序生成的汇编代码最后是一条 ret 指令,恰好能返回到 loader。

练习 4、5

练习 4 将 sys_terminate 封装为对 axstd::process::exit 的调用即可。

练习 5 我将所有的 abi 调用和为一个分发函数:

1
2
3
4
5
6
7
8
fn abi_entry(abi_num: usize, arg0: usize) {
match abi_num {
SYS_HELLO => abi_hello(),
SYS_PUTCHAR => abi_putchar(arg0 as u8 as char),
SYS_TERMINATE => abi_terminate(arg0 as i32),
_ => panic!("unsupport abi call!"),
}
}

然后将这个函数的地址作为第一个参数传入应用,即通过 a0 传参:

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

在应用中,封装 putchar 等函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn putchar(c: u8) {
unsafe {
core::arch::asm!("
li a0, {abi_num}
la t0, {abi_entry_addr}
ld t0, (t0)
jalr t0",
abi_num = const SYS_PUTCHAR,
abi_entry_addr = sym ABI_ENTRY,
in("a1") c,
clobber_abi("C"),
)
}
}

实际上,所有 abi 函数都是跳转到 abi_entry 执行,不同之处在与参数的传递,我规定第一个参数 a0 是 abi 号,后面的是 abi 函数的参数。在 putchar 中,a0 被设置为 SYS_PUTCHARa1 被设置为 c 的值。注意,在内联汇编的结尾要加上 clobber_abi("C"),保持寄存器在执行这段汇编代码前后的一致,否则将出现混乱。

练习 6

目前,在批处理且每个应用地址空间相同的情况下,练习 6 实现起来比较简单,保持每个应用复用全局的应用页表 APP_PT_SV39 即可。

总结报告

练习1:为 image 设计一个头结构,包含应用的长度信息,loader 在加载应用时获取它的实际大小

这个实验刚开始的时候一头误水,以为是生成镜像后,让loader自己判断应用长度和个数,发现很难实现,后面才发现练习1的要求是在image开头加入应用长度信息,这样loader只需要获取相应的信息就可以获取实际大小和个数,实现非常轻松,后面才了解到例如elf等文件,都会有相应的头部信息给loader进行判断。

应用头

1
2
3
4
5
struct AppHeader {
start: usize,
size: usize,
content: &'static [u8],
}

后面感谢邝劲强同学提供了一个生成镜像的脚本思路,以及dd中seek选着字节跳过的用法。

练习 2:在练习 1 的基础上,扩展 image 头结构,让 image 可以包含两个应用

在完成练习1后,练习2就变得很轻松了,顺着练习1的思路只需要在镜像开头加入应用个数的信息,然后loader获取后,循环调用每一个app即可。

镜像头:

1
2
3
struct ImageHeader{
ptr_len: usize
}

循环调用:

1
2
3
4
5
6
7
8
9
let mut app_start = PLASH_START + ptr_len;
for _ in 0..app_nums {
let app_header = image_header.load_app(app_start);
println!("start:{:#}",app_header.start);
println!("size:{}", app_header.size);
println!("context:{:?}",app_header.content);
println!("......................................");
app_start += app_header.size + ptr_len;
}

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

刚开始没注意到改文档了,写的noop,然后直接报错了,调试了一下,最后看群里面才发现是nop。然后好像就没什么了,把实验2的实现放在练习3就行了。

练习4 请实现 3 号调用 -SYS_TERMINATE 功能调用,作用是让 ArceOS 退出,相当于 OS关机

这个在前面的学习中已经发现axstd中已经实现了axstd::process::exit,只需要封装下调用即可,还是比较简单的。

练习5: 按照要求改造应用 hello_app

这个应该是6个练习中最难的一个,刚开始按照实验4的思路把汇编指令全部移动到了外部的镜像中,但是和实验4不同的是,这里会执行3个系统调用(应该是大于三个的,因为打印字符串,吊调用了多次putchar),然后我执行完第一个后就发生panic,然后为对riscv汇编指令不熟,也不知道如何查看寄存器状态,这里感谢刘逸珑同学大半夜了教我如何使用gdb,最后发现是a7寄存器的值发生了改变,即原本传入的abi_table,在调用第一个函数后a7的值发生了改变,后面发现是在执行后axhal调用了a7,存了一下值,于是问题就简单了,用一个t2寄存器存一下a7的值即可。
实现的代码如下:

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
fn hello() {
unsafe {
core::arch::asm!("
li t0, {abi_num}
slli t0, t0, 3
add t1, a7, t0
mv t2,a7
ld t1, (t1)
jalr t1
mv a7,t2",
abi_num = const SYS_HELLO,
clobber_abi("C")
)
}
}

fn putchar(c: u8) {
unsafe {
core::arch::asm!("
li t0, {abi_num}
slli t0, t0, 3
add t1, a7, t0
mv t2,a7
ld t1, (t1)
jalr t1
mv a7,t2",
clobber_abi("C"),
abi_num = const SYS_PUTCHAR,
in("a0") c,

)}
}

fn terminate(exit_code: i32) {
unsafe {
core::arch::asm!("
li t0, {abi_num}
slli t0, t0, 3
add t1, a7, t0
mv t2,a7
ld t1, (t1)
jalr t1
mv a7,t2",
abi_num = const SYS_TERMINATE,
in("a0") exit_code as u8,
clobber_abi("C")
)
}

}

练习6:

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

这个比较简单只需要在练习5的基础上使用putchar函数,并且删除wfi,让程序不要等待中断即可。

个人总结

刚开始我每个项目都进去听了一下,最后被arceos的组件化思想给吸引了,加上老师上课任务都很用心,因为平时还有课,就专注于项目1了,收获不小,rust和unikernel,甚至如何编写makefile 都是在从这次开源活动中学习到的,尤其是项目1。最让我深刻的就是自己编写makefile然后成功让镜像加入到了loader里面并运行,和练习5调试那个汇编代码和寄存器,这两个地方,解决困难后的兴奋感。

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 更多考验的是思路。同一个问题使用相似的方法不断重复错误实现或纠结文档中的具体颗粒细节让我走了很多弯路。看似死胡同的路线中,及时停下尝试切换思路或寻找更多的资料补全知识脉络会让思维更清晰。而这也是我从本期训练营获得最大的收获,解决问题的思维比不断努力解决问题本身更重要。