0%

一阶段

用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的偏移量。

一阶段

这个内容不难,看文档+坚持就行了

二阶段

这个内容上了些难度,主要是对内容的结构理解, 代码上问题不大

三阶段 6道题

解题思路

  1. 修改sbi::putchar的内容, 添加颜色信息.
1
2
3
4
5
6
7
8
9
10
11
12
13
/// Writes a byte to the console.
pub fn putchar(c: u8) {
// #[allow(deprecated)]
// for b in b"\x1b[31m" {
// sbi_rt::legacy::console_putchar(*b as usize);
// }
#[allow(deprecated)]
sbi_rt::legacy::console_putchar(c as usize);
// #[allow(deprecated)]
// for b in b"\x1b[31m" {
// sbi_rt::legacy::console_putchar(*b as usize);
// }
}
  1. 修改marcos的内容, 添加颜色信息.
1
2
3
4
5
6
7
8
/// Prints to the standard output, with a newline.
#[macro_export]
macro_rules! println {
() => { $crate::print!("\n") };
($($arg:tt)*) => {
$crate::io::__print_impl(format_args!("\x1b[31m{}\x1b[0m\n", format_args!($($arg)*)));
}
}
  1. 修改main.rs的内容,直接在输出结果上添加颜色信息 .
1
2
3
fn main() {
println!("\x1b[31m[WithColor]: Hello, Arceos!\x1b[0m");
}

hashmap

解题思路

  1. 修改std的内容,直接引入rust std hashbrown作为hashmap算法
  1. 给axstdhashmap.rs 手动实现hahsmap的内容
1
2
pub mod hashmap; 
pub use hashmap as collections;

难点 :

1. 涉及一些hashmap的原理思想. 图简单,我直接设置了比较大的bucket , 冲突相关的内容也没有多管

2. 需要掌握iter的内容,因为main.rs中有关于iter方法的调用

bump_alloc

题目思路

非常简单, 因为只有lib.rs需要修改

题目难点

  1. Byte 分配需要关注对齐 . 对齐公式 通过gpt分享得知 .具体的推导我仅仅手动算了一遍

  2. Page 分配需要理解num_pages 和 align_pow2的概念 .

    首先一个是分配的页数,这个不太困难; align_pow2,这个通过查询资料可知 是页面的对齐. 它跟Byte
    的对齐有什么区别呢? 毕竟有page_size这种天然的对齐标准. gpt提供了关于dma组建的对齐示例, 也
    就是os 可能需要对齐更大的页

  3. 对AllocResult 的理解 ,需要针对不同的情况返回不同的Err值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/// The error type used for allocation.
#[derive(Debug)]
pub enum AllocError {
/// Invalid `size` or `align_pow2`. (e.g. unaligned)
InvalidParam,
/// Memory added by `add_memory` overlapped with existed memory.
MemoryOverlap,
/// No enough memory to allocate.
NoMemory,
/// Deallocate an unallocated memory region.
NotAllocated,
}

/// A [`Result`] type with [`AllocError`] as the error type.
pub type AllocResult<T = ()> = Result<T, AllocError>;

rename

题目难点

关于这道题目,我仅仅描述一下我自己的思考

  1. 首先要明白如何对文件系统(fs)进行抽象与支持。

    • 文件系统通常包含 Dir(目录)、File(文件)节点,以及一个统一对外的文件系统抽象框架。
  2. 了解了不同节点(node)之间的关系以及它们各自的职责之后,我们需要进一步确认“名称(name)”的管理部分。

    • 通过查看代码可以发现,名称相关的信息主要存储在 Dir 中,通常使用如 BTreeMap 或 HashMap 等结构来维护。
  3. 确定 rename 操作的实现思路:

    1. 查找并从原目录中移除 old 节点;

    2. 查看目标路径 new 的节点是否已存在;

      • 如果存在,先将其删除;
    3. 将 old 节点插入到新路径对应的目录中。

关于项目结构的理解

ArceOS 采用组件化的设计理念,通过 define_api_type!axfs_vfs::impl_vfs_dir_default! 封装了具体实现与默认行为。

可能需要改进的地方

当前的 rename 实现仅支持同一文件系统(或相同根节点)内的路径;当 old 和 new 不在同一根目录下时,操作会失败。

sys_mmap

题目难点

