0%

开源系统训练营第三阶段-贺琨鹏

课后练习1:print_with_color

axstd中的wirte方法中,利用终端控制序列(ANSI 转义码)进行有颜色的输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let color_prefix = b"\x1b[32m";
let color_suffix = b"\x1b[0m";


if let Err(e) = arceos_api::stdio::ax_console_write_bytes(color_prefix) {
return Err(e);
}

let result = arceos_api::stdio::ax_console_write_bytes(buf);
if let Err(e) = result {
return Err(e);
}

if let Err(e) = arceos_api::stdio::ax_console_write_bytes(color_suffix) {
return Err(e);
}

result
}

MMIO:将设备寄存器直接映射到内存空间上,并具有独立的地址
u_3_0模拟闪存磁盘,启动时自动从文件加载内容到固定的MMIO区域。(对读操作不需要驱动,直接访问)

paging特征决定了是否启动后期重建完成的空间映射

课后练习3:实现bump内存分配算法

引导问题(bootstrap problem),也被称为“先有鸡还是先有蛋”的问题。内核在刚启动时,需要分配内存来建立自己的数据结构(例如内存映射、页表等),但此时还不知道系统中有多少物理内存,也没有初始化内存分配器。然而,要调用库函数(如用于解析设备树、初始化驱动等),可能需要动态内存分配。这就导致了一个悖论:需要分配内存来初始化内存管理,但又需要内存管理来分配内存
练习:在引导阶段实现bump内存分配
分别为BaseAllocatorByteAllocatorPageAllocator实现特征即可

实现相关特征即可,注意在页分配的时候进行对齐,字节分配器下的dealloc只需对count进行操作即可

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
pub struct EarlyAllocator {
start: usize,
end: usize,
b_pos: usize,
p_pos: usize,
count: usize,
}

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


impl BaseAllocator for EarlyAllocator {
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 {
self.end += size;
Ok(())
}
}

impl ByteAllocator for EarlyAllocator {
fn alloc(&mut self, layout: Layout) -> AllocResult<NonNull<u8>> {
let size = layout.size();

if self.b_pos + size > self.p_pos {
return Err(allocator::AllocError::NoMemory);
}

let ptr = self.b_pos;
self.b_pos = self.b_pos + size;
self.count += 1;

unsafe { Ok(NonNull::new_unchecked(ptr as *mut u8)) }
}

fn dealloc(&mut self, _pos: NonNull<u8>, _layout: Layout) {
if self.count > 0 {
self.count -= 1;
}
if self.count == 0 {
self.b_pos = self.start;
}
}

fn total_bytes(&self) -> usize {
self.end - self.start
}

fn used_bytes(&self) -> usize {
self.b_pos - self.start
}

fn available_bytes(&self) -> usize {
self.p_pos - self.b_pos
}
}



impl PageAllocator for EarlyAllocator {
const PAGE_SIZE: usize = 0x1000;

fn alloc_pages(&mut self, num_pages: usize, align_pow2: usize) -> AllocResult<usize> {
let size = num_pages * Self::PAGE_SIZE;

let new_p_pos = (self.p_pos - size) & !(align_pow2 - 1);

if new_p_pos < self.b_pos {
return Err(allocator::AllocError::NoMemory);
}

self.p_pos = new_p_pos;
Ok(self.p_pos)
}

fn dealloc_pages(&mut self, _pos: usize, _num_pages: usize) {
}

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.b_pos) / Self::PAGE_SIZE
}
}

增加axsync
抢占式调度机制
触发优先级高的任务及时获得调用机会,避免个别任务长期不合理占据CPU

支持从磁盘块设备读数据,替换PFlash
管理方式:static or dyn

块设备驱动Trait:BlockDriverOps

课后练习4:为shell增加文件操作命令

