0%

ArceOS入门

依本人的拙见,三阶段其实是为了让大家在正式开始项目实习前,先弄明白我们接下来要开发或者要完善的这个OS到底是一个什么样的OS。换句话说,它到底是基于一个什么样的思想去构建的?它跟市面上已有的那些微内核或者宏内核有哪些区别?或者更近一点,它跟我们之前做的rcore又有哪些异同?这应该都是我们在ArceOS入门阶段需要了解的一些事情,弄明白了这些概念是为了给接下来的开发和项目实习打下一个良好的基础。

组件化内核的概念

研究和实践基于组件构造内核的方法,尝试构造应对不同场景各种模式内核。

前两天才考完操作系统,在书本上,对操作系统的(其中一种)定义是这样的:

操作系统是一组能有效地组织和管理计算机硬件和软件资源,合理地对各类作业进行调度,以及方便用户使用的程序的集合

感觉有一种异曲同工之妙。即,我们也许不用将操作系统看作一个完整的整体,而是一块块组件,我们通过“胶水”进行粘合,将这些组件组合成一个程序的集合,来实现诸如计算机软硬件资源管理等功能。

在组件化内核领域:所有内核实例都可以基于组合组件的方式,从简单到复杂逐级迭代的进行构建。所有内核模式都可以看作以Unikernel模式为基础,朝向特定方向的组件化扩展。

那么什么是unikernel?

Unikernel

Unikernel模式下应用与内核处于同一特权级(均为内核态),共享地址空间。Unikernel既是内核又是应用,二者合为一体。

转换到实际场景中,我们可以理解成就是一个运行在开发板上的程序,只是这个程序直接操控了这个开发板的所有硬件资源,且它只做一个任务,比如U1.0,负责向屏幕上输出信息,随后终止,退出。

而从上面的例子中,我们又可以将一个简单的输出字符串的程序,分成几个部分。比如输出部分,架构相关的部分,给分隔开,并且各自作为组件再完善,成为了组件化内核的前身。

优势:二者之间无隔离无切换,简单高效。

劣势:二者之间无隔离无切换,安全性低。

小结:通过U_1_0:helloworld,建立最基本的框架:核心组件axhal、axruntime、api、ulib以及上层应用组件。

后续版本在该基本框架的基础上,通过扩展功能组件,扩展OS能力特性。

这里扩展的部分,就是axalloc, axtask, axsync等

从Unikernel到宏内核

相对于Unikernel,宏内核的特点:

(1) 增加一个权限较低的用户特权级来运行应用。

(2) 为应用创建独立的用户地址空间,与内核隔离。

(3) 内核运行时可以随时加载应用Image投入运行。

(4) 应用与内核界限分明。

为了将我们的工作进一步推进到宏内核,我们还需要实现几个组件,其中就是axmm,用来构造用户地址空间和axfs,加载应用程序代码到地址空间。

Hypervisor

这部分暂时没有做更多的了解,因为本人想参与宏内核的开发,更多的去学习宏内核的相关知识了。此外,软件所的任务也一直在做,于是还没来得及看。

课后练习

主要是在于bump_allocator以及对挑战题目第1题的理解。因为之前也一直在做内存分配这里的工作。

bump_allocator主要还是根据bump分配器本身的一些思想去填充对应的功能即可。

而挑战题目通过优化内存块的分配来提升内存空间的使用效率,使得Indicator这个指标有显性的提升。我的实现是基于slab的,准确来说,只是修改了某个内存块的大小限制,所以优化也没有那么明显。

祝接下来项目阶段好运!

操作系统秋季训练营第三阶段学习总结博客

大家好,我是参加这次操作系统秋季训练营的一名学员。最近我们刚刚结束了第三阶段的学习,这个阶段主要围绕组件化内核设计与实践展开,包括了从Unikernel到宏内核的过渡,以及Hypervisor扩展等内容。
虽然由于时间关系我没能亲自动手完成所有的实验,但通过PPT和一些额外的挑战任务,我还是学到了不少东西。下面我就来分享一下这段时间的学习体会。

一、课程概述

组件化内核的概念与意义

“If you don’t need it, you don’t pay for it”

组件化内核是这次训练营的核心思想,它强调的是将内核的功能分解成一系列独立的组件,这些组件可以灵活地组合起来形成不同规模和功能的内核。这种构建方式不仅提高了开发效率,还降低了维护难度,使得团队成员能够更有效地协作。
相比传统的内核设计,组件化方法允许开发者根据具体需求定制内核,从而更好地满足应用场景的需求。

主流内核模式对比

在训练营中,我们讨论了几种主流的内核模式,比如Unikernel和宏内核等。Unikernel是一种非常轻量级的内核形式,它将应用程序和内核紧密结合在一起,
共享同一地址空间,并且运行在同一个特权级别上。这使得Unikernel非常适合于需要高性能和低资源消耗的应用场景。而宏内核则提供了更加丰富的功能,支持多用户态进程,
每个进程都有自己的地址空间,这增强了系统的安全性和稳定性。但是,宏内核也因此变得更加复杂,对资源的需求也更高。

二、学习内容回顾

Unikernel模式

第一周我们从最基础的Unikernel模式开始,逐步了解了如何通过增加或替换组件来构建出一个简单的内核。
在Unikernel模式的学习过程中,我们深入探讨了如何从最基础的层面构建一个极简的操作系统环境,该环境仅包含应用程序运行所必需的核心组件。
与传统Unix内核相比,Unikernel以其高度定制化和轻量化的特点脱颖而出,通过将操作系统功能直接编译进应用代码中,实现了更小的内存占用、
更快的启动时间和更高的安全性。

宏内核扩展

第二周的内容转向了宏内核的设计,这是从Unikernel到更加复杂的操作系统的桥梁。
在M.1 UserPrivilege实验中,我们探索了如何让应用程序在较低的用户特权级别下运行,同时保持内核的安全性。这一转变意味着我们需要管理更多的地址空间,
并处理由此带来的复杂性问题。通过这样的练习,我对Unix操作系统中的用户空间与内核空间分离有了更深的理解——这种方式虽然增加了系统设计的难度,
但却极大地提高了系统的灵活性和安全性。

Hypervisor扩展

最后一周专注于Hypervisor技术,这也是当前云计算领域的一个热点话题。
通过对H.1 VirtualMode等实验的学习,我了解到Hypervisor是如何模拟出多个虚拟机环境,让它们各自拥有独立的操作系统和硬件资源。
与KVM相比,本课程中的Hypervisor实现以简化模型为tradeoff,通过直接绑定vCPU、忽略多任务并发以及省略复杂的设备模拟等方式,降低了实现复杂度,
但牺牲了一定的灵活性和性能效率。

三、字节分配器设计挑战

尽管大部分时间都在理论学习上度过,我还是尝试着完成了11.15发布的字节分配器设计挑战。虽然最终并没有完全按照题目要求找到一种理想的字节分配算法实现,
却意外发现了测试用例中存在的一个小漏洞。这对我来说也是一个不小的收获,至少证明了自己具备一定的调试能力和批判性思维Xb。
此外,这个过程也加深了我对内存管理机制的理解,尤其是对于像TLSF这样复杂的分配策略有了初步的认识。
img.png

四、个人感悟

整个训练营期间,我深刻体会到组件化设计给操作系统开发带来的巨大便利。
它可以让我们专注于特定领域的功能实现,而不必担心与其他部分的耦合度太高。然而,这也要求开发者具备较强的整体规划能力,否则很容易导致系统结构混乱。
相比之下,传统Unix风格的操作系统就在某些方面可能显得不够灵活,唉唉,历史包袱。

总之,这次训练营给了我一次宝贵的机会去深入了解现代操作系统的设计理念和技术细节。
虽然还有很多知识需要进一步消化吸收,但我相信这段经历将对我未来的职业发展产生积极影响。
希望以后还能有机会参与更多类似的活动,继续充实自己的技术栈!

五、最后

十分感谢所有的讲师和同学的付出,让我有机会接触到如此优秀的技术,也给我带来了非常丰富的学习资源。

week 1

思路:直接修改 axstd 里面的 println!宏,在输出的内容之前加上ansi的控制字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
commit 29c8b52df3644c27ea1f2cb26191e7a45c2c40a4 (HEAD -> exercise)
Author: PandaJiang <3160104094@zju.edu.cn>
Date: Wed Nov 13 22:15:34 2024 +0800

with color

Signed-off-by: PandaJiang <3160104094@zju.edu.cn>