关于这道题目,我仅仅描述一下我自己的思考:

  1. 首先要明确 mmap 的核心目标是将文件的某一段内容直接映射到用户虚拟地址空间,实现对文件的“内存视图”。

    • 用户可以像访问内存一样访问文件内容,不需要中间 read/write 的过程;
    • 需要完成虚拟地址到物理页框的映射,并填充内容。
  2. mmap 涉及的几个关键抽象:

    • 用户地址空间(AddrSpace):表示一个进程所拥有的虚拟内存范围,负责查找空闲地址、执行实际映射等;
    • 页表管理(PageTable):负责建立虚拟地址到物理地址的实际映射;
    • 文件抽象(FileLike):通过 file descriptor 查找到内核抽象的文件,支持 read 方法填充页框。
  3. mmap 系统调用参数设计灵活但容易传错:

    • addr 是用户建议的映射地址,不一定被采用;
    • length 需页对齐;
    • prot 表示权限,如 PROT_READ
    • flags 控制共享/私有等策略;
    • fd 是文件描述符;
    • _offset 是映射文件的偏移,暂未处理。
  4. 着重理解mmap 的参数及设计思想

  • addr:建议映射地址。用户可以建议映射起始地址,但系统可能忽略这个值,选择更合理的地址(比如避开冲突、留出 guard page 等)。传 NULL 表示让内核自动选择。

  • length:映射的内存大小。必须是页大小的整数倍,否则需要向上对齐。映射区域大小必须足够容纳所需的文件内容。

  • prot:映射内存页的访问权限,如:

    • PROT_READ: 映射的页可读;

    • PROT_WRITE: 可写;

    • PROT_EXEC: 可执行;

    • PROT_NONE: 禁止访问。

  • flags:映射策略,如:

    • MAP_SHARED: 多个进程共享映射(写操作对原始文件可见);

    • MAP_PRIVATE: 拷贝写(写时拷贝,不影响原始文件);

    • MAP_FIXED : 强制映射到 addr 所指定的地址 ;

    • MAP_PRIVATE:私有映射(写时复制,写入不会影响原文件);

    • MAP_SHARED:共享映射(多个进程映射同一页,写入会同步影响原文件);

    • MAP_ANONYMOUS:匿名映射,不与任何文件关联,fd 被忽略,内容初始为零;

    • MAP_FIXED:强制映射到 addr 所指定的地址,可能覆盖已有映射,风险较高;

    • MAP_POPULATE:提前分配并加载所有页(非懒加载),避免首次访问缺页异常;

    • MAP_STACK:指定这是一个栈区域(部分系统支持,可能用于自动扩展);

  • fd:文件描述符。指向将被映射的文件。若使用 MAP_ANONYMOUS,则忽略该参数。

  • offset:映射起始位置在文件中的偏移。要求是页大小的倍数,表示从文件中的哪个位置开始映射。


关于项目结构的理解

ArceOS 使用组件化与模块边界清晰的方式封装 mmap 逻辑,其核心设计涉及:

  1. sys_mmap:系统调用入口,接收用户传参,执行空间分配、权限设定、数据写入等完整过程。

  2. AddrSpace 结构体(位于 axmm 模块):

    • 代表进程虚拟地址空间;
    • 提供 find_free_area 方法搜索空闲虚拟内存区域;
    • 提供 map_alloc 映射物理内存页;
    • 提供 write 将文件内容写入内存页。
  3. get_file_like:将用户传入的 fd 映射为 FileLike 类型,用于后续 read

  4. 参数处理:

    • 映射的起始地址使用 addr + 0x20_0000 作为搜索 hint 避免与低地址冲突;
    • 映射长度自动对齐为页大小;
    • 文件内容读取后通过 uspace.write() 写入虚拟地址空间。

可能需要改进的地方

目前仅仅针对测例进行编程, 一些mmap 应当实现的功能还未进行

simple_hv

hv 两种类型

  1. 直接运行在硬件上,如 KVM、Xen

  2. 运行在操作系统中,如 VirtualBox

题目解题思路

解决Trap的不同处理方式.

直接通过跳过指令, sepc + 4 的方式实现

三阶段挑战题

难点剖析

首先分析一下Rust的alloc 过程.

alloc是通过传入的参数layout进行处理的 , layout是通过申请大小进行生成的. 这里就涉及到了layout的两个重点参数: size , align . size大小就是核心的数值, 类似malloc的空间范围. align可以用来作为对齐的要求,也可以用来反推alloc数据的类型.

