0%

前三阶段总结报告

各阶段总结

第一阶段

rust的基本筛查,没什么说的。rust的诸多库日后再学,目前就系统库使用比较多。

第二阶段

这里是一个简单的小操作系统

好的,这是参照您提供的原文,并进行书面化、精炼化处理后的中文版本:

Chapter 1: 应用程序与基本执行环境

本章旨在阐明操作系统作为系统软件与应用层软件在执行环境层面存在的本质差异,而非对操作系统自身的复杂理论进行深入剖析。
同时,通过具体的代码范例,演示了在操作系统此种独特的执行环境下构建可执行程序的途径。

用户态程序的运行普遍依赖于特定的语言运行时环境及其附带的标准库。所谓运行时,通常包含预先编译的代码片段,用以实现诸如数组边界检查等基础功能;
标准库则对底层系统调用进行了封装,例如,常见的打印函数其内部便是通过write系统调用将数据输出至显示设备。
操作系统亦承担了程序运行的准备与善后职责,涵盖虚拟内存空间的辟配、程序代码的加载以及退出时的资源清理等环节。
但是,操作系统是直接运行于硬件之上的,并无现成的系统调用或预置软件可供利用,一切功能从零开始编码实现。

Chapter 2: 批处理系统

本章的核心议题集中于系统调用的具体实现过程,此过程涉及较多工程实践层面的细节。从上下文保存与恢复的视角来看,系统调用与常规的函数调用具有相似性:
两者均要求在调用发生前保存当前的执行上下文(主要指寄存器状态),并在调用返回时予以恢复,
这与编译原理中函数调用的序言(prologue)和尾声(epilogue)机制异曲同工。

其主要差异体现在以下两个方面:

  1. 操作系统必须负责保存所有的通用寄存器(因为用户态的调用约定对此并无保障)以及一系列关键的控制状态寄存器(CSRs),例如sstatus(记录CPU当前的特权级别)、sepc(记录发生trap时的指令地址,即trap返回后应继续执行的地址)、scause/stval(分别记录trap发生的原因及相关的附加信息)。这些CSRs对于用户态程序而言通常是透明的。
  2. 出于安全性的考量,操作系统必须为内核态的执行维护一个独立的内核栈,而非在用户栈的基础上进行扩展。RISC-V架构为此提供了sscratch寄存器,可用于保存内核栈的地址。当陷入(trap)发生时,操作系统可以从sscratch寄存器中获取内核栈的起始地址,并切换至内核栈上开始后续处理。

另外,在此阶段,所有的内存操作均基于物理地址进行,尚未实现内核态与用户态之间的内存隔离。
理论上,应用程序能够访问包括内核栈在内的所有物理内存区域,这具有极大的潜在风险。此内存隔离问题将在后续实现虚拟内存机制时得到解决。

Chapter 3: 多道程序与分时多任务

第三章引入了抢占式(preemptive)机制。这使得操作系统能够以分时复用的方式交替运行多个程序,从而显著提升了系统的交互体验。

实现分时系统的关键技术在于引入了时钟(timer)中断。定时器会周期性地产生中断信号,使操作系统得以重新获得CPU的控制权,
并依据预设的调度策略来决定是否需要切换到另一个应用程序执行。这种应用程序间的切换过程,与系统调用的处理流程相似,均涉及到中断当前应用的执行并在稍后某个时刻恢复其运行,
因此同样需要对应用程序的运行上下文进行细致的保存与恢复。

在本章中,应用程序被抽象为“任务”(Task)的实体,并通过“任务控制块”(Task Control Block, TCB)的数据结构来进行统一的调度与管理。
实验任务之一是实现trace系统调用。一个值得注意的实现细节是,若实验要求分别统计每个应用程序的系统调用次数,这通常需要借助特定的数据结构
(例如二维数组)来实现。在设计实现时,若将此统计结构直接置于TaskControlBlock内部,可能会因TaskControlBlock自身内存布局或大小的限制而引发内核崩溃。
因此,将此类统计结构设计在更高层级的TaskManager中,通常是更为稳妥和健壮的方案。

Chapter 4: 地址空间

本章的核心内容围绕虚拟内存的实现展开。通过引入虚拟内存机制,每个应用程序都能拥有各自独立的、私有的地址空间,从而无需关心其在物理内存中的实际存储位置。
虚拟内存的管理主要依赖于诸如MemorySetMapArea(原文提及MemoryMap,但在教程实践中MapArea更为常用)等精心设计的数据结构,
其核心设计思想与Linux等成熟操作系统中的虚拟内存管理机制存在共通之处。