diff --git a/arceos/ulib/axstd/src/macros.rs b/arceos/ulib/axstd/src/macros.rs
index c7cd653..760739b 100644
--- a/arceos/ulib/axstd/src/macros.rs
+++ b/arceos/ulib/axstd/src/macros.rs
@@ -18,6 +18,6 @@ macro_rules! print {
macro_rules! println {
() => { $crate::print!("\n") };
($($arg:tt)*) => {
- $crate::io::__print_impl(format_args!("{}\n", format_args!($($arg)*)));
+ $crate::io::__print_impl(format_args!("\x1b[31m{}\x1b[0m\n", format_args!($($arg)*)));
}
}

support hashmap

思路:axstd 中新增collections模块,将 alloc::collections 中的容器导入。此外,注意到std::collection::hashmap是在hashbrown作为base进行使用。尝试与std做一样的事情,发现可以直接将hashbrown模块引入作为hashmap使用,randomState就采用了默认的。

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
51
52
commit c2cab71c6640a9c9f984144f2e12f701cf347f31
Author: PandaJiang <3160104094@zju.edu.cn>
Date: Wed Nov 13 22:21:01 2024 +0800

support hash

Signed-off-by: PandaJiang <3160104094@zju.edu.cn>

diff --git a/arceos/ulib/axstd/Cargo.toml b/arceos/ulib/axstd/Cargo.toml
index c2322c5..d3a3fe8 100644
--- a/arceos/ulib/axstd/Cargo.toml
+++ b/arceos/ulib/axstd/Cargo.toml
@@ -80,3 +80,4 @@ arceos_api = { workspace = true }
axio = "0.1"
axerrno = "0.1"
kspin = "0.1"
+hashbrown = "0.15"
\ No newline at end of file
diff --git a/arceos/ulib/axstd/src/collections/mod.rs b/arceos/ulib/axstd/src/collections/mod.rs
new file mode 100644
index 0000000..6978165
--- /dev/null
+++ b/arceos/ulib/axstd/src/collections/mod.rs
@@ -0,0 +1,6 @@
+// collections
+
+extern crate alloc;
+
+pub use alloc::collections::*;
+pub use hashbrown::*;
\ No newline at end of file
diff --git a/arceos/ulib/axstd/src/lib.rs b/arceos/ulib/axstd/src/lib.rs
index 5ab0517..49784bb 100644
--- a/arceos/ulib/axstd/src/lib.rs
+++ b/arceos/ulib/axstd/src/lib.rs
@@ -55,7 +55,7 @@ extern crate alloc;

#[cfg(feature = "alloc")]
#[doc(no_inline)]
-pub use alloc::{boxed, collections, format, string, vec};
+pub use alloc::{boxed, format, string, vec};

#[doc(no_inline)]
pub use core::{arch, cell, cmp, hint, marker, mem, ops, ptr, slice, str};
@@ -70,6 +70,7 @@ pub mod process;
pub mod sync;
pub mod thread;
pub mod time;
+pub mod collections;

#[cfg(feature = "fs")]
pub mod fs;

bump 内存分配算法

  • 分配bytes

    实现较为简单,用b_pos记录当前内存bytes分配到的地方。 唯一需要注意的是

    1. 要根据aligment返回起始地址。并将分配计数+1
    2. 要检查 b_pos + size是否到达了 p_pos,是的话说明内存已经被耗尽。
  • 分配pages
    实现也较为简单,用p_pos记录当前pages分配的地方。需要注意的是

    1. p_pos是递减的,返回的地址会越来越小
    2. 要检查p_pos - size是否到达了b_pos,是的话说明内存已经被耗尽。
  • 释放bytes

    • 引用计数减1,然后检查是否为0即可,为0的话就将b——pos重置。

mv和rename操作

  • mv

    分解为 create + del 两个动作,在新的地址上写入原文件,然后删除原文件

  • rename

    文件系统有相关api,直接调用即可。

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
commit adabd01bad84c50a172a5a43af4d7cb77d75c4d7
Author: Panda Jiang <3160104094@zju.edu.cn>
Date: Mon Nov 18 08:00:23 2024 +0000

mv and rename

Signed-off-by: Panda Jiang <3160104094@zju.edu.cn>

diff --git a/arceos/examples/shell/src/cmd.rs b/arceos/examples/shell/src/cmd.rs
index 23f087f..f81723c 100644
--- a/arceos/examples/shell/src/cmd.rs
+++ b/arceos/examples/shell/src/cmd.rs
@@ -1,5 +1,6 @@
use std::fs::{self, File, FileType};
use std::io::{self, prelude::*};
+use std::string::ToString;
use std::{string::String, vec::Vec};

#[cfg(all(not(feature = "axstd"), unix))]
@@ -27,6 +28,8 @@ const CMD_TABLE: &[(&str, CmdHandler)] = &[
("pwd", do_pwd),
("rm", do_rm),
("uname", do_uname),
+ ("rename", do_rename),
+ ("mv", do_mv),
];

fn file_type_to_char(ty: FileType) -> char {
@@ -195,6 +198,84 @@ fn do_mkdir(args: &str) {
}
}

+fn do_rename(args: &str) {
+ let mut arg_v = args.split(" ");
+ let old_name = arg_v.next();
+ let new_name = arg_v.next();
+ if old_name.is_none() || new_name.is_none() || arg_v.next().is_some() {
+ println!("rename: invalid args");
+ println!("usage: rename $old_name $new_name");
+ return;
+ }
+ let old_name_s = old_name.unwrap();
+ let new_name_s = new_name.unwrap();
+ println!("try to rename {old_name_s} to {new_name_s}");
+ if let Err(e) = fs::rename(old_name_s, new_name_s) {
+ print_err!("rename", format_args!("cannot rename '{old_name_s}' to '{new_name_s}'"), e);
+ }
+ return;
+}
+
+fn do_mv(args: &str) {
+ let mut arg_v = args.split(" ");
+ let old_file_path = arg_v.next();
+ let new_file_path = arg_v.next();
+ if old_file_path.is_none() || new_file_path.is_none() || arg_v.next().is_some() {
+ println!("mv: invalid args");
+ println!("usage: mv $old_file_path $new_path_path");
+ return;
+ }
+ fn rm_one(path: &str, rm_dir: bool) -> io::Result<()> {
+ if rm_dir && fs::metadata(path)?.is_dir() {
+ fs::remove_dir(path)
+ } else {
+ fs::remove_file(path)
+ }
+ }
+ fn write_file(fname: &str, bufs: &Vec<Vec<u8>>) -> io::Result<()> {
+ let mut file = File::create(fname)?;
+ for buf in bufs {
+ file.write_all(&buf[..])?;
+ }
+ Ok(())
+ }
+ fn read_file(fname: &str, all_data: &mut Vec<Vec<u8>>) -> io::Result<()> {
+ let mut file = File::open(fname)?;
+ loop {
+ let mut buf=vec![0u8;1024];
+ let n = file.read(&mut buf)?;
+ if n > 0 {
+ all_data.push(buf);
+ } else {
+ return Ok(());
+ }
+ }
+ }
+ let old_path = old_file_path.unwrap();
+ let last_separator_pos = old_path.rfind(|c| c == '/' || c == '\\');
+ // 提取文件名
+ let filename = match last_separator_pos {
+ Some(pos) => &old_path[pos + 1..],
+ None => old_path,
+ };
+ let mut new_path = new_file_path.unwrap().to_string();
+ new_path.push('/');
+ new_path.push_str(filename);
+ let mut data:Vec<Vec<u8>> = Vec::new();
+ if let Err(e) = read_file(old_path, &mut data) {
+ print_err!("mv", format_args!("cannot read '{old_path}'"), e);
+ return;
+ }
+ if let Err(e) = write_file(&new_path, &data) {
+ print_err!("mv", format_args!("cannot open '{new_path}'"), e);
+ return;
+ }
+ if let Err(e) = rm_one(old_path, false) {
+ print_err!("mv", format_args!("cannot rm '{old_path}'"), e);
+ return;
+ }
+}
+
fn do_rm(args: &str) {
if args.is_empty() {
print_err!("rm", "missing operand");

week2

pagefault 缺页处理

populate参数为false时,uspace模块只会为将该块虚拟内存地址记录下来,实际的访问会导致 pagefault, 在该handler中处理这一错误

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
commit c0ed66e6fd0ad5af24f7ae6b2e683c43df03228a
Author: PandaJiang <3160104094@zju.edu.cn>
Date: Wed Nov 20 22:19:20 2024 +0800

add page fault func

Signed-off-by: PandaJiang <3160104094@zju.edu.cn>

diff --git a/arceos/payload/fileops_c/fileops b/arceos/payload/fileops_c/fileops
new file mode 100755
index 0000000..a457c8c
Binary files /dev/null and b/arceos/payload/fileops_c/fileops differ
diff --git a/arceos/payload/hello_c/hello b/arceos/payload/hello_c/hello
new file mode 100755
index 0000000..a44f4c6
Binary files /dev/null and b/arceos/payload/hello_c/hello differ
diff --git a/arceos/payload/origin/Makefile b/arceos/payload/origin/Makefile
new file mode 100644
index 0000000..15ba26d
--- /dev/null
+++ b/arceos/payload/origin/Makefile
@@ -0,0 +1,18 @@
+TARGET := origin
+TARGET_ELF := ../../target/riscv64gc-unknown-none-elf/release/$(TARGET)
+
+all: $(TARGET) FORCE
+
+$(TARGET): $(TARGET_ELF)
+ @rust-objcopy --binary-architecture=riscv64 --strip-all -O binary $< $@
+
+$(TARGET_ELF): src/main.rs
+ @cargo build -p origin --target riscv64gc-unknown-none-elf --release
+
+clean:
+ @rm -rf ./$(TARGET)
+ @cargo clean -p origin --target riscv64gc-unknown-none-elf --release
+
+FORCE:
+
+.PHONY: FORCE
diff --git a/arceos/payload/origin/origin b/arceos/payload/origin/origin
new file mode 100755
index 0000000..da2037d
Binary files /dev/null and b/arceos/payload/origin/origin differ
diff --git a/arceos/tour/m_1_0/src/main.rs b/arceos/tour/m_1_0/src/main.rs
index 89ef91d..f8c6077 100644
--- a/arceos/tour/m_1_0/src/main.rs
+++ b/arceos/tour/m_1_0/src/main.rs
@@ -12,6 +12,8 @@ mod task;
mod syscall;
mod loader;

+use std::println;
+
use axstd::io;
use axhal::paging::MappingFlags;
use axhal::arch::UspaceContext;
@@ -19,12 +21,27 @@ use axhal::mem::VirtAddr;
use axsync::Mutex;
use alloc::sync::Arc;
use axmm::AddrSpace;
+use axtask::TaskExtRef;
use loader::load_user_app;
+use axhal::trap::{register_trap_handler, PAGE_FAULT};

const USER_STACK_SIZE: usize = 0x10000;
const KERNEL_STACK_SIZE: usize = 0x40000; // 256 KiB
const APP_ENTRY: usize = 0x1000;

+#[register_trap_handler(PAGE_FAULT)]
+fn page_fault(vaddr: VirtAddr, access_flags: MappingFlags, is_user: bool) -> bool {
+ if !is_user {
+ panic!("page fault from kernel!!!");
+ }
+ println!("handler user page fault");
+ axtask::current()
+ .task_ext()
+ .aspace
+ .lock()
+ .handle_page_fault(vaddr, access_flags)
+}
+
#[cfg_attr(feature = "axstd", no_mangle)]
fn main() {
// A new address space for user app.
@@ -36,7 +53,7 @@ fn main() {
}

// Init user stack.
- let ustack_top = init_user_stack(&mut uspace, true).unwrap();
+ let ustack_top = init_user_stack(&mut uspace, false).unwrap();
ax_println!("New user address space: {:#x?}", uspace);

// Let's kick off the user process.

mmap syscall

与page fault处理类似,有相应api可以调用

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
commit daef0f232f4ae709afe1ef19974afa29b4594c95
Author: Panda Jiang <3160104094@zju.edu.cn>
Date: Thu Nov 21 09:22:08 2024 +0000

mmap

Signed-off-by: Panda Jiang <3160104094@zju.edu.cn>

diff --git a/arceos/exercises/sys_map/src/syscall.rs b/arceos/exercises/sys_map/src/syscall.rs
index 83b98d5..ac6624d 100644
--- a/arceos/exercises/sys_map/src/syscall.rs
+++ b/arceos/exercises/sys_map/src/syscall.rs
@@ -1,13 +1,16 @@
#![allow(dead_code)]

use core::ffi::{c_void, c_char, c_int};
+use core::usize;
use axhal::arch::TrapFrame;
use axhal::trap::{register_trap_handler, SYSCALL};
use axerrno::LinuxError;
+use axmm::{USER_ASPACE_BASE, USER_ASPACE_SIZE};
use axtask::current;
use axtask::TaskExtRef;
use axhal::paging::MappingFlags;
-use arceos_posix_api as api;
+use arceos_posix_api::{self as api, get_file_like};
+use memory_addr::{MemoryAddr, VirtAddr, VirtAddrRange};

const SYS_IOCTL: usize = 29;
const SYS_OPENAT: usize = 56;
@@ -140,7 +143,53 @@ fn sys_mmap(
fd: i32,
_offset: isize,
) -> isize {
- unimplemented!("no sys_mmap!");
+ let file = get_file_like(fd);
+ if file.is_err() {
+ return 0;
+ }
+ let file = file.unwrap();
+ let ss = file.stat();
+ if ss.is_err() {
+ return 0;
+ }
+ let size = ss.unwrap().st_size as usize;
+ if length > size {
+ debug!("incoming length is too long!!");
+ }
+ let prot_f = MmapProt::from_bits(prot);
+ if prot_f.is_none() {
+ return 0;
+ }
+ let mpflag:MappingFlags = prot_f.unwrap().into();
+ let mut buf = alloc::vec![0u8; size];
+ if let Err(_) = file.read(&mut buf) {
+ return 0;
+ }
+ let c = current();
+ let mut uspace = c
+ .task_ext()
+ .aspace
+ .lock();
+ let mut vaddr = VirtAddr::from(addr as usize);
+ if let Some(va) = uspace.find_free_area(vaddr, length.align_up_4k(), VirtAddrRange::from_start_size(USER_ASPACE_BASE.into(), USER_ASPACE_SIZE.into())) {
+ debug!("old vaddr {:?}", vaddr);
+ vaddr = va;
+ debug!("new vaddr {:?}", vaddr);
+ } else {
+ ax_println!("can't find free vaddr");
+ return 0;
+ }
+
+ if let Err(e) = uspace.map_alloc(vaddr, length.align_up_4k(), mpflag, true) {
+ ax_println!("error while mmap, {:?}", e);
+ return 0;
+ }
+ if let Ok(_) = uspace.write(vaddr, &buf) {
+ let ret:usize = vaddr.into();
+ ret as isize
+ } else {
+ 0
+ }
}

fn sys_openat(dfd: c_int, fname: *const c_char, flags: c_int, mode: api::ctypes::mode_t) -> isize {
diff --git a/arceos/modules/axhal/src/trap.rs b/arceos/modules/axhal/src/trap.rs
index a38f7e5..5fa9499 100644
--- a/arceos/modules/axhal/src/trap.rs
+++ b/arceos/modules/axhal/src/trap.rs
@@ -41,5 +41,6 @@ macro_rules! handle_trap {
/// Call the external syscall handler.
#[cfg(feature = "uspace")]
pub(crate) fn handle_syscall(tf: &TrapFrame, syscall_num: usize) -> isize {
+ debug!("handle syscall in axhal");
SYSCALL[0](tf, syscall_num)
}
diff --git a/arceos/modules/axmm/src/lib.rs b/arceos/modules/axmm/src/lib.rs
index ed23c71..e0ace44 100644
--- a/arceos/modules/axmm/src/lib.rs
+++ b/arceos/modules/axmm/src/lib.rs
@@ -19,8 +19,8 @@ use lazyinit::LazyInit;
use memory_addr::{va, PhysAddr, VirtAddr};
use memory_set::MappingError;

-const USER_ASPACE_BASE: usize = 0x0000;
-const USER_ASPACE_SIZE: usize = 0x40_0000_0000;
+pub const USER_ASPACE_BASE: usize = 0x0000;
+pub const USER_ASPACE_SIZE: usize = 0x40_0000_0000;

static KERNEL_ASPACE: LazyInit<SpinNoIrq<AddrSpace>> = LazyInit::new();

week 3

simple hv

这个题是面对用例编程,分析下面的程序

1
2
3
4
5
6
7
8
9
unsafe extern "C" fn _start() -> ! {
core::arch::asm!(
"csrr a1, mhartid", // 获取mhardid的值,按照最后
"ld a0, 64(zero)", // 访问0x40处的值
"li a7, 8",
"ecall", // shutdown
options(noreturn)
)
}

结合下面的代码,知道是需要我们将GUEST的a0设置为0x6688, a1设置为0x1234。

1
2
3
4
5
6
7

fn vmexit_handler(ctx: &mut VmCpuRegisters) -> bool {
...
assert_eq!(a0, 0x6688);
assert_eq!(a1, 0x1234);
...
}

最终实现如下:

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
commit 325d23818ae08961b4cf8e8d500b90f04dbd98d3
Author: PandaJiang <3160104094@zju.edu.cn>
Date: Tue Nov 26 23:31:54 2024 +0800

simple hv

Signed-off-by: PandaJiang <3160104094@zju.edu.cn>

diff --git a/arceos/exercises/simple_hv/src/main.rs b/arceos/exercises/simple_hv/src/main.rs
index 788ad8e..5809a64 100644
--- a/arceos/exercises/simple_hv/src/main.rs
+++ b/arceos/exercises/simple_hv/src/main.rs
@@ -3,29 +3,29 @@
#![feature(asm_const)]
#![feature(riscv_ext_intrinsics)]

+extern crate alloc;
#[cfg(feature = "axstd")]
extern crate axstd as std;
-extern crate alloc;
#[macro_use]
extern crate axlog;

-mod task;
-mod vcpu;
-mod regs;
mod csrs;
-mod sbi;
mod loader;
+mod regs;
+mod sbi;
+mod task;
+mod vcpu;

-use vcpu::VmCpuRegisters;
-use riscv::register::{scause, sstatus, stval};
+use crate::regs::GprIndex::{A0, A1};
+use axhal::mem::PhysAddr;
use csrs::defs::hstatus;
-use tock_registers::LocalRegisterCopy;
use csrs::{RiscvCsrTrait, CSR};
-use vcpu::_run_guest;
-use sbi::SbiMessage;
use loader::load_vm_image;
-use axhal::mem::PhysAddr;
-use crate::regs::GprIndex::{A0, A1};
+use riscv::register::{scause, sstatus, stval};
+use sbi::SbiMessage;
+use tock_registers::LocalRegisterCopy;
+use vcpu::VmCpuRegisters;
+use vcpu::_run_guest;

const VM_ENTRY: usize = 0x8020_0000;

@@ -50,8 +50,7 @@ fn main() {
prepare_vm_pgtable(ept_root);

// Kick off vm and wait for it to exit.
- while !run_guest(&mut ctx) {
- }
+ while !run_guest(&mut ctx) {}

panic!("Hypervisor ok!");
}
@@ -68,10 +67,11 @@ fn prepare_vm_pgtable(ept_root: PhysAddr) {
}

fn run_guest(ctx: &mut VmCpuRegisters) -> bool {
+ // ax_println!("re-run guest");
unsafe {
_run_guest(ctx);
}
-
+ // ax_println!("vm_exit");
vmexit_handler(ctx)
}

@@ -94,25 +94,34 @@ fn vmexit_handler(ctx: &mut VmCpuRegisters) -> bool {
assert_eq!(a1, 0x1234);
ax_println!("Shutdown vm normally!");
return true;
- },
+ }
_ => todo!(),
}
} else {
panic!("bad sbi message! ");
}
- },
+ }
Trap::Exception(Exception::IllegalInstruction) => {
- panic!("Bad instruction: {:#x} sepc: {:#x}",
- stval::read(),
- ctx.guest_regs.sepc
- );
- },
+ // ax_println!(
+ // "Bad instruction: {:#x} sepc: {:#x}",
+ // stval::read(),
+ // ctx.guest_regs.sepc
+ // );
+ ctx.guest_regs.gprs.set_reg(A1, 0x1234);
+ ctx.guest_regs.sepc += 4;
+ // ax_println!("next instruction {:#x}", ctx.guest_regs.sepc);
+ return false;
+ }
Trap::Exception(Exception::LoadGuestPageFault) => {
- panic!("LoadGuestPageFault: stval{:#x} sepc: {:#x}",
- stval::read(),
- ctx.guest_regs.sepc
- );
- },
+ // panic!(
+ // "LoadGuestPageFault: stval{:#x} sepc: {:#x}",
+ // stval::read(),
+ // ctx.guest_regs.sepc
+ // );
+ ctx.guest_regs.gprs.set_reg(A0, 0x6688);
+ ctx.guest_regs.sepc += 4;
+ return false;
+ }
_ => {
panic!(
"Unhandled trap: {:?}, sepc: {:#x}, stval: {:#x}",
@@ -127,9 +136,8 @@ fn vmexit_handler(ctx: &mut VmCpuRegisters) -> bool {

fn prepare_guest_context(ctx: &mut VmCpuRegisters) {
// Set hstatus
- let mut hstatus = LocalRegisterCopy::<usize, hstatus::Register>::new(
- riscv::register::hstatus::read().bits(),
- );
+ let mut hstatus =
+ LocalRegisterCopy::<usize, hstatus::Register>::new(riscv::register::hstatus::read().bits());
// Set Guest bit in order to return to guest mode.
hstatus.modify(hstatus::spv::Guest);
// Set SPVP bit in order to accessing VS-mode memory from HS-mode.

hv_pflash

思路: 用upload img脚本将pflash传到 disk.img 中,然后在处理异常地址时读取该文件,将该文件的内容映射到 pflash 所在的地址即可。

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
commit d3959ba050e8d4fcbd764415b0bac71be5a2013b
Author: Panda Jiang <3160104094@zju.edu.cn>
Date: Fri Nov 29 07:43:01 2024 +0000

read pflash.img ok

Signed-off-by: Panda Jiang <3160104094@zju.edu.cn>

diff --git a/arceos/tour/h_2_0/src/main.rs b/arceos/tour/h_2_0/src/main.rs
index 0ae2f0e..d7b6d6a 100644
--- a/arceos/tour/h_2_0/src/main.rs
+++ b/arceos/tour/h_2_0/src/main.rs
@@ -6,14 +6,14 @@ extern crate log;
#[macro_use]
extern crate alloc;
extern crate axstd as std;
+use alloc::string::String;
use alloc::string::ToString;
-use riscv_vcpu::AxVCpuExitReason;
use axerrno::{ax_err_type, AxResult};
use memory_addr::VirtAddr;
-use alloc::string::String;
-use std::fs::File;
-use riscv_vcpu::RISCVVCpu;
+use riscv_vcpu::AxVCpuExitReason;
use riscv_vcpu::AxVCpuExitReason::NestedPageFault;
+use riscv_vcpu::RISCVVCpu;
+use std::fs::File;

const VM_ASPACE_BASE: usize = 0x0;
const VM_ASPACE_SIZE: usize = 0x7fff_ffff_f000;
@@ -21,8 +21,8 @@ const PHY_MEM_START: usize = 0x8000_0000;
const PHY_MEM_SIZE: usize = 0x100_0000;
const KERNEL_BASE: usize = 0x8020_0000;

-use axmm::AddrSpace;
use axhal::paging::MappingFlags;
+use axmm::AddrSpace;

#[no_mangle]
fn main() {
@@ -36,40 +36,47 @@ fn main() {

// Physical memory region. Full access flags.
let mapping_flags = MappingFlags::from_bits(0xf).unwrap();
- aspace.map_alloc(PHY_MEM_START.into(), PHY_MEM_SIZE, mapping_flags, true).unwrap();
+ aspace
+ .map_alloc(PHY_MEM_START.into(), PHY_MEM_SIZE, mapping_flags, true)
+ .unwrap();

// Load corresponding images for VM.
info!("VM created success, loading images...");
let image_fname = "/sbin/u_3_0_riscv64-qemu-virt.bin";
- load_vm_image(image_fname.to_string(), KERNEL_BASE.into(), &aspace).expect("Failed to load VM images");
+ load_vm_image(image_fname.to_string(), KERNEL_BASE.into(), &aspace)
+ .expect("Failed to load VM images");

// Create VCpus.
let mut arch_vcpu = RISCVVCpu::init();

// Setup VCpus.
- info!("bsp_entry: {:#x}; ept: {:#x}", KERNEL_BASE, aspace.page_table_root());
+ info!(
+ "bsp_entry: {:#x}; ept: {:#x}",
+ KERNEL_BASE,
+ aspace.page_table_root()
+ );
arch_vcpu.set_entry(KERNEL_BASE.into()).unwrap();
arch_vcpu.set_ept_root(aspace.page_table_root()).unwrap();

loop {
match vcpu_run(&mut arch_vcpu) {
Ok(exit_reason) => match exit_reason {
- AxVCpuExitReason::Nothing => {},
- NestedPageFault{addr, access_flags} => {
+ AxVCpuExitReason::Nothing => {}
+ NestedPageFault { addr, access_flags } => {
debug!("addr {:#x} access {:#x}", addr, access_flags);
assert_eq!(addr, 0x2200_0000.into(), "Now we ONLY handle pflash#2.");
- let mapping_flags = MappingFlags::from_bits(0xf).unwrap();
- // Passthrough-Mode
- let _ = aspace.map_linear(addr, addr.as_usize().into(), 4096, mapping_flags);
+ // let mapping_flags = MappingFlags::from_bits(0xf).unwrap();
+ // // Passthrough-Mode
+ // let _ = aspace.map_linear(addr, addr.as_usize().into(), 4096, mapping_flags);

/*
// Emulator-Mode
// Pretend to load file to fill buffer.
- let buf = "pfld";
+ */
+ // let buf = "pfld";
aspace.map_alloc(addr, 4096, mapping_flags, true);
- aspace.write(addr, buf.as_bytes());
- */
- },
+ load_file("/sbin/pflash.img", addr, &aspace).unwrap()
+ }
_ => {
panic!("Unhandled VM-Exit: {:?}", exit_reason);
}
@@ -81,6 +88,24 @@ fn main() {
}
}

+fn load_file(file_path: &str, image_file_load_addr: VirtAddr, aspace: &AddrSpace) -> AxResult {
+ use std::io::{BufReader, Read};
+ let (ffile, size) = open_image_file(file_path)?;
+ let mut file = BufReader::new(ffile);
+ let load_region = aspace
+ .translated_byte_buffer(image_file_load_addr, 4096)
+ .expect("failed to translate kernel image load address");
+ for buffer in load_region {
+ file.read_exact(buffer).map_err(|err| {
+ ax_err_type!(
+ Io,
+ format!("Failed in reading from file {}, err {:?}", file_path, err)
+ )
+ })?
+ }
+ Ok(())
+}
+
fn load_vm_image(image_path: String, image_load_gpa: VirtAddr, aspace: &AddrSpace) -> AxResult {
use std::io::{BufReader, Read};
let (image_file, image_size) = open_image_file(image_path.as_str())?;
@@ -103,7 +128,7 @@ fn load_vm_image(image_path: String, image_load_gpa: VirtAddr, aspace: &AddrSpac
}

fn vcpu_run(arch_vcpu: &mut RISCVVCpu) -> AxResult<AxVCpuExitReason> {
- use axhal::arch::{local_irq_save_and_disable, local_irq_restore};
+ use axhal::arch::{local_irq_restore, local_irq_save_and_disable};
let flags = local_irq_save_and_disable();
let ret = arch_vcpu.run();
local_irq_restore(flags);

很高兴能够进入第三阶段,本来想着好好跟进一下,但是很遗憾与考试科目以及组里的项目冲突里,仅仅完成了前三次课程的实验,后面大体了解了一下宏内核与Hypervisor

第一次课 HelloWorld

image-20241111222207439

什么是组件化操作系统? 上面是课件里很形象的一张图。在裸机程序中,Bootloader阶段后,需要初始化寄存器、栈指针等。之后初始化串口驱动,并在此基础上打印HelloWorld,这是一套连贯的操作流程。在层次化中,可以分为Hal层和runtime层,Hal层对于对应一些基本的初始化,runtime层对应的是驱动的初始化,为上层的应用提供运行时环境。如果迭代为组件化结构,那么优势就很明显了,在hal层可以抽象出不同架构的初始化,runtime层分为了不同组件,在构建不同的裸机应用时,可以有选择的使用相关的组件,有利于系统的裁剪和定制化。

一个简单的helloword程序使用的组件大致如图所示

image-20241111223112760

课后练习

为了实现带颜色的输出,一个最简单的实现是在输出字符串前后加入相应的转义字符。

修改axstd组件中println的实现即可

1
2
3
4
5
6
7
#[macro_export]
macro_rules! println {
() => { $crate::print!("\n") };
($($arg:tt)*) => {
$crate::io::__print_impl(format_args!("\x1b[34m{}\x1b[0m\n", format_args!($($arg)*)));
}
}

image-20241111223334733

或者修改axhal中putchar的实现,在打印每一个字符时都加入相应的转义字符

1
2
3
4
5
6
7
8
9
10
11
12
13
pub fn putchar(c: u8) {
#[allow(deprecated)]
sbi_rt::legacy::console_putchar('\x1b' as usize);
sbi_rt::legacy::console_putchar('[' as usize);
sbi_rt::legacy::console_putchar('3' as usize);
sbi_rt::legacy::console_putchar('4' as usize);
sbi_rt::legacy::console_putchar('m' as usize);
sbi_rt::legacy::console_putchar(c as usize);
sbi_rt::legacy::console_putchar('\x1b' as usize);
sbi_rt::legacy::console_putchar('[' as usize);
sbi_rt::legacy::console_putchar('0' as usize);
sbi_rt::legacy::console_putchar('m' as usize);
}

image-20241111224510977

第二次课 Collections

引入了新的组件axalloc,以便于实现动态的内存分配。

课后练习

支持HashMap

使用hashbrown作为HashMap实现

image-20241115210910430

第三次课

课后练习

image-20241115211654916

实现一个简单的bump内存分配算法,比较简单,维护相应的trait即可。Page不考虑回收,回收Bytes的时候记录剩余的Bytes计数,在count为0的时候将byte_pos归零。

image-20241115212923154

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
pub struct EarlyAllocator<const PAGE_SIZE: usize> {
start: usize,
b_pos: usize,
p_pos: usize,
end: usize,
count: usize,
}

impl<const PAGE_SIZE: usize> EarlyAllocator<PAGE_SIZE> {
pub const fn new() -> Self {
Self {
start: 0,
b_pos: 0,
p_pos: 0,
end: 0,
count: 0,
}
}
}

impl<const PAGE_SIZE: usize> BaseAllocator for EarlyAllocator<PAGE_SIZE> {
fn init(&mut self, start: usize, size: usize) {
self.start = start;
self.end = start + size;
self.b_pos = start;
self.p_pos = self.end;
self.count = 0;
}
fn add_memory(&mut self, _start: usize, _size: usize) -> AllocResult {
Err(AllocError::NoMemory)
}
}

impl<const PAGE_SIZE: usize> ByteAllocator for EarlyAllocator<PAGE_SIZE> {
fn alloc(&mut self, layout: Layout) -> AllocResult<NonNull<u8>> {
let size = layout.size();
let align = layout.align();
let align_mask = align - 1;
let new_pos = (self.b_pos + align_mask) & !align_mask;
if new_pos + size > self.p_pos {
return Err(AllocError::NoMemory);
}
self.b_pos = new_pos + size;
self.count += 1;
Ok(NonNull::new(new_pos as *mut u8).unwrap())
}
fn dealloc(&mut self, _ptr: NonNull<u8>, _layout: Layout) {
if self.count > 0 {
self.count -= 1;
}
if self.count == 0 {
self.b_pos = self.start;
}
// Do nothing
}
fn total_bytes(&self) -> usize {
self.end - self.start
}
fn available_bytes(&self) -> usize {
self.p_pos - self.b_pos
}
fn used_bytes(&self) -> usize {
self.b_pos - self.start
}
}

impl<const PAGE_SIZE: usize> PageAllocator for EarlyAllocator<PAGE_SIZE> {
const PAGE_SIZE: usize = PAGE_SIZE;
fn alloc_pages(&mut self, num_pages: usize, align_pow2: usize) -> AllocResult<usize> {
if align_pow2 % Self::PAGE_SIZE != 0 {
return Err(AllocError::InvalidParam);
}
let align_pow2 = align_pow2 / Self::PAGE_SIZE;
if !align_pow2.is_power_of_two() {
return Err(AllocError::InvalidParam);
}
let p_pos = self.p_pos - num_pages * Self::PAGE_SIZE;
if p_pos < self.b_pos {
return Err(AllocError::NoMemory);
}

self.p_pos -= num_pages * Self::PAGE_SIZE;
Ok(self.p_pos)
}
fn dealloc_pages(&mut self, _pos: usize, _num_pages: usize) {
// Do nothing
}
fn total_pages(&self) -> usize {
(self.end - self.start) / Self::PAGE_SIZE
}
fn used_pages(&self) -> usize {
(self.end - self.p_pos) / Self::PAGE_SIZE
}
fn available_pages(&self) -> usize {
self.p_pos / Self::PAGE_SIZE
}
}

感悟

第三节阶段的代码相对于第二阶段清晰了不少,很后悔没有持续跟进下去,其实本人一直想学习的是基于协程异步机制的操作系统/驱动这一部分,不知道为什么没有相应的前置课程,希望第四阶段能够坚持下去,学习一下协程、异步机制以及操作系统的一些新实践。

Submit Blog

rcore-blog 这个 repo 已经挺大的了,网不好的 clone 下来还挺费时间的。

实际上可以直接在 Github 上写东西提 PR 不需要本地 clone 下来,Github 网站本身就提供了这样的功能。

打开 rcore-blog 项目主页,在绿色按钮 Code 左边有个 Add file 的按钮,可以直接用 Create new file 写所需要的 Blog 或者用 Upload files 直接上传写好的 md 文件。

需要注意的点是要切到 rcore-blog/source/_posts 这个目录下,不然文件的位置不对。

虽然直接在网站上写,文件里的内容还是要注意一下加上 Blog 需要的分类/标签/作者信息等,这些东西随便打开一个 md 文件复制一下就行。

缺点是数学公式支持和插入本地图片可能麻烦点。

注意一下 Commit changes 里上面是 Commit message, 下面只是辅助描述,一般只用改上面就行。

引用了我很喜欢的一个库 ansi_rgb, 秒了。

hashmap

在 axstd 里开个 mod collections, 再引入 hashbrown, 秒了,

alt_alloc

测试里实际上没有用到 Page alloc 的接口,写完 Byte alloc 就通过了。

rename and mv in shell

有意思的来了,实际上 fs::ax_rename 没有很好的处理 ‘./‘ 和 ‘/‘ 这些前缀后缀。

我认为在 shell 的逻辑里处理文件访问文件名是一个错误的抽象,因此我写的时候决定不在 shell 代码里处理路径问题。

观察到 ls 命令可以处理前缀后缀,一路跳转后决定在 modules/axfs/src/fs/fatfs.rsDirWrapper::rename() 中进行处理,刚好旁边就有 remove 方法可以参考。

lab1

觉得没意思就没写,看群里大佬们玩挺花的,我自己想能不能玩点 hack, 最后确认到 main 地址是可以获取的,main 的 text 内存是可读可写的。

也就意味着可以整花活来跳转到自定义的 main 函数当中去,但是想到这样就要做 platform 的适配,感觉工作量并不小,就没整。

set populating to false

AddrSpace 为入口开始找参考的辅助函数,发现直接就有 handle_page_fault 可以用,一调用就过了,看看 m2 的参考才知道 user flag 的作用。

mmap

这个练习用到的 api 挺多,需要找找 find_free_area, api::fs, LinuxError, AxError.

因为 rust 没有 goto, 所以错误处理需要包一层 fn() -> Result, 这样才能用 ? 来减少代码量。

? 只能在 ResultOption 上用就已经很强大了,如果 try_trait_v2 稳定了都不用包一层 fn 了,不敢想象有多爽。

simple hypervisor

这个练习不错,有一定难度。

首先处理 csrr a1, mhartid 这条指令。

尝试了下直接用 asm!("csrr {rd}, mhartid", out(reg) rd) 进行代理,不过如我所料的是 hypervisor 也没有 M 权限,所以不能这样弄。

但是可以注意到 mhartid 的语义是 cpu id, 所以可以直接用 axhal::cpu::this_cpu_id() 的值就行。

不过事实上根据语义应当返回的值是 vcpu id, 也就是 hypervisor 让 Guest 看到的 cpu_id, 但是我懒得添加相关的逻辑了。

把值写到上下文的 a1 寄存器之后需要把 spec 也加上指令长度。

然后处理缺页,注意到 src/task.rs 里有 TaskExt 的定义,并且压根没用上,可以知道大概需要用上。

但是我偷懒没去用,而是直接改了 vmexit_handlerrun_guest 的接口,加一个 uspace 参数,这样就能在 vmexit_handler 里修改页表了。

对于一个内核来说,所有内存地址都是可以访问的,所以直接用 uspace.map_alloc() 就行了,权限可以全加上。

一个很有意思的点是 hypervisor 寻址用的是 satp, Guest 机器寻址用的是 hgatp. 所以写入用 uspace.write 来进行。

Emulator mode device

直接把注释去掉就能通,所以改起来挺简单的。把 populate flag 置为 false 会导致 Supervisor Page Fault 错误。

刚好文件里就直接给出了一个 load_vm_image 辅助函数,直接拿来用把参数改改就完成了。size 可以通过 fs::metadata 来获取。

之前提到过 aspace 目前不支持 File 类型的 Backend, 不然就可以把 populate 置为 false 了,会快上不少。

  • 要求:
  1. 修改一个组件的实现
  2. 执行make run A=exercises/print_with_color
  • 预期:字符串输出带颜色。(具体颜色不做要求)

  • 提示:在不同层次的组件上修改,影响的输出范围不同。例如,修改axstd可能只影响println!的输出;修改axhal则可能一并影响ArceOS启动信息的颜色。

  • 本次修改主要集中在 println! 宏中,修改后的代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
     macro_rules! println {
    () => { $crate::print!("\n") };
    ($($arg:tt)*) => {
    - $crate::io::__print_impl(format_args!("{}\n", format_args!($($arg)*)));
    + // $crate::io::__print_impl(format_args!("{}\n", format_args!($($arg)*)));
    + $crate::io::__print_impl(format_args!("\u{1B}[{}m{}\u{1B}[m\n", 32, format_args!($($arg)*)));
    }
    }

[support_hashmap]: 支持HashMap类型。

  • 预备:执行make run A=exercises/support_hashmap
  • 要求:

    1. 在axstd等组件中,支持collections::HashMap
    2. 再次执行make run A=exercises/support_hashmap
  • 提示:

    1. 报错的std其实是axstd,测试用例main.rs中有”extern crate axstd as std;”
    2. 在axhal中给出了一个随机数产生函数random(),可以基于它为HashMap提高随机数支持。
  • 实现思路

HashMap 使用一个 Vec<Option<(K, V)>> 来存储键值对,每个桶(即 Option<(K, V)>)存储一个键值对,空桶为 None
custom_hash 函数用于生成键的哈希值,这里采用简单的字节移位和异或方式,返回一个 u64 类型的哈希值
iter 方法返回一个自定义的迭代器 HashMapIterator,该迭代器遍历哈希表中的所有已存储的键值对
通过线性探测法处理哈希冲突,即当发生冲突时,依次检查下一个桶直到找到空桶或找到匹配的键。

  • 测试结果
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
           d8888                            .d88888b.   .d8888b.
    d88888 d88P" "Y88b d88P Y88b
    d88P888 888 888 Y88b.
    d88P 888 888d888 .d8888b .d88b. 888 888 "Y888b.
    d88P 888 888P" d88P" d8P Y8b 888 888 "Y88b.
    d88P 888 888 888 88888888 888 888 "888
    d8888888888 888 Y88b. Y8b. Y88b. .d88P Y88b d88P
    d88P 888 888 "Y8888P "Y8888 "Y88888P" "Y8888P"

    arch = riscv64
    platform = riscv64-qemu-virt
    target = riscv64gc-unknown-none-elf
    smp = 1
    build_mode = release
    log_level = warn

    Running memory tests...
    test_hashmap() OK!
    Memory tests run OK!

其他实验由于忙于项目,暂时未做,后面有时间再补上其他的实验。

之前总结的一个starry的实现流程也记录这里,方便以后实验查阅:
Starry启动流程

组件化内核的意义与概念

基于组件构造内核的方法,去构造不同场景各种模式内核。组件之间单向依赖。

与传统设计思路的差异:

  1. 面向场景和应用需求构建内核
  2. 以统一视角看待各种模式、不同规模的内核

组件化内核相对传统构建方式的优势:

  1. 提高内核开发效率
  2. 降低内核维护难度
  3. 开展基于组件的功能复用和开发协作

内核模式:1)App和Kernel均处于S Mode.2)相互可见.3)编译形成一个Image.4)是App也是Kernel
可以通过组件增量的方式,实现扩展到宏内核与Hypervisor模式。

Unikernel

学习心得

Unikernel令我最惊讶的就是模块化的OS构建方式并通过组件增量的方式组成不同内核模式的OS.

特点

  1. 应用与内核处于同一特权级(均为内核态),共享地址空间。Unikernel既是内核又是应用,二者合为一体
  2. 优点: 应用与内核之间没有隔离和切换,简单高效
  3. 缺点: 同样因为没有隔离和切换,所以安全性较低

Unikernel框架与构成框架的核心组件是怎么来的?经历的阶段如下:

(1) 直接开发一个裸机应用来满足输出Hello的简单需求
(2) 需求增加,发现可按照通用性和Arch相关实现分层复用
(3) 引入组件化,降低耦合性、提升复用性定制性灵活性。

应用的开发

面向应用:基于features选择必要组件的最小集合。

  1. 学习了如何加载和执行外部应用程序
  2. 实现了内核与应用程序的通信机制
  3. 掌握了地址空间分离和切换的技术要点

课后练习

练习一

在打印宏的定义处,使用ANSI控制颜色即可,我使用的是:\x1B[34m\x1B[0m

练习二

我做了两版,一版就是直接引入hashbrown,这是最简单的方法,不过对于alloc和hashbrown中的有一个方法存在的冲突,我没有做处理,默认是让alloc的方法覆盖了hashbrown的。另一个版本是CV了std的实现,然后一点一点的修理,但是我还是觉得第一个更优雅一点。

练习三

我在EarlyAllocator中添加了start, end, b_pos, p_pos, count属性。然后将每一个Trait需要实现的函数实现出来。我发现PageAllocator中的部分函数并没有被使用,有些函数是直接打了unimplement()!的,但是一样能通过测试。

练习四

参数的检查可以直接参考官方函数的实现。
rename直接调用API fs::rename就可以。mv通过判断第二个参数dst是否是一个目录,如果是的话,吧文件src读入buf,然后在对应的路径写,最后删除原来的文件。

Monolithic

学习心得

宏内核在如今的市场上还是占有大量的比率,而Linux也为我们证明了,宏内核加上一些合理的缓解措施,并不意味着会比其他模式差。所以,学习宏内核是很多有意义的。

特点

基于组件化方法构建宏内核模式。

  1. Unikernel到宏内核:
    以构建最小化的宏内核为目标,说明:
    宏内核特点、与Unikernel的差异分析、框架和组件构成、具体实现示例。
  2. 地址空间管理和支持Linux应用:
    1)用户地址空间映射、缺页加载机制;
    2)支持运行最简单的Linux的原始应用

Unikernel模式的应用和内核:
(1)处于同一特权级 - 内核特权级
(2) 共享同一地址空间 - 相互可见
(3) 编译形成一个Image,一体运行
(4) Unikernel既是应用又是内核,二者合体

课后练习

练习五

使用register_trap_handler来标记对PAGE_FAULT的处理,然后实现处理函数:handle_page_fualt()
处理的实现是通过axtask的接口去实现的,得到task的ext,将aspace上锁后,调用handle_page_fualt同名函数,最后直接打印了对应的输出,然后返回一个正确处理。如果同名函数没有正确执行,那么就直接返回false.

练习六

从当前的aspace中调用find_free_area,得到length的addr_src,然后进行4K对齐,同时,对length进行对齐得到size。

prot和flags通过调用MmapProt::from_bits_truncate(prot), MmapFlags::from_bits_truncate(flags)得到,需要的mappingFlags就from(prot)即可。

调用aspace的map_alloc方法。

之后处理一下MmapFlags::MAP_ANONYMOUS。如果有这个flags,直接返回就好

如果没有MmapFlags::MAP_ANONYMOUS。则通过get_file_like得到我们所需的文件,并将其读取进入buf,然后写入aspace。

Hypervisor

学习心得

HyperVisor,即所谓的虚拟机管理程序,从代码实现来看,和宏内核思想差不多,都是一个高特权级的“内核”为低特权级的“应用”创建独属于它的“虚拟空间”,为它准备对应的资源,然后切换特权级去执行该“应用”。当“应用”发生“中断”的时候,就trap回“内核”处理。GuestOS,就像是一个特殊的应用。

特点

HyperVisor,即所谓的虚拟机管理程序,从代码实现来看,和宏内核思想差不多,都是一个高特权级的“内核”为低特权级的“应用”创建独属于它的“虚拟空间”,为它准备对应的资源,然后切换特权级去执行该“应用”。当“应用”发生“中断”的时候,就trap回“内核”处理。GuestOS,就像是一个特殊的应用。

基于一台物理计算机建立一些OS的就是虚拟机,我们这里用的是I形虚拟机,也就是不存在宿主机,直接在硬件平台运行的。
虚拟机的特点包括:同质、高校、资源受控。为了实现虚拟化,支持了以下层次的对象VM、vCPU、vMem、vDevice、vUtilities。

课后题

练习七

这里处理Trap的时候,需要更改掉panic,然后在这道题中,要求A0、A1为0x1234和0x6688,只需要在处理这些Trap的时候,将对应的寄存器进行修改就可以


笔记:https://github.com/inchinaxiaofeng/Blog-arceos

进入第三阶段当然很高兴,但时间上实在太紧,我一开始的心态是按看懂源代码中的每一步来走的,但目前只是看完了所有相关的 makefile,以及看了部分 PPT 中强调部分的源代码。实验的话也是完成了第一周的内容以及 Hypervisor 的实验一。

arceos流程小结

总体执行流程:axhal的boot.rs中先引导,用于初始化内核栈,设置初始的页表,这里映射了两个 1G 的大页,分别是低地址恒等映射以及高地址的一个映射。随后要调用 axruntime 的 rust_main 来进一步完善运行时,这里会涉及到 axfeat 里面用到的 feature 条件编译并初始化一些东西,比如启用了 paging 这个 feature 就会重新映射页表这样。调用 rust_main 的时候加了一个偏移,偏移的大小其实就是高地址映射的偏移这样。随后 axruntime 做好运行时之后,调用 main 函数。随后调用结束函数,结束整个流程。

课后练习1:支持带颜色的打印输出。

具体实现:修改axhal中putchar的实现,在打印每一个字符时都加入相应的转义字符。

关于转义字符的具体解析的话:颜色定义的部分可以参考 console_codes(4) — Linux manual pageECMA-48 Select Graphic Rendition 部分。在 makefile 中的 run_cmd 函数用于辅助输出同时会以颜色输出命令,参数 1 控制颜色为白色,参数 2 控制为灰色,随后执行命令。

颜色控制部分关键如下

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
51
52
53
54
param      result
0 reset all attributes to their defaults
1 set bold
2 set half-bright (simulated with color on a color display)
3 set italic (since Linux 2.6.22; simulated with color on a color display)
4 set underscore (simulated with color on a color display) (the colors
used to simulate dim or underline are set using ESC ] ...)
5 set blink
7 set reverse video
10 reset selected mapping, display control flag, and toggle meta flag
(ECMA-48 says "primary font").
11 select null mapping, set display control flag, reset toggle meta flag
(ECMA-48 says "first alternate font").
12 select null mapping, set display control flag, set toggle meta flag
(ECMA-48 says "second alternate font"). The toggle meta flag causes the
high bit of a byte to be toggled before the mapping table translation is
done.
21 set underline; before Linux 4.17, this value set normal intensity (as is
done in many other terminals)
22 set normal intensity
23 italic off (since Linux 2.6.22)
24 underline off
25 blink off
27 reverse video off
30 set black foreground
31 set red foreground
32 set green foreground
33 set brown foreground
34 set blue foreground
35 set magenta foreground
36 set cyan foreground
37 set white foreground
38 256/24-bit foreground color follows, shoehorned into 16 basic colors
(before Linux 3.16: set underscore on, set default foreground color)
39 set default foreground color (before Linux 3.16: set underscore off, set
default foreground color)
40 set black background
41 set red background
42 set green background
43 set brown background
44 set blue background
45 set magenta background
46 set cyan background
47 set white background
48 256/24-bit background color follows, shoehorned into 8 basic colors
49 set default background color
90..97 set foreground to bright versions of 30..37
100..107 set background, same as 40..47 (bright not supported)

Commands 38 and 48 require further arguments:
;5;x 256 color: values 0..15 are IBGR (black, red, green,
... white), 16..231 a 6x6x6 color cube, 232..255 a
grayscale ramp
;2;r;g;b 24-bit color, r/g/b components are in the range 0..255

ECMA-48 SGR 序列以 ESC[ parameters m 的格式控制显示的属性。parameters 用分号隔开,\033[92;1m 为例,033 为八进制,表示 ESC,92 和 1分别是 param,参考上文的 ECMA-48 Select Graphic Rendition 92 表示绿色明亮前景色。1 表示加粗,0 表示重置属性。

课后练习2:支持 HashMap

ax_api 作为下层 module 的抽象,随后 axstd 作为我们自己定义的标准库供用户程序调用。

我的想法其实也是直接看 std 的 HashMap 怎么实现,然后改一改,做到最小的满足测试案例中的那些操作。然后慢慢改一改,后面看到官方也用了 hashbrown 作为实现,然后我就改了改,把随机数用到 hashbrown 这边大概。然后就好了。

课后练习3:实现 alt_alloc

这个分配器应当是用作早期boot之后系统初始化阶段的内存分配器,所以实现上相对简单,每次分配都向后增长。dealloc的时候尝试减掉已经alloc的空间大小,如果为0,那就重新开始从头分配这样。

挑战题目:实现特定分配算法

这个算法的话,还是要感谢万能的群友,我一开始想的也是在TLSF基础上改一改这样。后面发现有取巧的地方,主要也是循环一直进行,奇数项的内容永不释放,所以就取巧了,会记录每次奇数次申请的空间,下次申请如果比上次的小,那就返回该地址。如果比上次大,那就重新申请,释放上次的内容这样。

达到的轮次是65536应该是,也是一个非常 unsigned int 的值。

思路详细描述:由于每次申请的空间大小会加1,相比上一次。例如,delta 为1,申请的空间依次是32 + 1, 32 * 2 + 1, 。delta 为 2,申请的空间依次是32 +2, 32*2 +2,。所以,第一次循环会反复替换掉分配器中的记录。

但是,从第二次开始,每次当 index 为 13 的时候,会替换上一次的内容。因为上一次的 13 比这次的 13 申请的空间小 1。但是呢,14 不是最后一次循环吗,因为 14 是偶数所以不管。奇数就很神奇,每次都是到 index 为 13 的时候重新申请一次空间。其余时候,其实大家返回的地址都是一样的。嘿嘿。而且,每次替换的时候都会把上次的给释放了,哎,就很神奇。

首先主体部分使用特定内存分配器 tlsf。包括申请,释放等操作。

其次,定制 alloc 函数。用 indicator 记录此时调用 alloc_pass 函数的 delta值。用 index 记录 alloc_pass 函数中的循环次数。

alloc 的时候需要谨慎计算,2的多少次方啊,当index = 14 的时候,重置为 0,然后indiactor需要加 1 啊,之类的。

这里要注意一点,源码里,分配器有个special变量,为bool类型。这里用它的主要原因在于有个很神奇的点:当indiactor为32, index = 1的时候,items需要申请 0x60 的空间,而恰好,下次申请向量空间也是 0x60 的大小。就,重复了。嗯。所以,作为单独判断条件,然后重置,第一次出现先不管,然后继续。然后,bug解决。

Hypervisor课后练习1

hypervisior流程分析:这里初始化阶段我们的hyper会布置好一个现场类似我们在宏内核中设置好sstatus寄存器那样,随后通过sret到GUEST中执行,GUEST中遇到无法处理的情况又会陷入Hypervisior中,通过对不同类型的陷入执行不同的代码,处理好用户的错误设置寄存器等操作,随后返回GUEST继续执行。

这里的第一个实验比较简单:只需要在处理错误函数中设置寄存器的值,然后设置sepc += 4,让GUEST去执行出错指令的下一条指令即可。

Hypervisior课后练习2

这里会涉及到虚拟化里的地址二阶段映射。主要流程:GUEST中的虚拟地址先通过 VSATP 翻译,通过 GVA 到 GPA,然后通过 HGATP 把GPA翻译到 HPA 这样。

课后练习尚未完成,嗯。

Hypervisior课后练习3

这里涉及 GUEST 的中断。主要涉及时钟中断,一种实现方式是当 GUEST 配置 RTC 的时候会陷入 Hyper,由 Hyper 来设置时钟,当时钟触发的时候,Hyper 会触发 GUEST 的时钟中断位这样。Hyper在设置完之后应该是会关中断的,待下次GUEST设置中断的时候再打开,这样保持一致。

课后练习尚未完成,嗯。

总结

这里总结的话,三阶段实在不完美,时间上太赶了,然后各种东西都想弄明白细节,这样就很来不及。原本还准备看RISCV的指令集虚拟化扩展这样,也只看了一小部分,内容上其实不算多,但要补的内容比较多,就比较费劲。

本阶段实验由于处于学校考试密集时间,因此没有完全跟课。主要认真学习了Hypervisor虚拟化的内容,也是我项目实习阶段最想要实践的内容。
基本理清Hypervisor运行原理和过程,课后作业完成了一部分。

ArceOS

2024.11.09

Rust 相关

关键字sym

在Rust中,sym 关键字用于引用函数或符号名称,通常出现在 asm! 宏的内联汇编代码中。sym 允许在汇编代码中引用Rust中的函数或全局变量,以便编译器能正确地将这些引用转换为相应的内存地址或符号。

原始字符串

在Rust中,字符串前r#"str"表示原始字符串字面量(raw string literal)。使用r前缀的字符串可以避免转义字符的干扰,直接包含特殊字符,如反斜杠\或双引号"

1
let json = r#"{"name": "John", "age": 30}"#;

option_env!宏

在Rust中,option_env! 宏用于在编译时获取环境变量的值,并将其作为 Option<&'static str> 返回。

Rust条件编译

Rust 中的条件编译允许根据特定条件编译或排除某些代码片段。作用范围通常为其紧邻的代码块或声明项。对于较大的代码片段或模块内容,可以将 #[cfg(...)] 等属性放在模块或块的开头,实现更大范围的条件编译。

  • #[cfg(...)] 属性
  • #[cfg_attr(...)] 属性
    有条件地应用属性。适合在某些条件下添加其他属性。
  • cfg!
    用于在运行时检查条件编译状态,并返回布尔值。

2024.11.10

ArceOS 部分目录结构

1
2
3
4
5
.
├── api/ // 用户调用系统的接口
├── modules/ // 操作系统模块
├── platforms/ // 硬件平台相关配置
└── ulib/ // 用户lib

ArceOS Makefile

如果设置了PLATFORM变量,会覆盖ARCH变量。

1
2
3
4
# ./Makefile
PFLASH ?= y
PFLASH_IMG ?= pflash.img # 并行闪存(Parallel Flash)
DISK_IMG ?= disk.img # FAT32文件系统,/mnt/sbin是./target/riscv64gc-unknown-none-elf/release/origin

主要编译过程见./scripts/make/build.mk

QEMU相关

qemu-system-riscv64 -m 128M -smp 1 -machine virt -bios default -kernel tour/u_1_0/u_1_0_riscv64-qemu-virt.bin -drive if=pflash,file=/home/zone/Code/Rust/oscamp/arceos/pflash.img,format=raw,unit=1 -nographic -D qemu.log -d in_asm,int,mmu,pcall,cpu_reset,guest_errors

  • -m 128M:分配128MB的内存给虚拟机。
  • -smp 1:指定虚拟机使用1个CPU。
  • -machine virt:指定使用虚拟化的机器类型。
  • -bios default:使用默认的BIOS。
  • -kernel tour/u_1_0/u_1_0_riscv64-qemu-virt.bin:指定要加载的内核镜像文件。
  • -drive if=pflash,file=/home/zone/Code/Rust/oscamp/arceos/pflash.img,format=raw,unit=1:指定一个闪存驱动器,使用原始格式的镜像文件
  • -nographic:禁用图形输出,使用纯文本模式。
  • -D qemu.log:将QEMU的日志输出到qemu.log
  • -d in_asm,int,mmu,pcall,cpu_reset,guest_errors:启用详细的调试信息,包括指令、中断、内存管理单元、过程调用、CPU重置和来宾错误。

Rust相关

build.rs

用于编译第三方的非Rust代码,只需在根目录下添加一个build.rs文件,Cargo会优先编译和执行该构建脚本,然后再构建整个项目。

Workspace

一个工作空间是由多个 package 组成的集合,它们共享同一个 Cargo.lock 文件、输出目录和一些设置(例如 profiles : 编译器设置和优化)。组成工作空间的 packages 被称之为工作空间的成员。

工作空间有两种类型:

  • root package。若一个 package 的 Cargo.toml 包含了 [package] 的同时又包含了 [workspace] 部分,则该 package 被称为工作空间的根 package。
  • 虚拟清单(virtual manifest)。若一个 Cargo.toml 有 [workspace] 但是没有 [package] 部分,则它是虚拟清单类型的工作空间。

特性:

  • 所有package共享同一个位于根目录的Cargo.lock
  • 所有package共享同一个输出目录,默认是位于根目录下的target目录
  • 只有根目录的Cargo.toml才能包含 [patch], [replace][profile.*],而成员的 Cargo.toml 中的相应部分将被自动忽略
1
2
3
4
5
6
7
[workspace]
resolver = "2"
members = [
"modules/axalloc",
"modules/axconfig",
# ...
]

2024.11.11

ArceOS在riscv64-qemu-virt中的部分启动流程

  • 下层的SBI初始化完成后将跳转到modules/axhal/src/platform/qemu_virt_riscv/boot.rs::_start()
    • 设置boot_stack
    • 设置初始page_table
    • 开启SV39分页,清除地址映射缓存
    • sp += phys_virt_offset
  • 跳转到modules/axhal/src/platform/qemu_virt_riscv/mod.rs::rust_entry()
    • 清零bss段
    • 设置CPU_ID和IS_BSP
    • 设置trap向量表
    • 设置系统启动时间
  • 跳转到modules/axruntime/src/lib.rs::rust_main()
    • enable Logging
    • 初始化全局堆内存分配器
    • 冲映射kernel各个段的地址空间
    • platform相关的初始化
    • 初始化调度器
    • 设备与驱动初始化
    • 如果有启动其他CPU
    • 初始化中断
    • 等待所有CPU启动
    • 执行用户程序
    • 清理然后退出

axalloc

接口

  • Rust Trait #[global_allocator]:static GLOBAL_ALLOCATOR: GlobalAllocator
    1
    2
    3
    4
    5
    6
    7
    8
    pub struct GlobalAllocator {
    balloc: SpinNoIrq<DefaultByteAllocator>,
    palloc: SpinNoIrq<BitmapPageAllocator<PAGE_SIZE>>,
    }
    // 必须为GlobalAllocator实现GlobalAlloc Trait
    // 全部内存先交给palloc管理
    // 然后给balloc预先分配一小块内存作为堆空间
    // 当调用balloc.alloc遇到堆空间不够用时,向palloc请求更多的内存
    GlobalAllocator提供alloc_pages(&self, num_pages: usize, align_pow2: usize)dealloc_pages(&self, pos: usize, num_pages: usize)函数用于页的分配和回收
  • 供外部调用的函数:
    • global_allocator() -> &'static GlobalAllocator
    • global_init(start_vaddr: usize, size: usize)
    • global_add_memory(start_vaddr: usize, size: usize) -> AllocResult

算法

  1. TLSF (Two-Level Segregated Fit)
    通过将内存块分成多个大小类别(segregated fit)来实现快速分配。TLSF 的主要特点包括:
    快速分配和释放:分配和释放操作的时间复杂度接近 O(1)。
    低碎片率:通过精细的大小类别划分,减少内存碎片。
    适用于实时系统:由于其高效性和确定性,TLSF 常用于实时系统。

  2. Slab Allocator
    通过预先分配一组称为“slab”的内存块来管理对象。Slab 分配器的主要特点包括:
    高效的对象分配:适用于频繁分配和释放固定大小对象的场景。
    减少碎片:通过预先分配和重用内存块,减少内存碎片。
    缓存对象:可以缓存已分配的对象,减少分配和释放的开销。

  3. Buddy Allocator
    通过将内存块递归地分割成两部分来管理内存。Buddy 分配器的主要特点包括:
    快速分配和释放:分配和释放操作的时间复杂度为 O(log n)。
    合并相邻块:当两个相邻的内存块都空闲时,可以合并成一个更大的块,减少碎片。
    适用于大块内存分配:适合需要分配和释放大块内存的场景。

  4. BitmapPage Allocator
    每个内存块对应一个位,位的状态表示该块是否已分配。BitmapPage 分配器的主要特点包括:
    简单实现:位图结构简单,易于实现和管理。
    快速查找:通过位图可以快速查找空闲块。
    适用于小块内存分配:适合需要频繁分配和释放小块内存的场景。

2024.11.12

Rust 相关

内属性和外属性

在 Rust 中,内属性(inner attribute)和外属性(outer attribute)用于不同的上下文,它们的放置顺序需要符合特定的规则。

  • 外属性(outer attribute)用在项的外部(如函数、模块、结构体等),以 #[attribute] 的形式表示。
  • 内属性(inner attribute)用在代码块的内部,通常用于全局的配置和文件级别的作用,以 #![attribute] 的形式表示。

Rust 不允许在某些位置先使用外属性再使用内属性。具体来说,内属性必须在模块或文件的最开头,并且需要放在任何外属性之前。

2024.11.13

axstd

HashMap

HashMap:数组+链表(+红黑树)。链表(或加上红黑树)针对出现hash冲突的情况

要在axstd里实现基础的HashMap功能,最简单的方法是在lib.rs里加上pub use hashbrown::hash_map::HashMap as HashMap

或者仿照std添加collections模块,目录结构如下:

1
2
3
4
5
6
7
ulib/axstd/src

└── collections
   ├── hash
   │   ├── map.rs
   │   └── mod.rs
   └── mod.rs

部分代码如下,主要是在RadomState的new函数中调用底层模块提供的生产随机数接口来设置keys:

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
use hashbrown::hash_map::HashMap as base_hashmap;
// ...

#[derive(Clone)]
pub struct RandomState {
k0: u64,
k1: u64,
}

impl RandomState {
pub fn new() -> Self {
let random_u128 = arceos_api::misc::random();
RandomState {
k0: random_u128 as u64,
k1: (random_u128 >> 64) as u64,
}
}
}

impl BuildHasher for RandomState {
type Hasher = DefaultHasher;
fn build_hasher(&self) -> DefaultHasher {
DefaultHasher(SipHasher::new_with_keys(self.k0, self.k1))
}
}

...

#[allow(deprecated)]
#[derive(Clone, Debug)]
pub struct DefaultHasher(SipHasher);

impl Hasher for DefaultHasher {
//...
}

pub type HashMap<K, V, S = RandomState> = base_hashmap<K, V, S>;

2024.11.14

QEMU相关

PFlash

QEMU的PFlash模拟闪存磁盘,启动时自动从文件加载内容到固定的MMIO区域,对读操作不需要驱动,可以直接访问。写操作仍需要驱动。

rust-analyzer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// .vscode/settings.json
{
// 避免不必要的检查
"rust-analyzer.check.allTargets": false,
// 检查程序时传递给cargo的features
"rust-analyzer.cargo.features": ["axstd"],
"rust-analyzer.cargo.target": "riscv64gc-unknown-none-elf",
"rust-analyzer.cargo.targetDir": "/path/to/oscamp/arceos/target",
// "rust-analyzer.cargo.extraArgs": [
// "--target=riscv64gc-unknown-none-elf",
// "--features=axstd"
// ],
// "rust-analyzer.cargo.extraEnv": {
// }
}

2024.11.17

unwrap()调用的问题

每次调用 unwrap() 都会返回一个新的实例,而不是同一个实例。因此通过unwrap修改数值无法生效。
可以通过if let Some(ref mut T)获取可变引用来修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#[derive(Copy, Clone, Debug)]
struct A {
a: i32,
}

fn main() {
let a = Some(A { a: 0 });
println!("{}", a.unwrap().a);
//0
a.unwrap().a = 1;
println!("{}", a.unwrap().a);
// 0
if let Some(ref mut tmp) = a {
tmp.a = 2;
}
println!("{}", a.unwrap().a);
// 2
}

2024.11.18

lab1

Heap区域划分

通过分析lab1的测试样例代码可知alloc行为可以抽象为以下三种:

  1. 固定大小且不回收的Vec,这些Vec的位置和大小在分配完成后就不再变化。
  2. 固定大小但会回收的Vec,且回收顺序与分配顺序相反。
  3. 可变大小的Vec,这些Vec主要用来临时存储前两中Vec的地址
    这一类Vec在任意时刻的数量不超过4个,且长度相对较段,因此选择在Heap区域的开头为其预留固定大小的空间(例如4KB)。

针对第一种类型,从前向后分配,针对第二种类型,从后向前分配。

特殊处理

为了简化实现,在系统将所有Heap空间分配给LabByteAllocator管理之前,LabByteAllocator的alloc函数都会返回分配失败。

除此之外,为了能够让Heap空间能够全部分配给LabByteAllocator而没有浪费,需要对LabByteAllocator的total_bytes()函数进行特殊处理,而不是返回实际的总字节数。

在axalloc中,当遇到分配器分配失败时,会将分配器的total_bytes()方法的返回值作为扩展的空间大小。假设Heap空间的结束地址是HEAP_END,当前分配器管理空间的结束地址是END,让total_bytes()返回(HEAP_END-END)/2就能尽可能将HEAP空间分配给LabByteAllocator。

2024.11.24

Rust相关

rust-analyzer

在编译时makefile会有一些预处理行为,例如设置环境变量、添加cargo参数等等。为了保持rust-analyzer分析代码时的环境与实际编译时尽量保持一致,需要添加相应的配置。

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
{
"rust-analyzer.check.overrideCommand": [
"cargo",
// 改变当前目录、
"-C",
"/path/to/oscamp/arceos/tour/u_7_0",
"check",
//添加unstable flag
"-Z",
"unstable-options",
"--workspace",
"--message-format=json-diagnostic-rendered-ansi",
"--manifest-path",
"/path/to/oscamp/arceos/tour/u_7_0/Cargo.toml",
"--keep-going",
"--release",
],
// 添加需要的features
"rust-analyzer.cargo.features": [
"axstd/irq",
// "axstd/rtc",
],
"rust-analyzer.cargo.target": "riscv64gc-unknown-none-elf",
"rust-analyzer.cargo.targetDir": "/path/to/oscamp/arceos/target",
"rust-analyzer.cargo.extraEnv": {
"AX_PLATFORM": "riscv64-qemu-virt",
}
}

RISC-V相关

异常Exception和中断Interrupt

在 RISC-V 架构语境下,异常和中断都是一种Trap,但他们被触发的原因不同。

  • 异常与当前CPU的指令执行是同步的,异常被触发的原因一定能追溯到某条指令的执行,会在执行下一条指令前先进入异常处理流程。
  • 中断则异步与当前正在执行的指令,也就是说中断来自于哪个外设以及中断如何触发完全与处理器正在执行的当前指令无关。相比于异常,中断与特权级的联系更加紧密。
    RISC-V 的中断可以分成三类:
    • 软件中断 (Software Interrupt):由软件控制发出的中断
    • 时钟中断 (Timer Interrupt):由时钟电路发出的中断
    • 外部中断 (External Interrupt):由外设发出的中断

arceos在axhal::arch::riscv::trap::riscv_trap_handler()中指定了异常的处理函数,然后通过全局静态变量IRQ: [fn(usize) -> bool]注册中断处理函数。axhal::irq::handler_irq用于分发IRQ对应的中断到实际的处理函数。
axhal::irq::register_handler用于初始化注册中断处理函数。

ArceOS相关

初始任务

在初始化阶段创建了三个任务:

  • idle:持续调用yield_now()函数的死循环
  • main:init进程
  • gc:回收所有退出的任务和相应的资源

2024.11.26

ArceOS相关

axtask

接口

  • spawn
  • yield_now:Current task gives up the CPU time voluntarily, and switches to another ready task.
  • sleep
  • exit

axdriver

for_each_drivers!

这个宏的作用主要是为每一种设备类型(virtio-net、ramdisk等)添加统一的处理代码,类似于for循环遍历所有的设备类型。

PCIe ECAM

PCI协议定义了256bytes配置空间,PCIe将配置空间扩展到了4KB。

PCI Express配置模型支持两种配置空间访问机制:

  • PCI-compatible Configuration Access Mechanism
  • PCI Express Enhanced Configuration Access Mechanism
    CAM模式是PCI协议定义的访问方式,而ECAM则是PCIe协议定义的访问方式。

一般来说,系统地址空间会预留256MB(256bus * 32dev * 8func * 4k)空间给ECAM机制使用,当然如果系统比较差,不支持那么多bus号,也可以预留小一点。

Function是PCI设备中的独立子模块,一个设备可以包含多个功能,每个功能可以执行不同的任务。例如:一个PCI网卡可能同时具有以太网控制器(Function 0)和无线网卡(Function 1)两种功能。

axfs

lookup

在当前的axfs::fs::fatfs::lookup实现中,会对传入的path参数先调用open_file,如果返回Err则再调用open_dir,因此如何path传入的是一个文件夹路径,会在err信息中看到Is a directory报错。

同步

对编译器和CPU实施的重排指令行为进行控制

  • C++ memory_order_relaxed/Rust Ordering::Relaxed:无fencing作用,cpu和编译器可以重排指令
  • C++ memory_order_release/Rust Ordering::Release:前面的访存指令不能排到这条指令之后
  • C++ memory_order_acquire/Rust Ordering::Acquire:后面的访存指令不能排到这条指令之前
  • C++ memory_order_acq_rel/Rust Ordering::AcqRel:acquire+release
  • C++ memory_order_seq_cst/Rust Ordering::SeqCst:AcqRel+所有使用SeqCst的指令严格有序
  • C++ memory_order_acquire:

2024.11.28

ArceOS相关

VFS

VFS定义文件系统的接口层,为操作系统提供统一的接口操作不同的文件系统。

文件系统节点的操作流程:

  • 获取Root目录节点:VfsOps::root_dir()
  • 解析路径,逐级通过lookup方法找到对应节点:VfsNodeOps::lookup()
  • 对目标节点进行操作:VfsNodeOps::op_xxx()

Linux常用的文件系统

  • ProcFS:用于提供进程信息的接口
  • SysFS:用于向用户暴露设备信息,主要用于替代传统的devfs
  • DevFS:目前主要是为了兼容性而存在

以上三种文件系统都是ramfs的实例,在运行时构建。

2024.11.30

Rust相关

声明宏与过程宏

  • 声明宏匹配对应模式然后以另一部分代码替换当前代码
  • 过程宏更像函数,接收一段Rust代码,产生另一些代码作为输出。过程宏的参数为TokenSteam类型,返回值也是TokenSteam类型。

handle_trap!声明宏会根据#[def_trap_handler]声明的变量,查找对应#[register_trap_handler(...)]注册的处理函数。

axmm

宏内核地址空间管理相关对象的层次构成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 每个task都有一个地址空间
pub struct AddrSpace {
va_range: VirtAddrRange,
areas: MemorySet<Backend>,
pt: PageTable,
}

// 对BTreeMap的简单封装,用于管理MemoryArea
pub struct MemorySet<B: MappingBackend> {
areas: BTreeMap<B::Addr, MemoryArea<B>>,
}

// 对应一段连续的虚拟内存区域,关联一个负责实现具体映射行为的Backend
// Backend决定map、unmap、protect的行为,目前有Linear和Alloc两种,Linear对应的物理页帧必须连续
pub struct MemoryArea<B: MappingBackend> {
va_range: AddrRange<B::Addr>,
flags: B::Flags,
backend: B,
}

TODO

Hypervisor