默认先进行alloc, 如果在alloc的过程中返回了NoMemory,会调用pageAllocator进行page上的alloc添加新的内存空间; 如果page的空间也被分配完了进程就会keilled.

观察main.rs

每次会分配15个Vec ,然后释放8个, 也就是说偶数项的Vec其实只是临时存在,尽可能分开作为临时alloc空间.

为图简洁,采用tlsfAllocator进行长期存活Vec的分配行为

解题思路

按对齐分类分配:

align == 8 → 使用 TlsfByteAllocator,适合长期存活数据。

其他对齐 → 自定义分配器,用 bump 分配方式管理。

自定义分配策略:

每 15 次分配中偶数位 → 从 end 向前分配(短期内存)。

其余 → 从 start 向后分配(长期内存)。

释放策略:

align == 8 → 交由 TLSF 回收。

其他只支持最近短期分配的内存释放。

目的:

模拟临时与持久内存的分离分配,提高模拟 allocator 的灵活性。

组件化操作系统设计与实现

oscamp 第三节阶段总结. 由于部分操作系统原理性质的内容在二阶段中已经学过了, 因此总结主要针对两部分:

  1. 新概念: ArceOS 在传统内核上的创新
  2. 老概念: 与 rCore 的实现不同的部分, 以及分析原因

为什么要组件化?

ArceOS 的优势区间在于快速针对特定领域构建出一个最合适的内核, 主要解决的痛点就是”从头开发一个操作系统太繁琐”, 而”现成的方案并不完全适用于应用场景”. 一些操作系统的可扩展性通过内核编译选项或者配置文件来实现, 但是这种方法无法在更深层次修改组装一个操作系统内核, 因此 ArceOS 采用了组件化的方案灵活组装某些功能.

ArceOS 的主要结构 (Unikernel)

  1. ArceOS 提供 axhal (Hardware Abstraction Layer) 封装了硬件相关的接口和 driver, 整理统一为 axhal 组件对外的接口
  2. 基于 axhal, 我们有 axruntime 作为运行时.
  3. 应用与内核处于同一特权级, 编译为一个 Image 运行, 在实际应用中, 非传统 PC 上的操作系统往往不需要支持多种多样的任务(包括这个实验到最后也没有并行的实现, 只有任务并发调度)
    • 应用于内核的交互在当前 Unikernel 阶段下是通过 axstd (对标 Rust 标准库), 而 axstd 会使用 arceos_api 提供的操作系统 API
    • 在未来宏内核拓展时则是通过实际的系统调用, 经过 Trap 在中断向量表中异常处理程序调用 arceos_posix_api 提供的操作系统 API, POSIX 标准的 API 是为了能够让 glibc / musl-libc 的 Linux 应用程序能够被兼容

ArceOS 的引导过程(Unikernel)

  1. 链接脚本指定的起始地址 _start, 这个过程里:
    1. 先建立栈, 以便可以调用函数
     2. 准备页表, 启用 MMU
     3. 切换地址空间后偏移一下栈指针
     4. 调用 `axhal` 中的 `rust_entry`
  2. axhal 组建的 rust_entry, 这个过程:
    • 清理 bss
    • 设置中断向量表 stvec 寄存器
    • 调用 axruntimerust_main
  3. axruntimerust_main, 这个过程:
    • 相当于内核正式启动, 打印 logo 和必要信息等
    • 重映射, 以及一些必要的初始化(如设备和中断初始化)
    • 调用应用程序中的 main() 函数

ArceOS 中一些特定功能的实现

组件组装编译(通过 Rust 的 feature 进行条件编译)

分页机制

还是熟悉的 SV39. 但是注意 Hypervisor 下拓展了两位, 不过我们暂时并没有管这两位.

内核启动早期恒等映射 SBI 和 Kernel, 然后再把这一段映射到高地址(0xffff_ffc0_0000_0000 以后), 目的是为未来宏内核拓展留下足够空间, 让 Unikernel 在高地址运行. 同时 pc, sp 也同样要增加偏移.

物理页帧分配与动态内存分配

ArceOS 中的虚拟页面是通过 MemoryArea::new(start, size, flags, Backend::new_alloc(populate)) 来将 [start, start + size) 映射到 Backend 新创建的物理页面上的, 而 Backend 则是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// axmm/src/backend/alloc.rs
fn alloc_frame(zeroed: bool) -> Option<PhysAddr> {
let vaddr = VirtAddr::from(global_allocator().alloc_pages(1, PAGE_SIZE_4K).ok()?);
if zeroed {
unsafe { core::ptr::write_bytes(vaddr.as_mut_ptr(), 0, PAGE_SIZE_4K) };
}
let paddr = virt_to_phys(vaddr);
Some(paddr)
}

