0%

Lab1

3.28

完成 Rust 语言练习

Lab2

4.7

risc-v知识欠缺,花费大量时间浏览 risc-v 手册。

完成 Lab1

学习 第四章 Rust 中的动态内存分配,地址空间, SV39 多级页面管理机制(上)。

成果:

初步了解risc-v多级分页机制。

4.10

完成 Lab2, 选做了 Lazy 分配策略。

大部分时间花在了 Lazy 策略初始化不分配内存上,通过查看多个函数的调试输出,完善了mmap申请的虚拟页表和物理页表映射结构上的设计。

4.11

开始 Lab3:spawn, 迁移 Lab2 中的工作。

spawn 功能非常简单,记得最后要添加到 manager 中。

4.15 总结

rcore 的文档很详细,通过读文档已经能对 rcore 很清晰的了解了,之后做实验也不存在太大的困难。选做题只做了 mmap lazy 策略。

Lab3

4.17

做完第二个小测验,发现没有安装 riscv-linux-musl-gcc, ArchLinux 中这个包在 AUR 中可以找到,riscv64-gnu-toolchain-musl-bin^AUR^, 也可以在 github 上找到相应的二进制文件 riscv-gnu-toolchain/releases

4.23

完成 Lab3, 选做题花费的时间多了些,主要是卡在了最后一个点上,费了一番功夫才完成了。现在回想起来感觉是因为是在晚上3,4点脑子不清醒,今天下午就顺利很多。
题目难度本身不大,第三阶段需要对项目的结构有一定的了解,不然会不知道要找的模块在哪里。

rustling

大部分练习没什么印象,都是很基础的练习。

唯一印象深刻的是对#[cfg(feature)]的考察,cargo在编译时会根据feature来选择编译字段、函数、源代码,私认为这是个很有用的特性。在编译大型项目时,就可以塞入很多功能,让用户能够选择feature来控制项目的编译。

rcore

之前做过mit 6.1810 操作系统实验课,是基于riscv的用c编写的操作系统。这次写rcore主要是体验两者在实验流程的设计差异,以及crust在编写内核上的特性与差异。

我个人觉得rcore内容上更丰厚更复杂,但是实验设计上有点太简单了,或者说为学生实现了太多。我觉得可以丰富实验内容,让学生体会到内核的调用流程,以及rust语言实现内核的优点。

  1. syscall实验中,可以增加几个系统调用函数,并让学生完成从用户态增加函数,到内核态具体实现

  2. syscall实验中,内核接收syscall_id,通过match分发到对应的函数。我个人觉得这里应该提供两个数组引导学生去填写新增的系统调用函数:

    1. index -> syscall_id: SYSCALL_MAP = [SYS_GETTIME, SYS_READ, SYS_WRITE, .. ]
    2. index -> syscall_func: SYSCALL_FUNC = [sys_gettime, sys_read, sys_write, ..]

    这样,很自然的就能想到trace系统调用应该怎么实现。我觉得syscall_id设置成64,93,124,...应该有rcore设计上的考量,教学项目是不是可以设置从0开始的连续自然数呢?

  3. virtual memory中,实验设计书上没有仔细讲从虚拟地址的39位怎么映射到物理地址的,而代码更是直接帮学生实现好了地址转换、地址映射、地址查找。我个人觉得这里可以划分几个实验让学生实现

  4. virtual memory中,增加一个中断实验,内核处理page_fault,进而进一步考察copy on write页表缺页的实验

  5. 接上一条,增加trap的处理实验

  6. 增加考察汇编代码的简单编写,比如在代码中插入汇编代码、在.S中编写汇编代码。并让学生体会为什么这么做:手动控制寄存器。在这个过程中,自然的就了解了编写的函数本质就是汇编代码中的符号,再通过汇编链接到一起

  7. 测试可以更丰富,有些测试过于简单了

  8. 用户端的shell代码应该捕获ctrl z, ctrl d, ctrl c之类的字符或者添加exit, quit来让用户退出

在这次实验中,我深刻体会到了rusttrait抽象的强大之处,在virtual memory实验中,VirtAddrPhyAddrstruct对相关trait的实现可以很方便让用户操作地址还不会混淆。

arceos

非常的复杂,内容也非常的多。我一直认为大型项目的价值在于项目的架构以及各个api的语义设计。

