课后练习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内存分配 分别为BaseAllocator
,ByteAllocator
,PageAllocator
实现特征即可
实现相关特征即可,注意在页分配的时候进行对齐,字节分配器下的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"
来实现重映射
可选的重建映射
为了管理更大范围的地址空间,包括设备的MMIO范围
分类和权限的细粒度控制
宏内核
增加了用户特权级
创建独立的用户地址空间
内核运行运行时可以随时加载应用Image投入运行
应用与内核界限分明
高端内核空间,低端用户应用空间
宏内核的地址空间管理
地址空间区域映射后端
linear:对应物理地址必须连续
alloc:按页映射,可能不连续
多级调用handle_page_fault
populate为true时,申请页帧后直接完成映射;为false时仅建立空映射
如何让Linux的原始应用直接在我们宏内核上直接运行
实现兼容页面,包含syscall
,procfs & 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设备读出数据
模拟模式:为虚拟机模拟一个pflash
透传模式:直接把qumu的pflash透传给虚拟机
地址映射的支持 两阶段的地址映射 GVA -> GPA -> HPA
虚拟机时钟中断 为了支持抢占式调度
问题:宿主环境失效问题 通过中断注入