fn dealloc_frame(frame: PhysAddr) {
let vaddr = phys_to_virt(frame);
global_allocator().dealloc_pages(vaddr.as_usize(), 1);
}

通过动态内存分配器分配一段地址(返回的是虚拟地址), 再将虚拟地址转换为物理地址返回, 这样就新 allocate 了一个物理页面.

ArceOS 的动态内存分配器提供两个功能: 按字节分配和按页分配. 后者是通常的动态内存调用, 前者是为了操作系统本身服务的, 相当于 rCore 的 FrameAllocator, 对物理地址进行管理, 分配页面. 字节分配器不够了也会向页分配器发送请求要求追加内存.

ArceOS 的动态内存分配器框架向下封装算法, 向上暴露接口. 常见的(字节)动态内存分配算法有: TLSF, Buddy, Slab 等. 设计动态内存分配算法是挑战题目之一. 但我还一点没做 (页分配是通过 Bitmap 管理的)

任务与任务调度

数据结构方面和 rCore 差不多, 不过似乎没有内部可变性模式了, 不知道是出于什么考虑.

ArceOS 的调度框架由 “当前可被调度的任务队列”run_queue 和 “当前阻塞中不可被调度的任务队列” wait_queue 和一个 “现在在运行的任务” task 组成 (系统还有 idle 待机任务 和 gc 回收清理任务); 向下需要 TaskContext 切换和特定调度算法的支持, 向上暴露 yield sleep spawn 等 API. 大部分 API 都非常直观, 操作相应含义的队列和任务控制块即可. 因此我们主要介绍向下需求的算法: 上下文切换和调度算法.

  1. 上下文切换与 rCore 几乎一致, 保存 ra, sp 和 RISCV 调用约定规定的通用寄存器, 保存并切换到下一个任务的上下文即可

  2. 协作式 FIFO 调度算法: 字面意思, 按照队列的先进先出模式按照特定顺序调度

  3. 抢占式 Round Robin 调度算法:

    被抢占 = 内部条件 && 外部条件

    外部条件是当前任务的抢占开关, 在”禁用抢占”->”启用抢占”的临界边缘触发. 这里我们在时钟中断时执行的 on_time_tick 里更新当前任务的外部抢占开关.

    内部条件是内部设置的抢占 guard, 以及自旋锁等, 防止在任务不希望的时候被打断.

    新任务仍会在队尾加入, 调度顺序不变, 只是多了一个抢占机制.

  4. 抢占式 CFS 调度算法(Completely Fair Scheduled, “绝对公平”的调度算法)

    $\text{vruntime} = \text{init_vruntime} + \frac{\delta}{weight(\text{prior})}$

    • $\text{vruntime}$ 最小的任务优先执行
    • 新任务的 $ \text{init_vruntime}$ 初始化为 $min(\text{vruntime}_{tid})$, 以便让新任务尽快被调度
    • 每次触发时钟中断对当前任务的 $\delta$ 按照上述式子递增, $\text{prior}$ 是优先级, 优先级越高递增越缓慢也就越容易处在前列并被调度; 然后把 $\text{vruntime}$ 最小的任务放在队首

存储设备(以及其他设备)的管理框架 (与 rCore 不同)

存储底层设备是 QEMU PFlash 模拟的闪存磁盘, 通过 MMIO 方式将文件映射到特定内存地址(在 SBI 起始的 0x80000000 之前, 在 qemu 的设备树之后)

也可以从块设备读取其数据 (drv_blockdrv_virtio 模块)

代码结构上, AllDevices 管理 block net display 等所有的设备, 设备用 struct AxDeviceContainer<D>(Vec<D>) 统一管理, 可以管理不同类型的 device.

驱动是在 axruntime::rust_main 调用的 init_drivers 中初始化的, 先基于总线 probe 设备, 然后通过宏生成代码放入 AllDevices

文件系统

文件设备基于块设备, 块设备的行为描述为 trait BlockDriverOps.

块设备以上的层次结构如下:

ArceOS 的文件设备相对 rCore 来说更加贴近现实, 如目录和挂载的概念: 目录项是一个 DirNode, 可以把一个 fs 文件系统挂载到这个目录上, 挂载 (mount) 的意义实际上就是把磁盘上扁平的数据关系建立为树形结构

宏内核支持

目前为止我们的 Unikernel 架构 ArceOS 与宏内核 (Monolithic) 架构八竿子打不着还有很大差别:

  • 没有特权级切换
  • 没有应用自己的地址空间
  • 没有实现系统调用
  • 没有实现加载应用

那么逻辑就很清楚了, 我们要按照上面的点增量构建一个宏内核(这也是组件化的有点, 高复用性):

  1. 先创建用户地址空间: 一切操作都需要对数据进行, 而数据需要在地址空间上
  2. 加载应用数据并建立用户栈到地址空间
  3. 伪造现场(临时上下文)并放入新任务的内核栈上(此时仍然在内核态)
  4. 调用 sret 从 S 特权级返回 U 特权级, 进入用户态, 从刚刚加载的应用入口开始执行
  5. 用户应用调用了系统调用, 通过异常中断向量表 Trap 进 S 特权级, 内核处理这个调用

用户地址空间的内存分布

  • 地址空间的高地址区域是内核空间(内核栈等可以共享)
  • 在内核根页表中只有高区域, 低地址区域是空
  • 以这个内核根页表为模板为应用进程复制了低地址区域的应用空间后, 所有页表中高地址那部分的内核空间的虚拟地址映射到相同的物理地址, 实现了共享.

加载用户数据

读到内存缓存区, 再到内核地址空间, 再在用户地址空间中与内核地址空间存应用的那部分映射到相同的部分

任务控制块扩展

宏内核需要一些 Unikernel 不具备的任务信息, 如用户地址空间, 上下文等. 这里用 def_task_ext! 宏注册扩展任务控制块的结构体类型.

这样, 记下 sepc 寄存器值, 标记 sstatus CSR 为”切换前来自用户态”(因为 RISCV 不存在专门从内核态到用户态的切换的指令, 只能假装当前在内核态是因为刚刚从用户态过来的, 然后返回回去)

系统调用

ArceOS Monolithic 中系统调用的实现路径是 Trap 进内核态后, 由 axhal::arch::trap 通过 link_me 从应用程序的 syscall 处理函数中调用到 arcesos_posix_api 来实现功能的

link_me: 对系统调用的响应函数通过 #[register_trap_handler(SYSCALL)] 注册.

Linux App 支持

我们已经有了一个最小化的宏内核, 但是还不能直接跑 Linux ELF 可执行文件:

  • 地址空间太 plain 了, 没有逻辑分段 (.text .data 等都要模仿 Linux)

    逻辑段实现上是一个 BTreeMap<address, area>, 通过 mmap 映射一段地址到一段页面上(以及设置访问权限等). 映射后端包含 LinearAlloc 两种方式, 前者是带偏移的连续地址映射到一个段, 后者是可能不连续的多个物理页帧的集合映射到一个段.

  • Linux App 大多数需要与 glibcmusl-libc 进行参数协同

    加载 ELF 文件, 然后只需要读取 r-xrw- 的部分 (.text.data), 即 Type 为 Load 的两个段.

    内存中和文件中代码段的长度和地址一般是恒等映射的, 毕竟就在第一个

    但是由于 BSS 段的存在, 需要重新计算并映射 .bss.data 段作为数据段

    初始化用户栈, 把命令行参数加入进去(参数和环境变量), 这个二阶段管道那一章 ch7 有说过

    musl-libc 启动较为简洁, 需要实现的系统调用较少.

  • 其他缺少于 Linux 的部分: procfs, sysfs.

Hypervisor

Hypervisor 是 Guest 与 Host 相同指令集情况下高效(虚拟化消耗可忽略)地对硬件资源进行管理的虚拟机

虚拟化模式的寄存器相关变化

  • misa[7] = 1 表示启用 Hypervisor 虚拟化
  • s* 寄存器作用不变(sstatus stvec sepc), Host 新增 hs* 寄存器用于对 Guest 的路径控制(异常/中断委托等)
  • vs* 寄存器操作 Guest 中的 S 特权级寄存器, 如 vsepc 要设置为 0x80200000, vsstatus 要设置为初始 S (VS) 特权级
  • hstatus[7] 记录上一次进入 HS 特权级前的模式 sret 根据这个决定是返回虚拟化用户态还是 Host