通过查看调用链了解到了是user -> axstd -> api -> modules/xxx。我觉得这个项目最有意思的地方在于组件化操作系统,通过feature来选择编译Unikernel、宏内核、虚拟机。如果组件化内核编译出的各种类型内核性能与原生内核差距不大的话,感觉会是很方便的内核开发方式。
郑友捷老师讲的组件化内核让我收益很多,我准备后续学习一下cargo的功能来了解这个项目是怎么组织不同功能的组件的。
我自己一直有个疑惑,编写内核的时候要不要用alloc::collections中的数据结构,以及为了内核稳定性是不是应该只用官方库和自己编写的库而少用第三方库。

这个项目也让我逐渐意识到一个事实:编译器提供c语言库,其他高级语言通过调用c语言库(汇编)来与硬件/操作系统交互。

阶段一

在原版的rustlings基础上加入了一些针对训练营需要的unsafe相关的知识和构建过程中build.rs的应用。属于比较基础的内容,跟着评测机一道道做过去就可以了。

阶段二

这个阶段主要是通过基于rust编写操作系统内核rcore完成几个实验,借助实验理解一个具有进程/线程管理、内存管理、文件系统、进程间通信和提供了一定同步机制的内核是如何构成并运作起来的。

比较印象深刻的有这么几个知识点:

  • 链接脚本与全局符号的使用
  • Rust的汇编嵌入
  • 第四章中,在使用分离内核空间的时候。通过设计跳板页来解决切换页表后指令执行的问题。跳板页
  • 第六章了解了文件系统,了解了块设备的概念,对文件系统的各个抽象层有了一定的了解。
  • 第七、八章了解了操作系统是如何为应用提供同步原语的

跳板

由于rcore使用了分离内核空间的设计,所以在Trap的时候需要切换页表。但在切换页表之后,pc寄存器还是忠实的在其原来的位置自加到下一条指令,如果内核内存空间程序内存空间对这段代码的映射不是在同一个位置的话,则会表现出来程序跳转到了别的地方执行的效果。因此需要设计一个跳板页,在虚存中将其映射到所有内存空间的最高页,确保在切换之后,也能正确运行下一条指令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# trap.S
...
.section .text.trampoline
.globl __alltraps
.globl __restore
.align 2
__alltraps:
csrrw sp, sscratch, sp
...

# linker.ld
...
stext = .;
.text : {
*(.text.entry)
. = ALIGN(4K);
strampoline = .;
*(.text.trampoline);
. = ALIGN(4K);
*(.text .text.*)
}
...

在上面的汇编可以看到,我们给trap.S分配到了.text.trampoline段,并在链接脚本中定义了一个strampline符号来标记他的位置,这样我们可以在Rust中找到这个跳板页,映射到我们期望的位置。

但将跳板也映射到别的地方带来了新的问题,原来__alltraps中最后跳转到trap_handler使用的是call trap_handler。我们可以通过obj-dump看看编译得到的指令。

1
2
3
4
5
6
7
8
9
10
# obj-dump -Dx ...

...
80201056: 73 90 02 18 csrw satp, t0
8020105a: 73 00 00 12 sfence.vma
8020105e: 97 80 00 00 auipc ra, 0x8
80201062: e7 80 e0 0b jalr 0xbe(ra) <trap_handler> # pc+0x80be
...
000000008020911c g F .text 00000000000003b2 trap_handler
...

可以看到,这里用的是pc相对寻址,也就是基于当前指令的偏移找到trap_handler所在的位置。但是现在__alltraps已经在虚拟内存中被我们映射到最高页去了,也就是说我们实际运行代码的时候是在下面这一段内存中。

1
2
3
4
5
6
7
8
9
# gdb
>>> x /20i $pc-10
0xfffffffffffff054: ld sp,280(sp)
0xfffffffffffff056: csrw satp,t0
0xfffffffffffff05a: sfence.vma
=> 0xfffffffffffff05e: jr t1

>>> p /x $t1
$9 = 0x8020911c

很明显如果这里跳转到$pc+offset$的话,并不是跳到位于正常代码段的trap_handler。所以我们要将这里换成寄存器跳转,将trap_handler的地址放到寄存器t1中,这样才能顺利地调用到trap_handler

也就是指导书中所说的

跳转指令实际被执行时的虚拟地址和在编译器/汇编器/链接器进行后端代码生成和链接形成最终机器码时设置此指令的地址是不同的。