底层以fatfs为框架实现好了,我们只需调用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
fn do_mv(args: &str) {
let args = args.trim();
if args.is_empty() {
print_err!("mv", "missing operand");
return;
}

let mut paths = args.split_whitespace();
let src = match paths.next() {
Some(p) => p,
None => {
print_err!("mv", "missing source file");
return;
}
};

let dest_dir = match paths.next() {
Some(p) => p,
None => {
print_err!("mv", "missing destination directory");
return;
}
};


let dest_path = format!("{}/{}", dest_dir, src);

if let Err(e) = fs::rename(src, &dest_path) {
print_err!("mv", format!("failed to move {} to {}", src, dest_dir), e);
}
}

fn do_rename(args: &str) {
let args = args.trim();
if args.is_empty() {
print_err!("rename", "missing operand");
return;
}

let mut paths = args.split_whitespace();
let old_name = match paths.next() {
Some(p) => p,
None => {
print_err!("rename", "missing old file name");
return;
}
};

let new_name = match paths.next() {
Some(p) => p,
None => {
print_err!("rename", "missing new file name");
return;
}
};
if let Err(e) = fs::rename(old_name, new_name) {
print_err!("rename", format!("failed to rename {} to {}", old_name, new_name), e);
}
}

ArceOS Unikernel包括两阶段的地址映射
Boot阶段默认开启1G空间的恒等映射
如果需要支持设备的MMIO区间,通过指定一个feature- "paging"来实现重映射

可选的重建映射

  1. 为了管理更大范围的地址空间,包括设备的MMIO范围
  2. 分类和权限的细粒度控制

宏内核

  • 增加了用户特权级
  • 创建独立的用户地址空间
  • 内核运行运行时可以随时加载应用Image投入运行
  • 应用与内核界限分明

高端内核空间,低端用户应用空间


宏内核的地址空间管理

地址空间区域映射后端

  1. linear:对应物理地址必须连续
  2. alloc:按页映射,可能不连续

多级调用handle_page_fault

populate为true时,申请页帧后直接完成映射;为false时仅建立空映射

如何让Linux的原始应用直接在我们宏内核上直接运行

实现兼容页面,包含syscallprocfs & sysfs等伪文件系统,awareness of aspace

ELF格式应用的加载

需要注意文件内偏移和预定的虚拟地址空间内的偏移可能不一致,特别是数据段的部分

存零,清零

应用的用户栈的初始化
支持系统调用的层次结构

系统调用号与体系结构相关

对Linux常用文件系统的支持

课后练习:实现mmap系统调用

Hypervisor

建立最简化的Hypervisor 过程 新建地址空间,加载内核镜像 准备上下文 设置两级页表 切换V模式 OS内核只有一条指令:调用sbi-call的关机操作。但由于这种操作超出了V模式的权限,导致VM_EXIT退出虚拟机,切换回Hypervisor Hypervisor响应VM_EXIT的函数(vmexit_handler`)检查退出原因和参数,进行处理,由于时请求关机,清理虚拟机后退出

进入虚拟化模式准备的条件
ISA寄存器misa第7位代表Hypervisor扩展的启用/禁止。
进入V模式路径的控制:hstatus第7位SPV记录上一次进入HS特权级前的模式,1代表来自虚拟化模式。
执行sret时,根据SPV决定是返回用户态,还是返回虚拟化模式。
Hypervisor首次启动Guest的内核之前,伪造上下文作准备:
设置Guest的sstatus,让其初始特权级为Supervisor;
设置Guest的sepc为OS启动入口地址VM_ENTRY,VM_ENTRY值就是内核启动的入口地址,
对于Riscv64,其地址是0x8020_0000。

如何从pflash设备读出数据

  1. 模拟模式:为虚拟机模拟一个pflash
  2. 透传模式:直接把qumu的pflash透传给虚拟机

地址映射的支持
两阶段的地址映射
GVA -> GPA -> HPA

虚拟机时钟中断
为了支持抢占式调度

问题:宿主环境失效问题
通过中断注入