控制流

虚拟化实际就是在 Guest 和 Host 直接来回转换, 之所以要切回 Host 是因为有些操作必须让 Host 来执行(比如 sbi_call 或 中断注入)

每个 vCPU (在这里为了简单, 仅对应一个 CPU 核心) 维护一组上下文状态, Guest 或者 Host.

切换到另一个状态时保存当前上下文并切换到对应的上下文, Guest 到 Host 是 VM_Exit, 反之是 sret

分页处理: 嵌套分页: GVA -> (vsatp) -> GPA -> (hgatp) -> HPA

虚拟设备: 透传模式或模拟模式

中断处理(中断注入): 只有 hvip 的对应位被设置为 1, Guest 的 vstvec 才会被触发, 否则 Guest 完全不知道有中断发生. vstvec 是 Guest 的中断入口, 但它仅响应 hvip 中已注入的虚拟中断, 而不是物理中断.

操作系统设计与实现中常用的 Rust 特性

oscamp 第一阶段的 rustlings 总结, 但因为去年写过一次 rustlings 了, 题目比较基础(除了最后的算法和数据结构实现有点麻烦)之前也接触过不少 Rust, 所以这次是总结一下二三阶段中特别需要的 Rust 特性

结构体和枚举

注意元组结构体

1
2
3
4
5
struct TupleStruct(u32, u32, u32);
let tuple_obj = TupleStruct {0, 255, 0};
assert!(tuple_obj.0, 0);

struct UnitStruct;

单元结构体主要是在不关心其关联的数据, 而只需要实现其关联函数或方法时存在
其实还比较常用的, 例如对某学基本数据类型的特化, 比如 rustlings 后面的 MinHeap, 三阶段中也有这样的情况

枚举这里需要比较熟悉 match 等模式匹配的手段:

1
2
3
4
5
6
7
match message {
Message::Move(p) => self.move_position(p),
Message::Echo(s) => self.echo(s),
Message::ChangeColor(x, y, z) => self.change_color((x, y, z)),
Message::Quit => self.quit(),
_ => ()
};

本实验中大量使用枚举来表示一些错误情况(这也是 Rust 和一些程序设计语言的常见处理方式)

二三阶段处理 Trap 时的 Trap::Exception(...) Trap::Interrupt 也都是枚举, 通过 match 模式匹配

模块

模块这里 rustlings 的考察比较基础, 这里补充一些 Cargo 管理工程的机制

  • module 用于代码结构分层, 控制作用域和可见性等

    Rustlings 只介绍了内联模块 即

    1
    2
    3
    4
    5
    mod delicious_snacks {
    pub use self::fruits::PEAR as fruit;
    pub use self::veggies::CUCUMBER as veggie;
    ...
    }

    但实际工程中还有很多创建模块的方法, 例如:

    • 拆分到一个文件中, 然后用 mod name; 声明告诉编译器这里有个模块. 声明后才会进入模块树.

    • 复杂模块应拆分到一个目录中, 并在这个目录下 mod.rs 中暴露接口和声明子模块, 这个目录下的其他文件就是这个目录的子模块, 例如 foo/mod.rs 中写下 pub mod bar;, foo/bar.rs 就成为了 foo::bar 子模块

    • mod foo; 会查找 foo/mod.rsfoo.rs

      例如三阶段 ArceOS 实验二, 需要在 axstd 下面创建 collections 子模块(创建collections/mod.rs), 然后再创建 collections::hash_map 子子模块(?)(创建 hash_map.rs 并在 collections/mod.rs 声明pub mod hash_map, pub use hash_map::HashMap)

  • crate 是可被编译的最小单位, 分为 binlib 两种, 前者可被编译为可执行程序, 后者可被编译为.rlib, 只能提供一些接口(没有 main 函数)

    三阶段需要对 crate 有一定的理解, 因为 ArceOS 的组件化是相当依赖于 Rust 的 crate 的.

  • package 是提供一系列功能的 crates 的集合, 可包含多个 bin crate 和最多一个 lib crate.

  • workspace 管理多个 packages 的编译环境工具链等的目录结构. 一般比较大型的项目会分为多个 packages 开发, 共享同一个 target/目录

    以 ArceOS 下的 Cargo.toml 为例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    [workspace]
    resolver = "2"
    members = [
    "modules/axalloc",
    ...
    ]
    [workspace.package]
    ...

    [workspace.dependencies]
    axstd = { path = "ulib/axstd" }
    axlibc = { path = "ulib/axlibc" }
    ...