阶段三

这个阶段正式接触到组件化操作系统arceos

在调用路径上任意一个地方加入颜色代码就可以了,本身并不复杂,主要是了解arceos的结构。

support_hashmap

考虑实现一个hashmap比较麻烦,直接引入hashbrown,将里面HashMap包到collections里面也可以通过。

但是hashbrown默认依赖的hashfold库在no_std下所提供的RandomState是基于内存布局的,而非每次都随机
可能会带来一些安全性问题

alt_alloc

实验要求实现一个bump alloctor,是一个比较简单的分配器,在给定的接口下实现就可以了

ramfs_rename

要求在给定的文件系统中实现rename的功能。看了测例中的注释仅要求在同级下重命名,不涉及移动。

搞清楚了VfsOpsVfsNodeOps两个trait之后,在路径上把目录项的名字改掉就好了。

sys_map

要求实现系统调用mmap

利用task_extaspace提供的接口就可以完成。通过find_free_area找到空闲的区域并通过map_alloc分配,然后将给定fd的数据读进来就可以通过了。需要注意一些接口有检查传入参数是否有对齐。

simple_hv

按照提示将a0``a1寄存器设置好,并将pc寄存器偏移以跳过当前指令即可。

总结

阶段三的任务总体来说比阶段二的时候来得要更简单,感觉主要还是了解arceos的架构以及UnikernelMonolithic KernelHypervisor的不同。并体会在不同的内核需求中,arceos是如何将不同的组件组合起来以达成需求的。

一阶段

用rust其实也有快5年了所以一阶段不是什么特别难的事,因为23年已经做过一次,这次增加了一些数据结构的实现,其实不是特别难,整体数据结构实现对于之前刷过leetcode的人都会比较熟,所以轻松就过了。

二阶段

其实23年最早刷过一遍所以这个比较简单,沿用23年的一些总结,这次lab1和23年的lab1有一些不同,不过核心是不变的

  1. lab1

    其实是一个很简单的lab,系统调用的次数统计
    问答题是很好的问题,也帮助我回忆和加深了risc-v的寄存器的作用,包括trap的流程,这个很重要,直接以代码展现出来,没学rcore的时候平时听到系统调用,其实是很抽象的,并不知道系统调用是怎么从用户态切换到内核态的,而rcore非常精彩的给我解答了这个问题,并且以代码展现,不再抽象。只能说感谢开源!

  2. lab2

    mmap 和 munmap 匿名映射,对我来说其实也不难,不过反而是问答题让我再次加深了SV39的结构,页表,页表项等等这些其实理解很抽象,包括用户态是怎么用到MMU的,MMU和操作系统存储的页表这些是怎么结合的,在这一张再结合linux的一些代码就理解了。其实是riscv 使用 SATP 寄存器来保存 MMU 映射表的根地址

  3. lab3

    spawn和stride 调度算法,这个其实也不算特别复杂,在给予fork和exec代码中只需要理解 spawn和他们的区别,就很容易写出来,而stride调度算法用一个小顶堆实现即可。因为之前看了linux的task_struct的实现,所以比较轻松就能理解。

  4. lab4

    这个要求实现linkatunlinkat 这个加深了我对硬连接的理解,并且文件系统的这章让我对linux的vfs也更加理解。整体来说明显会感受到磁盘读取和内存有异曲同工之妙。

  5. lab5

    死锁检测,这个就非常考察细心了,主要就是资源的分配、分出、释放,需要格外注意,否则都无法通过,顺带这里也有一个坑,就是检测用了sleep,sleep用的是get_time,所以要实现这个api不然程序就会卡在那

三阶段