硬件层面,内存管理单元(MMU)承担了虚拟地址到物理地址的自动翻译工作。操作系统仅需将页表的根节点指针(例如在RISC-V架构中,
此指针存放于satp寄存器)加载到特定的控制寄存器中,即可启用地址翻译功能。

虚拟内存机制的引入,对内核的整体结构进行了一次较大规模的重构。其主要原因在于,先前在系统调用处理和程序切换过程中所保存的上下文内容,
由于地址空间的根本性变革,必须进行相应的调整以适应新的内存模型。此过程涉及到若干复杂的技术细节:
例如,在启用虚拟内存之后,当程序从用户态陷入(trap)到内核态时,内核需要借助应用态的页表(或预设的内核自身映射关系)来访问并保存用户程序的上下文信息。
此外,由于内核与应用程序的地址空间通常是不同的(或者说,它们各自拥有不同部分的内存映射),在从用户态陷入内核态之前,
往往需要借助一个位于固定虚拟地址的“跳板页”(trampoline page)来实现上下文的平滑、安全切换。这些具体的设计考量与实现细节,
可能是在先前纯理论学习阶段未曾深入探究的。

本章实验中的一个难点可能在于sys_trace函数的移植。当需要向目标地址写入数据时,
一个常见的理解误区涉及对物理页号(Physical Page Number, PPN)的认知:PPN本身并非一个带有零填充的完整物理地址,它仅仅是物理页帧的一个编号。
正确的物理地址应通过将PPN左移页大小对应的位数(PAGE_SIZE,通常为12位,即乘以$2^{12}$),再加上页内偏移(offset)来计算得到。

Chapter 5: 进程

本章正式引入了“进程”(Process)这一核心概念。进程可以被视为前四章所学知识(例如内存管理、任务调度等)的进一步抽象、封装与整合。
进程抽象使得操作系统能够支持更为丰富和高级的语义,例如实现更为灵活多样的进程调度策略,
以及通过fork()exec()等经典的系统调用来创建新进程和加载执行新程序。本章还通过实现一个简易的Shell,使得该实验性操作系统在功能完备性上又迈进了一步。

本章内容主要在于如何有机地结合前述章节所介绍的技术手段来实现进程这一核心概念,并补充了进程管理相关的诸多细节,
例如初始进程(init process)的启动流程、进程正常或异常退出后的资源回收(reap)机制等。

本章的实验内容可能包括:

  • spawn系统调用:其实现可视为forkexec系统调用的某种组合。但其区别在于,spawn通常用于创建一个全新的进程来执行一个指定的程序,
  • 它会为新进程创建全新的地址空间(MemorySet)并加载目标程序,而不是像fork那样完全复制父进程的地址空间。
  • stride调度算法:其实现主要是在调度器(例如TaskManager)的数据结构中为每个进程维护其步长(stride)和优先级(priority)等属性,
  • 并依据这些属性进行调度决策。在透彻理解相关数据结构之后,该算法的实现过程相对较为清晰。

Chapter 6: 文件系统

本章介绍了文件系统的具体实现,这一机制使得程序和数据能够持久化地存储在块设备(如磁盘)之上,而不仅仅是暂时驻留在易失性的内存之中。
尽管文件系统的基本理论可能已在其他课程中有所涉猎,但通过本章的实际动手实现,学习者可以深入了解到诸多实践层面的细节,
例如操作系统是如何与磁盘驱动程序进行交互的。由于磁盘驱动程序通常已经提供了一层硬件抽象(例如通过BlockDevice trait定义接口),
文件系统的实现便主要集中在遵循这些预定义的接口规范,并构建文件系统自身的逻辑结构(例如inode、dentry、superblock等核心概念的实现)。

在实验中实现硬链接(hard link)功能时,正确的处理方式是在目录条目(directory entry)中将新的文件名链接到目标文件的inode之上,
而不是为每一个硬链接都创建一个全新的inode副本。

实验过程中可能的问题:在向DiskInode等核心数据结构中添加过多字段时,有时可能会偶发性地触发与虚拟内存相关的LoadPageError或类似的错误,
且这种错误行为可能表现出一定的随机性或平台依赖性。其深层原因可能与数据结构在内存中的对齐方式、大小变化触及了某些边界条件,
或是与底层内存管理模块之间存在一些微妙的交互问题有关,这类问题通常具有较高的调试难度。