Option<T>Result<T, E>

在我们本次操作系统的实验中会大量使用(其实 Rust 工程都会大量使用)

ArceOS 会封装为 AxResult

泛型与 Trait

泛型作为一种静态分发生成代码的方式, 本次实验中没有太需要这一块, 有多态基本都是在 dyn Trait 用 trait 对象动态分发

trait 在本次实验中就非常重要了, 组合形式的代码复用比继承灵活了不少, 例如:

  • rCore 中的”文件” File trait, 通过 write read 抽象地描述了一个”文件”应该具备的行为
  • ArceOS 中的 VfsOpsVfsNodeOps, 描述了文件系统应该提供的功能和一个文件节点应该提供的功能, 并且通过 Arc<dyn VfsOps> 实现对不同文件系统和不同文件节点的支持
  • 动态内存分配的接口 unsafe trait GlobalAlloc (这个其实是标准库的, 但是我们自己实现的内存分配器需要 impl GlobalAlloc for OurAllocator)

生命周期

想要对 lifetime 的理解炉火纯青是比较困难的, 幸运的是本次实验你只需要懂得一些基本的生命周期概念, 然后学习智能指针就行了(), 代码中大量使用 Arc<T> RefCell<T> 等.

但还要记住一句话: 任何时候保证引用有效(对这句话的理解应该还有一层: 任何引用本身存在的生命周期必须小于等于其引用内容的生命周期, 不然其内容死了之后这个引用就没有任何意义而且变得危险了)

迭代器

使用 obj.iter()obj.into_iter()/obj.iter_mut() 后可以链式地进行一些操作:

  • 迭代适配器: map filter for_each, 接收一个捕获迭代器元素的闭包, 执行某些操作, 但不会消耗迭代器返回值, 而是继续返回迭代器
  • 消费适配器: .collect() .sum(), 消费其中的迭代器并返回具体类型, 注意你可能需要显示标注我们需要 collect 成什么类型

智能指针

  • Box<T> 拥有其内部数据的所有权, 数据在堆上, 常用于解决递归类型无法计算大小的问题 (Box 本身是 Sized 的)

  • Rc<T>/Arc<T>: 多个智能指针共享同一个数据的所有权

    • let origin = Arc::new(data) 创建一个智能指针
    • let others = Arc::clone(&origin) 共享 origin 智能指针中的数据 data
    • Arc:strong_count(&original) 有多少指针在共享这一数据, 引用计数为 0 是释放数据空间
  • Weak<T>: 弱引用, 用于解决引用循环. 例如对于操作系统中父进程有一个 children: Vec<Arc<TaskControlBlock>>, 而子进程还需要得知自己父进程 parent: Arc<TaskControlBlock>, 这样就会导致引用计数始终不为 0, 最后数据无法被释放造成泄露, 所以需要 parent: Weak<TaskControlBlock>. Weak<T> 不需要引用计数为 0 就可以被清理, 其特性是:

    • 任何涉及弱引用的循环会在其相关的值的强引用计数为 0 时被打断
    • Rc<T> 维护一个 weak_count, 每次Rc::downgrade 创建 Weak 指针时增加弱引用计数
    • weak_count 无需计数为 0 就能使 Rc<T> 实例被清理, 因此使用 Weak 的数据时其数据是可能被清理掉的, 我们需要Weak:upgrade, 返回 Option<Rc<T>.
  • RefCell: 内部可变性设计模式

    我们可以在 RefCell<T> 本身不被绑定为可变时修改其内部的值, 在运行时进行借用检查.

    RefCell 提供的内部可变性在二阶段 rCore 的一些控制信息结构体中常用, 例如 TCB 将不可变数据作为直接的结构体成员, 可变数据放在 inner 里 (这里 UPSafeCell 就是基于 RefCell 封装的).

    我们显然不希望 process, kstack 这样逻辑上应为 immutable 的数据被改变, 所以整个 TCB 结构体应该被绑定在不可变变量上, 但是这样就导致 inner 也是不可变的了, 我们无法修改 inner 中的 trap_cx 上下文等, 所以我们需要依赖 RefCell 的运行时借用检查能力, 允许修改 inner 中的值

    根据 RefCell 的特性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    /// Task control block structure
    pub struct TaskControlBlock {
    /// immutable
    pub process: Weak<ProcessControlBlock>,
    /// Kernel stack corresponding to PID
    pub kstack: KernelStack,
    /// mutable
    inner: UPSafeCell<TaskControlBlockInner>,
    }

    pub struct TaskControlBlockInner {
    pub res: Option<TaskUserRes>,
    /// The physical page number of the frame where the trap context is placed
    pub trap_cx_ppn: PhysPageNum,
    /// Save task context
    pub task_cx: TaskContext,

    /// Maintain the execution status of the current process
    pub task_status: TaskStatus,
    /// It is set when active exit or execution error occurs
    pub exit_code: Option<i32>,
    }

    这个 UPSafeCellexclusive_access 方法不能嵌套调用, 会导致 BorrowMutError.