这次是新增的一个阶段,主要是为了让大家先熟悉arceos,在熟悉rcore以后其实再看arceos是比较轻松的,组件化操作系统的思想是一个很好的思想,同时也比较考验抽象能力如果做到高性能高抽象的同时又可以随意的扩展操作系统的个个组件,使用一个create引入开箱即用,个人认为是一个未来需要的方向。随着整体社会需求的发展大家对底层性能的要求越来越苛刻定制化需求也越来越多,对于操作系统也是百花齐放,同时在写一个新的操作系统的时候也总是需要重复造轮子,这个工作量其实也不算小,所以个人认为组件化操作系统在当前是一个很好的想法,高质量的组件化操作系统可以帮助个人和初创企业降低开发操作系统的难度还可以获得定制操作系统的优势来满足一些特殊的场景需要。

  1. print_color
    这个其实很简单,因为不想修改println的宏所以这里新增了一个print_color的宏来实现对颜色的打印,并再用log来包装print_color宏实现不同等级打印不同颜色的日志

    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
    #[macro_export]
    macro_rules! print_color {
    ($color:expr, $($arg:tt)*) => {{
    use axstd::io::Write;

    let mut out = $crate::io::stdout().lock();
    let _ = write!(out, "\x1B[{}m", $color);
    let _ = write!(out, $($arg)*);
    let _ = write!(out, "\x1B[0m");
    }};
    }
    pub enum LogLevel {
    Error,
    Warn,
    Info,
    Debug,
    }

    #[macro_export]
    macro_rules! log {
    (error, $($arg:tt)*) => {
    $crate::print_color!("31", concat!("[error] ", $($arg)*, "\n"));
    };
    (warn, $($arg:tt)*) => {
    $crate::print_color!("33", concat!("[warn] ", $($arg)*, "\n"));
    };
    (info, $($arg:tt)*) => {
    $crate::print_color!("32", concat!("[info] ", $($arg)*, "\n"));
    };
    (debug, $($arg:tt)*) => {
    $crate::print_color!("34", concat!("[debug] ", $($arg)*, "\n"));
    };
    }
  1. hashmap
    这个其实可以参考rust的std的rust实现,然后改一下就可以了,当然也可以从0自己实现一个最后别忘了这样才能使用std::map

    1
    2
    3
    4
    5
    6
    7
    mod map;

    #[cfg(feature = "alloc")]
    pub mod collections {
    pub use crate::map::HashMap;
    pub use alloc::collections::*;
    }
  1. bump_alloc
    这个主要需要了解什么是bump算法,在实现ByteAllocator和PageAllocator的时候需要注意b_pos和p_pos的验证,还有要注意对齐,还要理解一下align_pow2

    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
    fn alloc(&mut self, layout: Layout) -> AllocResult<NonNull<u8>> {
    let align = layout.align();
    let size = layout.size();
    let aligned = (self.b_pos + align - 1) & !(align - 1);
    let new_b_pos = aligned + size;

    if new_b_pos > self.p_pos {
    return Err(AllocError::NoMemory);
    }

    self.b_pos = new_b_pos;
    self.byte_alloc_count += 1;
    self.byte_alloc_total += size;
    Ok(NonNull::new(aligned as *mut u8).unwrap())
    }

    fn alloc(&mut self, layout: Layout) -> AllocResult<NonNull<u8>> {
    let align = layout.align();
    let size = layout.size();
    let aligned = (self.b_pos + align - 1) & !(align - 1);
    let new_b_pos = aligned + size;

    if new_b_pos > self.p_pos {
    return Err(AllocError::NoMemory);
    }

    self.b_pos = new_b_pos;
    self.byte_alloc_count += 1;
    self.byte_alloc_total += size;
    Ok(NonNull::new(aligned as *mut u8).unwrap())
    }
  1. rename
    这个需要修改一下axfs_ramfs组件大致思路是 获取当前节点(即当前目录)-> 查找要重命名的原始节点 old_node->拆解 new 路径,获得新文件名新父目录路径->获取根节点,并从中查找新父目录->从原目录中移除旧路径->将 old_node 插入新父目录,使用新文件名

  2. mmap file
    这个修改较多,在Backend新增了一个FileBacked

    1
    2
    3
    4
    5
    6
    /// File-backed mapping backend (lazy load).
    FileBacked {
    reader: ::alloc::sync::Arc<dyn crate::MmapReadFn>,
    file_offset: usize,
    area_start: VirtAddr,
    },

    在page_fault的时候进行实际内存申请

    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
    Self::FileBacked {
    reader,
    file_offset,
    area_start,
    } => {
    let va = vaddr.align_down(PAGE_SIZE_4K);
    let offset = file_offset + (va.as_usize() - area_start.as_usize());

    let vaddr = match global_allocator().alloc_pages(1, PAGE_SIZE_4K) {
    Ok(vaddr) => vaddr,
    Err(_) => return false,
    };

    let paddr = virt_to_phys(VirtAddr::from(vaddr));
    let buf = unsafe {
    core::slice::from_raw_parts_mut(axhal::mem::phys_to_virt(paddr).as_mut_ptr(), PAGE_SIZE_4K)
    };
    if !(reader)(offset, buf) {
    return false;
    }

    page_table
    .map_region(
    va,
    |_| paddr,
    PAGE_SIZE_4K,
    orig_flags,
    false,
    false,
    )
    .map(|tlb| tlb.ignore())
    .is_ok()
    }

    mmap

    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
    fn sys_mmap(
    addr: *mut usize,
    length: usize,
    prot: i32,
    flags: i32,
    fd: i32,
    offset: isize,
    ) -> isize {
    let binding = current();

    let mut aspace = binding.task_ext().aspace.lock();

    let vaddr = match aspace.find_free_area(
    VirtAddr::from(addr as usize),
    length,
    VirtAddrRange::from_start_size(aspace.base(), aspace.size()),
    ) {
    Some(base) => base,
    None => return -1,
    };

    let prot_flags = MmapProt::from_bits_truncate(prot);
    let mut map_flags = MappingFlags::USER;

    if prot_flags.contains(MmapProt::PROT_READ) {
    map_flags |= MappingFlags::READ;
    }
    if prot_flags.contains(MmapProt::PROT_WRITE) {
    map_flags |= MappingFlags::WRITE;
    }
    if prot_flags.contains(MmapProt::PROT_EXEC) {
    map_flags |= MappingFlags::EXECUTE;
    }

    let aligned_len = (length + PAGE_SIZE_4K - 1) & !(PAGE_SIZE_4K - 1);
    let hint = if addr.is_null() {
    aspace.base()
    } else {
    VirtAddr::from(addr as usize)
    };

    let file_obj = match get_file_like(fd) {
    Ok(f) => f,
    Err(_) => return -1,
    };

    let reader = alloc::sync::Arc::new(move |_offset: usize, buf: &mut [u8]| {
    file_obj.read(buf).is_ok()
    });

    if let Err(e) = aspace.mmap_file(vaddr, aligned_len, map_flags, offset as usize, reader) {
    return -1;
    }

    vaddr.as_usize() as isize
    }
  3. simple_hv
    这个其实蛮有意思的可以体验到guest操作自己没有权限的指令时候的一个流程以及体验的到page_fault的流程,第一个需要自己在VM中模拟 CSR 访问

    1
    2
    3
    ctx.guest_regs.gprs.set_reg(A1, 0x1234);
    ctx.guest_regs.sepc += 4;
    return false;

    第二个可以直接写成成对应的值

    1
    2
    3
    ctx.guest_regs.gprs.set_reg(A0, 0x6688);
    ctx.guest_regs.sepc += 4;
    return false;