此外,由于引入了标准输入(stdin)和标准输出(stdout)的概念(它们通常作为文件描述符0和1存在),在通过spawn或类似机制创建新进程时,
必须确保正确初始化其文件描述符表,特别是stdinstdout的指向,
否则可能导致新创建的进程无法正常进行输入输出操作(例如,测试程序可能无法打印任何内容到控制台)。

Chapter 7: 进程间通信

本章内容有助于加深对Shell命令执行机制的理解,特别是关于管道(pipe)和重定向(redirection)的处理方式。例如,对于形如a | b的管道命令,
以及包含输入/输出重定向的复杂命令(如cmd xx > yy << zz),
其解析和执行流程并非简单的串行化处理,而是由Shell进行统一的调度与编排,从而在不同程序(进程)之间建立起正确的数据流。

本章的核心技术在于,通过为不同类型的IPC(Inter-Process Communication,进程间通信)资源(例如管道Pipe)实现一个统一的File
trait(可理解为接口或特性),使得进程可以使用与文件操作相一致的文件描述符来管理这些IPC资源。举例来说,标准输出可以被视为一个实现了File
trait的对象,对其执行write操作最终会将数据导向屏幕显示或传递给管道的下一级。

I/O重定向(例如使用><符号)也遵循相似的原理:操作系统层面通过修改目标进程文件描述符表中的相应条目,
使其指向特定的文件或其他I/O端点,从而实现数据流向的改变。

对于其他形式的进程间通信机制,例如基于环形缓冲区(ring buffer)实现的管道,同样可以为其封装readwrite等文件操作接口。
当进程调用这些接口时,数据被读入或写入共享的环形缓冲区,以此方式实现了进程间的数据交换。

Chapter 8: 并发

本章探讨了操作系统与并发编程中的若干经典并发控制机制,例如锁(包括互斥锁Mutex、自旋锁SpinLock)、信号量(Semaphore)以及条件变量(Condition Variable)。

本章关于线程(Thread)管理的具体实现尤为值得关注。在此阶段,rCore的调度模型进一步向类Linux系统演进:线程成为基本的调度单位,
而进程(Process)则更多地扮演资源容器的角色,负责管理诸如文件描述符表(fd_table)、地址空间(MemorySet)等由同一进程内所有线程共享的资源。

为了支持多线程,进程需要为每一个在其内部运行的线程维护独立的运行上下文(Trap Context)和内核栈(Kernel Stack)。
由于同一进程内的所有线程共享相同的地址空间,它们的线程栈(用户态栈)通常被统一分配和管理在进程用户地址空间内的特定区域
(或者由内核统一管理,但对用户而言是透明的)。线程切换的机制,在保存和恢复上下文方面,与早期章节中描述的任务切换机制基本一致。
若发生跨进程的线程切换(即从一个进程中的线程切换到另一个不同进程中的线程),switch函数除了需要切换线程的上下文外,
还必须负责切换地址空间(即更换当前生效的页表),以确保线程能够在新的目标进程的地址空间中正确运行。

本章的实验任务之一是实现银行家算法(Banker’s Algorithm),这是一个经典的用于避免死锁的算法。

第三阶段

arcore 阶段,这是另一个更严谨也更复杂一点的系统

组件化基础 - Unikernel内核

课堂

组件化内核设计,即通过选择和组合可复用的软件模块来构建内核。这种方法使得构建各种类型的内核(从简单的 Unikernel
到更复杂的宏内核或 Hypervisor)具有灵活性,可以通过增量添加或扩展组件来实现。系统的启动过程也正体现了这一理念:
首先由特定于体系结构的硬件抽象层 (axhal) 进行早期设置(如 MMU 和栈初始化),然后转换到通用运行时环境 (axruntime),
后者会初始化日志、内存分配器和调度器等更多系统,并最终运行主应用程序。通过features可以实现定制化,允许开发者编译仅包含特定应用所需组件的内核。

之后我们讲了内存管理。

内存管理 是一个关键方面,包含两个核心领域:

分页 (paging) 是分阶段实现的。这类似于现代操作系统(如 Linux 或 Windows)中的虚拟内存机制。
一个强制的初始阶段建立基本的内存映射(例如,内核代码和数据的直接映射);
一个可选的第二阶段则重建更完整、更细粒度的虚拟到物理地址映射 ,这对于管理更大的地址空间(如整个物理内存上限)和设备 MMIO(内存映射 I/O,例如 UART、VirtIO 插槽的物理地址 0x1000_0000 等被映射到虚拟地址 0xffff_ffc0_1000_0000 )至关重要。