线程

这个实验不需要掌握太多 Rust 线程的使用方法, 因为我们需要自己实现线程机制

但是要对线程模型和同步互斥有一定的认识, 会在 rCore Tutorial Book 的 ch8 中学习到

在 ArceOS 中用得非常神仙, 尤其是对过程宏的运用:

  • 借助 linke_me 利用过程宏 #[register_trap_handler(SYSCALL)] 等注册异常处理函数
  • 由于 axhalaxlog 存在的循环依赖关系, axlog 必须以 extern ABI 而不是[dependencies] 的形式依赖 axhal. 但是这个不通过 Rust 编译器的检查, 所以用过程宏封装成 trait 的形式, 减少出错概率
  • 用宏对某些特定对象生成 trait 的默认行为这些相比以上用法已经算是比较一般的了

过程宏是基于 AST 进行代码生成的, 所以灵活度非常高, 这里也不需要掌握太深入, 但是至少要能读懂过程宏, 这样在三阶段中才不会太盲人摸象

类型转换

主要在地址空间的支持中需要对 PA VA PPN VPN 进行转换等涉及到 From Into 之类的 trait

剩下的时候都在能.into().into()不能的话就强行as

: 这是不负责任的喵

最后十道题

  • 链表

    • 活用 loop+match+ 模式匹配 能写得比 for 循环迭代更清晰
    • 裸指针解引用需要 unsafe 套一下, unsafe 最好在表达式层次
  • 排序: 比较简单, 和其他语言基本没什么差别. 可以用 array.swap(idx1, idx2) 代替 std::mem:swap 交换元素

  • 二叉搜索树: 主要是 Option<Box<TreeNode<T>>> 套着比较烦, 但其实都是必要的: Option 用来时刻提醒你防止 NULL 的情况, Box 用于解决树形结构的递归类型

  • 图的存储, BFS 和 BFS: vec![vec![];n] 创建一个 Vec<Vec<T>> 用来存边

    • VecQueue 作为 Rust 中的双端队列
    • while let Some(cur) = q.pop_front() {} 相比 while(!q.empty()) { auto cur = q.front(); q.pop(); } 高下立判了()
  • 利用栈进行括号匹配: 优雅, 太优雅了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    fn bracket_match(bracket: &str) -> bool {
    let mut s = Stack::new();
    for c in bracket.chars() {
    match c {
    '(' | '[' | '{' => s.push(c),
    ')' | ']' | '}' => {
    if matches!(s.peek(), Some(&res) if res == map_bracket(c)) {
    s.pop();
    } else {
    return false;
    }
    }
    _ => continue,
    }
    }
    s.is_empty()
    }

    这里 Some(&res) if res == ...matches! 宏提供的守卫, 宏很神奇吧

  • 用两个队列模拟栈: 每次插入时向 q2 插入元素, 然后把 q1 元素全部出队再入 q2 队, 最后交换 q1 q2, 这样 q1 的出队顺序就始终保证是原始数据应当符合的出栈顺序, 用的时候直接弹就行.

  • 二叉堆, 应该是最麻烦的一个, 需要注意有一个 items: vec![T::default()], 这个默认的 0 号节点用于占位, 这样我们就能从 1 开始构建根节点, 因此左子树是 $2 \times i$, 右子树是 $2 \times i + 1$

    • 还是利用 loop while 寻找需要的节点并反复执行操作(如弹出时交换被弹出节点与根节点后执行的下沉操作需要找的对应位置节点)
  • 邻接表存图: 注意数据类型是 HashMap<String, Vec<(String, i32)>>

以上数据结构几乎都不需要在本次实验中手动实现, 但是三阶段你要手写一个 HashMap