这个是对Hypervisor很好的一个体验也有了一个初步的认识,包括整个项目中的实验设计是非常好的,每一个新的功能都有一个简单的实验来上手体验,而且正是因为有了arceos也免去了很多最开始操作系统要处理的事情,直接进入体验Hypervisor代码量非常少,实验在risc-v指令集下,整个Hypervisor的体验很丝滑,代码结构很好,可以立马就对VM_ENTRY和VM_EXIT这个有点抽象的概念进行了具象

总结

新增的三阶段arceos是非常棒的,整体实验设计也很不错,在有了rcore的基础以后再看arceos是不困难的,一步一步的迈向抽象度更高的操作系统,大家在arceos里已经做了非常多的事情了,使得我们可以如此简单的启动一个os,并且体验到最小化的一个Hypervisor以及宏内核,也再次加深了对操作系统的理解,以及发现软硬协同的重要性。

ArceOS Record

ArceOS 的设计可以说优雅而不失健壮性,利用rust优秀的包管理机制和crates的特性组件化地搭建OS,将复杂的OS设计解耦,各个模块功能清晰、层次鲜明,

tutorial出于教学的目的,在modules引入了dependence crates;而在主线arceos中,将解耦做到了极致,形成了清晰的Unikernel层次:dependence crates -> kernel modules -> api -> ulib -> app,下为上提供功能,上到下形成层次鲜明的抽象,这种抽象又为异构内核的实现提供支持,以宏内核为例,其既可以使用api提供的功能,又可以复用kernel modules支持更多的功能,这种自由的复用和组织可以为定制化操作系统提供极大的便利和支持,方便基于需求实现特定OS

Read more »

训练营学习记录