通过 axalloc 组件引入了 动态内存分配,它提供了一个 GLOBAL_ALLOCATOR 。这与 C 语言中的 malloc/free 或 C++ 中的 new/delete 作用类似。该分配器支持字节和页分配,并采用了多种算法(如文稿中提到的 TLSF、Buddy、Slab 算法 ),从而能够支持像 Rust 的 Vec(动态数组)或 HashMap(哈希表)这类需要动态调整大小的数据结构 。

任务管理与调度对于实现并发至关重要。系统定义了任务状态(如图中所示的运行、就绪、阻塞、退出 ),并使用 TaskContext(保存了如 ra 返回地址、sp 栈指针以及 s0-s11等被调用者保存寄存器 )在上下文切换期间保存和恢复任务状态。探讨了多种调度算法:

协作式 FIFO调度:任务会一直运行,直到完成或主动调用 yield_now 放弃 CPU。这与早期单任务操作系统或者一些需要应用明确出让控制权的系统(如早期的 Windows)相似。
但是,如果一个任务陷入死循环或长时间不调用 yield_now,其他任务将得不到执行机会,造成系统“假死”。

抢占式轮询 (Round Robin, RR) 调度:为任务分配时间片(如 MAX_TIME_Slice ),当定时器中断发生,发现当前任务时间片耗尽时,调度器可以抢占该任务,并将其移至就绪队列的末尾 。这常见于简单的实时操作系统或分时系统。
注意时间片设置过短会导致频繁的上下文切换,增加系统开销;设置过长则会降低系统的响应速度。
完全公平调度器 (Completely Fair Scheduler, CFS):这种抢占式算法使用一个名为 vruntime (虚拟运行时间) 的概念来确定任务优先级 。vruntime 最小的任务将下一个运行,而 vruntime 的值受任务实际运行时间和其 ‘nice’ 值(优先级)的影响 。Linux 当前的默认调度器就是 CFS 的一种实现。

最后是设备交互与文件系统:

设备管理框架 (AllDevices) 为访问不同类型的设备(如块设备、网络设备和显示设备 )提供统一途径,类似于 Windows 中的设备管理器或 Unix 系统中的 /dev 目录。例如,当需要访问某个 VirtIO 块设备时,上层会通过 AllDevices 查找对应的驱动实例。
注意,设备驱动在 probe 阶段(如 probe_bus_devices 或针对 QEMU VirtIO MMIO 的地址范围探测 )如果未能正确识别硬件或初始化失败,设备将无法使用。我在做题时因为漏了一个参数卡了半天(真半天)

VirtIO 模型被用作设备驱动程序的示例,突出了 vring(一种共享内存环形缓冲区)在客户机和主机之间高效通信以及中断处理方面的应用 。这是一种在虚拟化环境中(如 QEMU )常用的半虚拟化设备接口。

题目

大部分题都在这一阶段,print_color更改下宏的定义就可以。hashmap我参考了hashbrown

hashbrown

hashbrown 之所以高效,主要归功于其精妙的SwissTable算法实现,该算法源自 Google。其核心优势在于缓存友好 (cache-friendliness) 和SIMD (Single Instruction, Multiple Data) 指令集的有效利用。

下面是一些核心要点:

开放寻址 (Open Addressing):与链式哈希表(separate chaining)不同,hashbrown 使用开放寻址策略,将所有条目直接存储在主哈希表数组中。这减少了指针间接寻址带来的开销,并提高了缓存局部性。当发生哈希冲突时,它会探测(probe)数组中的其他位置,直到找到一个空槽。

二次探测:简单易懂,就是遇到冲突后再次探测

元数据与主数据分离存储: hashbrown 将每个槽的元数据(例如哈希值的高位和控制字节)与实际的键值对分开存储。元数据通常更小,可以更紧凑地排列。

SIMD 优化查找: hashbrown 利用 SIMD 指令并行地检查多个元数据槽。它将哈希值的一部分(通常是高7位)存储在控制字节中。在查找、插入或删除操作时,可以加载一组控制字节(例如16个字节),并使用单个 SIMD 指令与目标哈希值的高位进行比较。这极大地加速了匹配过程,尤其是在哈希表密集的情况下。

特殊控制字节 (Special Control Bytes):控制字节不仅存储部分哈希值,还用于标记槽的状态,例如:

EMPTY: 槽为空。
DELETED (或称 TOMBSTONE): 槽之前被占用但现在已删除。这对于开放寻址的正确探测至关重要。
FULL: 槽已占用,并存储了部分哈希值。
低开销的元数据: 每个条目仅需要1字节的元数据开销(控制字节),这比许多其他哈希表实现要低得多,从而提高了内存效率。

默认使用快速的非加密哈希算法: hashbrown (以及现在 Rust 标准库中的 HashMap)通常默认使用像 FxHash 或类似的快速非加密哈希算法。这类哈希算法对于整数等简单键类型非常快,但可能不适用于需要抵抗哈希洪水攻击 (HashDoS) 的场景。

bump内存算法
这里我们的bump_allocator是一个双端分配器,一边是零散的小字节分配,一边是整页分配。只要注意好内存检查(插入内存有没有侵入已分配的这端的,以及
如果分配的特别多会不会影响到另一端的),然后两个标记指明当前内存分配位置即可。

Monolithic Kernel宏内核

课堂

异构内核扩展

核心目标是在保证可扩展性和高性能的前提下,让不同内核架构(如Unikernel、宏内核、Hypervisor)能够复用核心组件(称为”Backbone”)和系统服务 。

任务(Task)单元的扩展 是关键,因为任务是内核资源的集合体 。Unikernel的任务可能仅包含基本的上下文信息,而宏内核任务需要页表信息和文件描述符,Hypervisor任务则需要VCPU状态等 。
为了优雅地处理这些差异,而不直接修改核心Task结构(这会降低可读性和异构扩展性 ),或者避免像旧版Starry那样通过TaskID索引外部结构(这会带来性能开销 ),ArceOS引入了 extension域机制 。
这类似于在C语言结构体中预留一个void*指针,具体类型由使用者定义。具体做法是在Task结构中增加一个extension指针,外部模块可以预先定义好特定于某种内核模式的扩展对象(例如,宏内核的进程控制块),
然后在创建任务时在堆上分配内存给这个扩展对象,并将extension指针指向它 。这样,访问扩展域的开销接近于普通结构体成员访存,同时保证了不同内核模式按需扩展的灵活性 。
例如,宏内核的扩展可以包含指向其独立地址空间(页表)的引用和文件描述符表。未来的工作甚至设想了扩展的泛型化,使得不同类型的任务(Unikernel任务、宏内核进程、自定义任务)
可以携带不同的扩展,并在同一套调度、文件系统、网络框架下运行,例如在众核处理器上,某些核心运行Unikernel任务进行高性能计算,另一些核心运行宏内核任务负责管理和交互 。

系统服务的复用 也是一个重要议题,特别是如何让Unikernel已经提供的服务(如文件描述符表 Fd_table、虚拟内存管理、API处理器)方便地被其他架构(如宏内核)复用 。
挑战在于资源的隔离与共享:Unikernel中资源通常是全局唯一的,而宏内核中资源则归属于进程,并通过clone等机制控制共享 。
一个具体的复用目标是arceos_posix_api,它原本为Unikernel提供POSIX接口适配,但在宏内核场景下,可以直接复用其复杂的语义检查逻辑作为SYSCALL层的一部分,而无需为宏内核重新实现一套,从而减少冗余和开发精力 。
为了实现这种复用,引入了Resource(定义资源,使用Arc指针管理以支持共享)和NameSpace(保存所有Resource的集合)的概念 。对于Unikernel,NameSpace是全局唯一的;对于宏内核,每个任务(进程)拥有一份NameSpace,可以动态分配 。
Global namespace的布局可以在编译期通过link_section将所有全局Resource集中在特定段(如axns_resource段)来确定 。
当宏内核需要独有的NameSpace时,可以在堆上分配空间并将全局NameSpace拷贝过去;需要共享时,则利用Arc指针指向全局或父进程的Resource,
这样修改对共享的线程间可见,同时避免了不必要的拷贝开销 。这种设计使得新功能(如新的系统调用或资源类型)更容易接入,并与组件化思想联动。

总结来说,面对未来多样化的应用场景(如智能家居中需要高性能Unikernel的边缘节点和需要安全性的微内核或宏内核的控制中枢 ),组件化为异构内核的快速构建提供了有力支持,
它通过Unikernel基座和精心设计的扩展机制(如Task extension和Namespace),在最大化组件复用和灵活性的同时,兼顾了不同内核模式的特性需求 。
宏内核演进

核心在于引入用户特权级、独立的用户地址空间以及系统调用机制,最终目标是能够直接运行原始的Linux应用程序(二进制)。