这篇文章用来记录我在2025春夏季开源操作系统训练营的学习过程,之所以会参加本次训练营,是因为我想进一步学习操作系统以及学习操作系统以及通过完成rcore包括通过完成组件化操作系统进行进一步磨练自己。

训练营二阶段关于rcore实验完成记录

在第二阶段的学习过程中,我收获颇丰,深入理解了 Rust 语言和操作系统的核心概念。在学习 Rust 语言时,我全面掌握了其独特的所有权、借用和生命周期规则,这些特性为 Rust 提供了强大的内存安全保障。而在操作系统方面,我不再停留在浅显的层面,而是深入探讨了内核架构,从系统启动到各个模块的交互过程有了清晰的认知。特别是在进程管理方面,我了解了进程的创建、销毁及状态转换的原理,并深入分析了不同调度算法对 CPU 资源分配的影响。

在内存管理方面,我深入研究了物理内存分配与虚拟内存映射的机制,了解了页表机制在其中扮演的关键角色,惊叹于内存管理的复杂性与精巧性。在我的个人项目中,我将 Rust 和操作系统的知识结合,参与了从设计、实现到调试的全过程,解决了许多技术难题,这一过程让我不断成长和提升。

这一阶段的学习为我打开了全新的视野,未来我将继续深入探索,将所学的知识更好地应用于实践。

训练营三阶段关于arceos实验以及挑战实验

print_with_color
通过使用 ASCII 字符,实现了简单的控制台颜色输出。

support_hashmap
为了快速实现功能,引入了一个现成的库来处理哈希映射。

alt_alloc
由于测试用例较为简单,实现难度较低。严格按照要求实现后,我对是否完全正确也并没有特别的把握。

shell
在原有的 Shell 实现中,已经有了 rename 功能。为了简化,我直接调用了现有库来处理 rename,同时利用文件创建、复制文件内容和删除原文件的方式,模拟了 mv 命令的功能。

sys_map
通过使用 find_free_area 来找到合适的内存区域并进行数据读取,尽管 find_free_area 找到的内存地址并不完全符合 man mmap 的描述,但依旧能够实现所需的功能。

page_fault
难度适中,相比于原先的实现,这部分内容更多是基于 rcore 的基础进行了延伸与补充。

simple_hv
通过修改 guest 的 sepc 寄存器值,并设置 a0、a1 的值,成功实现了一个基础的 Hypervisor 操作。

通过第三阶段的学习,我理解了组件化操作系统内核的设计理念。相较于2阶段的rcore,这种组件化内核更像是可以随意拼接的积木,可以极大程度的根据自己的需求适配或灵活的扩展内核。开始时,我学习Unikernel这种内核结构,并阅读了如axhal、axruntime、axalloc等关键部分的代码,初步掌握acreos的运行逻辑和代码架构;尝试将arceos扩展为宏内核,也让我进一步体验到组件化内核的奇妙;同时根据PPT初步了解了虚拟化的原理和技术。Arceos的模块化内核很好的结合rust模块的特性,也让我思考模块化内核和微内核是否能够结合起来呢,这或许也是一个扩展方向吧。我第四阶段打算做rust异步运行时,希望能做成一个比较完备的项目!

一、前言

在过去两周,我学习了Unikernel, Monolithic Kernel, Hypervisor三种内核架构。经过学习,我对组件化操作系统有了初步的认识和掌握。以下是我对这两周学习过程的总结。

二、学习内容

  1. Unikernel

学习了Unikernel的基础与框架,包括如何从汇编代码进入到rust代码再进入到内核,并通过axhal -> axruntime -> arceos_api -> axstd 实现控制台的打印输出。

接下来引入了动态内存分配组件,以支持Rust Collections类型。通过引入axalloc模块,实现对内存的管理,并学习了动态内存分配的相关算法。通过这部分的学习,让我理解了rCore中为什么到后面的章节就可以使用Vec等集合类型。

之后引入任务数据结构并构建了通用调度框架,实现了抢占式调度。并实现了文件系统的初始化和文件操作。

实践作业:

实现带颜色的打印输出,理清控制台的打印输出的调用链即可, 可以在不同层次的组件上修改。

手写HashMap,我使用拉链法实现哈希表,并通过引入axhal提供的随机数增强鲁棒性。

实现bump分配算法,根据代码框架,实现EarlyAllocator的初始化和分配函数。

实现rename,首先是需要追踪是如何使用axfs_ramfa的,通过调试,可以发现底层实现是在DirNode,并且源数据结构其实就是btreemap,具体操作并不复杂。

2.Monolithic Kernel

在unikernel的基础上,引入用户态、系统调用等即可完成到宏内核的跨越,这一部分的学习让我更深刻的理解了组件化的优势,扩展task属性实现宏内核的进程管理以及分离调度属性和资源属性的策略更是让我眼前一亮。

实践作业:

实现sys_mmap系统调用,先使用fd读取源文件的内容,分配所需的内存空间,再查找用户态的页表得到相应的物理地址,将源文件内容写入即可。

3.Hypervisor

引入RISC-V H扩展,使原来的S态增强为HS态,并加入了VS态和VU态,通过对特权寄存器的修改,即可跨越到Hypervisor。

主要学习了VM-EXIT,由于Guest不存在M态,所以超出当前特权态的处理能力时会经历 VU -> VS -> (H)S -> M 的过程,本部分的作业也是和 VM-EXIT相关的,通过修改 vmexit_handler 函数以完成作业的要求。

一阶段总结

二刷了,也很快写完了,感觉没有什么好说的()

二阶段总结

又复习了一遍 rcore,顺便对比了一下去年写的代码,感觉去年写的代码明显很差,经常出现一些莫名其妙的的数据结构和函数调用,
今年对很多地方进行了重新编写,感觉自己对 rcore 的理解又上了一层。

今年担任助教的过程中,也主动被动的学了一些之前没有怎么关注的事情,对操作系统的理解也上了一层

三阶段总结

说起来这个三阶段去年是不做实验要求的,今年做了要求,但是因为之前在 arceos 进行了一个月的重度开发加上去年都写过了,这次
很快就写完了。

在写实验的过程中也发现,之前在写内核的时候有很多对情况的遗漏,现在也对这些情况进行了填补。

第一阶段

第一阶段主要是看了 rust权威指南 和b站杨旭的rust编程语言入门, 以及参考rust的官方文档

第二阶段

总结

第二阶段主要是参考rCore-Tutorial-Book 第三版的文档,以及每个ch都详细通过gdb观察函数调用关系,最后整理出完整的函数调用步骤,方便更好的理解内核态和用户态的交互

ch1

通过在switch和syscall函数打断点,读取寄存器的数据,更好的理解了内核态到用户态的跳转步骤,以及上下文切换的具体步骤

ch2

通过正确的对齐操作,和虚拟地址与物理地址的转换,进行map,并进行正确的错误处理:包括overlap以及传参错误的处理。

ch3

通过source breakpoint.txt快速发现函数调用路径和调用关系。

1
2
3
4
5
6
7
8
9
10
11
break run_tasks
break src/task/processor.rs:62
break trap_handler
break __switch
break syscall::syscall
break __restore
break __switch
break trap_return
break src/task/processor.rs:99
break exit_current_and_run_next
break suspend_current_and_run_next

通过对照fork与exec对spawn进行实现。

ch4

通过使用UPSafeCell块与VEC新建了一个全局LINK_VEC,记录已经链接的变量的inode与链接计数。并修改Inode中的create函数,实现创建链接。最后实现fstat获取文件状态。

ch5

通过类银行家算法实现检测死锁。

第三阶段

1
$crate::io::__print_impl(format_args!("\x1b[36m{}\x1b[0m\n", format_args!($($arg)*)));

通过传入颜色文本格式进行println打印

support_hashmap

通过两种方法实现了hashmap

  1. 通过直接use hashbrown::HashMap as InnerMap;导入hashmap
  2. 使用use axhal::misc::random;和vec自己构建了一个简单的hashmap

alt_alloc

使用bytes_pospages_pos构建了简单的EarlyAllocator
通过正确的align_upcheck is_valid_range,进行alloc和dealloc。

ramfs_rename

通过在btreemap中获取值并insert到其children中,进行rename。

sys_map

通过#[register_trap_handler(PAGE_FAULT)]进行注册PAGE_FAULT的trap_handler。
通过find_free_area,寻找一块可以放置对齐后的uspace的区域。
实现正确的给权限操作后,进行map_alloc
再获取对应fd的file,读取值写入buf中,再写入刚map_alloc的空间。

simple_hv

进行A0和A1寄存器的正确设置,并正确调整sepc的偏移量。