从未经修改的Linux应用(通常使用glibc或musl-libc编译)的角度看,它期望运行在一个与内核隔离的用户地址空间,并通过标准的系统调用(syscall)接口与内核交互 。
为了实现这种兼容性,组件化宏内核需要在几个层面进行适配:

用户地址空间的创建与管理:与Unikernel共享单一地址空间不同,宏内核为每个用户应用(进程)创建独立的页表和地址空间(AddrSpace对象)。
通常,页表的高端部分映射内核空间(所有进程共享),低端部分则映射独立的用户应用空间 。这意味着加载应用时,
内核会将ELF文件的代码段和数据段加载到用户地址空间的预定虚拟地址 。

ELF格式应用的加载:大多数应用编译为ELF格式。内核需要解析ELF头获取入口点(Entry point)以及各个段(Segment)的信息,
特别是类型为LOAD的段(代码段和数据段)。需要注意,ELF文件中段的偏移(Offset)和文件大小(FileSiz)可能与加载到内存后的虚拟地址(VirtAddr)
和内存大小(MemSiz)不同,尤其是BSS段(未初始化数据段),它在ELF文件中不占空间,但加载时需要在内存中预留并清零 。

用户栈的初始化:Linux应用(尤其是使用标准C库的应用)的main函数执行前,C库的启动代码会检查用户栈上的参数,如argc(参数个数)、
argv(参数指针数组)和envp(环境变量指针数组),甚至auxv(辅助向量)。因此,内核在切换到用户态执行应用首条指令前,
必须在用户栈上按照约定的布局准备好这些信息,并正确设置用户栈指针(SP寄存器)。
特权级切换与系统调用处理:应用通过特定的指令(如RISC-V的ecall )陷入内核态发起系统调用。内核需要在异常处理流程中识别出系统调用,
并根据调用号分发到相应的处理函数 。例如,示例m_3_0中涉及的set_tid_address (96), ioctl (29), writev (66), exit_group (94)等都是具体的系统调用 。
对于从内核态首次启动用户任务,由于多数架构没有直接的“切换到用户态”指令,一种常见的做法是在内核态伪造一个来自用户态的异常上下文现场(包括用户态的PC、SP、状态寄存器等),
然后通过异常返回指令(如RISC-V的sret)“返回”到用户态开始执行应用 。

任务属性扩展:为了在复用Unikernel调度机制的同时支持宏内核的进程概念,TaskInner结构通过ext成员进行了扩展 。
这个ext可以是一个空结构体(对于Unikernel),也可以是一个包含进程特有信息(如指向其用户地址空间AddrSpace的引用、进程ID等)的结构体(对于宏内核)。
这使得调度器只关注通用的任务属性,而将资源管理等模式相关的特性封装在扩展中 。

为了支持更高级的内存管理特性,如 缺页加载(Lazy Loading)和内存映射(mmap),内核需要处理页错误(Page Fault)异常 。
当应用访问一个尚未建立有效物理映射的虚拟地址时(例如,通过init_user_stack(&mut uspace, false)设置用户栈为Lazy映射 ),会触发页错误。
内核的页错误处理函数(如handle_page_fault)会介入,它通常会查找该虚拟地址所属的MemoryArea(内存区域)。每个MemoryArea关联一个Backend,
负责具体的映射操作 。Backend可以有两种主要类型 :

Linear Backend:用于已存在的、连续的物理内存区域的直接映射,例如设备MMIO或共享内存。

Alloc Backend:用于按需分配物理页帧并建立映射。在缺页异常发生时,Alloc后端会申请物理页帧,然后在页表中补齐映射 。
sys_mmap系统调用的实现也与此类似:如果populate参数为true,则立即分配并映射;如果为false,则仅建立空映射,
等待后续访问时通过缺页异常来实际分配物理页面 。

此外,为了更好地兼容Linux生态,还需要支持像ProcFS、SysFS这样的 伪文件系统,它们提供内核和进程信息的接口(如/proc)或暴露设备信息(如/sys)。
在ArceOS中,这些可以通过axfs_ramfs的实例或专门的axfs_devfs组件来实现 。

题目

page_fault

缺页相应,这里其实系统里已经有不少实现了,我们只需要获取当前用户空间,然后调用find_free_area和map_alloc就可以了

ramfs_rename

这里首先要修正依赖,让测试依赖本地的axramfs(笑),这里依赖本地的库之后,在dir.rs中添加rename的函数,这里底层是用了BTreeMap来保存
已经申请过的文件,然后值对应着在内存中的物理节点。更改BTreeMap就可以了,字符串处理仿照其他实现。