0%

<2025春夏季开源操作系统训练营第二阶段总结报告-1430329301Lsj>

rcore第二阶段学习心得

以下是针对 rCore 操作系统各个技术方向的详细扩展叙述:

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

rCore 的应用程序执行环境构建在硬件抽象层(HAL)之上,其核心依赖 RustSBI 提供的引导和硬件初始化服务。RustSBI 在启动阶段完成以下关键任务:初始化 CPU 核心、配置物理内存保护区域(如设置 PMP 寄存器)、启用时钟中断和外部设备中断(如 UART 串口)。应用程序以 静态链接的 ELF 格式 加载到内存中,内核解析 ELF 文件头获取代码段(.text)、数据段(.data)和未初始化数据段(.bss)的虚拟地址,并为其分配物理页帧,建立虚拟地址到物理地址的映射。

用户程序运行在 RISC-V U 模式(用户态),通过 ecall 指令触发特权级切换到 S 模式(监管态)执行系统调用。内核在初始化用户环境时,需配置用户栈空间(如分配 8KB 栈内存)并设置 Trap 上下文,确保异常处理时能正确保存和恢复寄存器状态。系统调用参数传递遵循 RISC-V ABI 规范:系统调用号存入 a7,参数依次存入 a0-a6,返回值通过 a0 返回。例如,sys_write 调用时,用户程序将文件描述符、缓冲区地址和长度分别存入 a0a1a2,内核通过 a0 返回实际写入的字节数。

为确保安全性,内核通过 内存保护机制 阻止用户程序直接访问内核空间。例如,用户程序若尝试访问高于 0x80000000 的内核地址,将触发页错误异常(scause=13),内核根据异常类型终止进程或按需分配物理页。


2. 批处理系统

批处理系统的核心设计是 顺序执行多个用户程序,每个程序独占 CPU 直至完成。内核在编译阶段将多个应用程序的 ELF 文件链接到内核镜像中,并在固定物理地址(如 0x80400000)依次加载。程序加载时,内核执行以下步骤:

  1. 内存清零:清除前一程序残留的数据,防止信息泄漏。
  2. 页表隔离:为每个程序创建独立的页表,仅映射其代码段、数据段和用户栈,避免越界访问。
  3. 上下文初始化:设置程序的入口地址(ELF 的 entry 字段)和初始栈指针(sp 指向用户栈顶部)。

当程序主动调用 sys_exit 或发生致命错误(如非法指令)时,内核通过 TaskManager 切换到下一程序。任务切换的关键在于保存当前程序的 任务上下文(TaskContext),包括 ra(返回地址)、sp(栈指针)和 s0-s11(保留寄存器),并通过汇编函数 __switch 切换到目标程序的上下文。批处理系统需确保 原子性切换,即在切换过程中屏蔽中断(通过 sstatus.SIE 位),防止时钟中断导致状态不一致。


3. 多道程序与分时多任务

分时多任务通过 时间片轮转算法 实现 CPU 资源共享。内核为每个任务维护一个 任务控制块(TaskControlBlock, TCB),包含以下元数据:

  • 任务状态:运行(Running)、就绪(Ready)、阻塞(Blocked)。
  • 时间片计数器:记录剩余时间片长度(如 10ms)。
  • 优先级:用于调度决策(如实时任务优先级高于普通任务)。
  • 上下文信息:包括寄存器快照和页表地址(satp 值)。

时钟中断(通过 sie.STIE 使能)触发调度器执行以下流程:

  1. 递减当前任务的时间片计数器,若归零则标记为就绪状态。
  2. 从就绪队列中选择下一个任务(如按优先级或轮询策略)。
  3. 调用 __switch 切换上下文,并更新 satp 以切换地址空间。

协作式调度 依赖任务主动调用 sys_yield,适用于 I/O 密集型任务减少切换开销;抢占式调度 则通过中断强制切换,适合 CPU 密集型任务提升吞吐量。内核需处理 临界区竞争,例如在修改任务队列时关闭中断,防止并发修改导致数据结构损坏。


4. 地址空间

地址空间通过 Sv39 分页机制 实现虚拟内存管理,支持 512GB 的虚拟地址范围。虚拟地址被划分为三级页表索引(VPN[2]、VPN[1]、VPN[0]),每级页表包含 512 个条目(8 字节/条目)。内核维护 全局页帧分配器,按需分配物理页帧并更新页表条目(PTE)的权限位(如 R/W/XU 位)。

用户程序访问虚拟地址时,若触发缺页异常(scause=12/13/15),内核执行以下处理:

  1. 检查访问地址是否合法(如位于用户代码段或堆栈范围内)。
  2. 分配物理页帧,建立虚拟地址到物理地址的映射。
  3. 刷新 TLB(通过 sfence.vma 指令),确保后续访问使用新页表。

内核自身采用 恒等映射(虚拟地址等于物理地址),简化直接内存访问(如操作外设寄存器)。用户程序通过动态映射访问特定设备(如 MMIO 区域),需在内核中注册设备内存范围并配置页表权限(如设置为不可执行)。


5. 进程与进程管理

进程是资源分配的基本单位,其控制块(PCB)包含以下信息:

  • 进程标识符(PID):唯一标识进程的整数。
  • 地址空间:指向页表的指针(satp 值)和内存映射信息。
  • 文件描述符表:记录打开的文件、管道或设备。
  • 父子关系:父进程 PID 和子进程链表。

进程创建 通过 fork 系统调用实现,内核复制父进程的地址空间(写时复制优化)和文件描述符表,并为子进程分配新的 PID。exec 系统调用则替换当前进程的地址空间,加载新程序的代码和数据。

进程调度 基于状态机模型:

  • 运行 → 就绪:时间片耗尽或被高优先级任务抢占。
  • 运行 → 阻塞:等待 I/O 完成或信号量资源。
  • 阻塞 → 就绪:资源可用或事件触发。

进程退出时,内核回收其物理内存、关闭打开的文件,并通过 waitpid 通知父进程回收终止状态。若父进程已终止,子进程由 init 进程 接管以避免僵尸进程。


6. 文件系统与重定向

rCore 的虚拟文件系统(VFS)抽象了 文件、目录和设备 的统一接口,支持以下操作:

  • 文件读写:通过 File traitreadwrite 方法实现,具体由设备驱动(如 UART)或块设备(如 virtio-blk)完成。
  • 目录管理:维护目录项(dentry)链表,支持 mkdirreaddir 操作。

重定向 通过复制文件描述符实现。例如,执行 echo hello > output.txt 时,shell 进程执行以下步骤:

  1. 打开 output.txt 并获取文件描述符 fd
  2. 调用 sys_dup 复制 fd 到标准输出(fd=1)。
  3. 子进程继承修改后的文件描述符表,sys_write 输出到文件而非控制台。

文件系统通过 页缓存 优化性能,将频繁访问的数据缓存在内存中,减少磁盘 I/O 操作。索引节点(inode)记录文件的元数据(如大小、权限和物理块地址),并通过 日志机制 确保崩溃一致性。


7. 进程间通信(IPC)

IPC 机制包括以下实现方式:

  • 共享内存:内核分配物理页帧,并映射到多个进程的地址空间。进程通过信号量或自旋锁同步访问。
  • 管道:基于环形缓冲区的 FIFO 队列,内核维护 pipe 结构体记录读写位置和等待队列。sys_pipe 创建管道后返回两个文件描述符(读端和写端),进程通过 readwrite 系统调用传输数据。
  • 信号:内核为每个进程维护信号处理函数表。当进程收到信号(如 SIGKILL)时,内核修改其 Trap 上下文,强制跳转到注册的处理函数。信号处理完成后,通过 sigreturn 系统调用恢复原执行流程。

消息队列 是另一种 IPC 方式,内核维护消息缓冲区,进程通过 msgsndmsgrcv 发送/接收结构化数据,支持阻塞和非阻塞模式。


8. 并发机制

内核并发通过以下机制管理:

  • 自旋锁(SpinLock):通过原子指令(如 amoswap)实现忙等待,适用于短期临界区(如修改任务队列)。使用前需关闭中断(sstatus.SIE = 0),防止死锁。
  • 互斥锁(Mutex):在锁竞争时让出 CPU,将当前任务加入阻塞队列,切换其他任务执行。解锁时唤醒等待任务。
  • 条件变量(Condvar):与互斥锁配合使用,通过 wait 释放锁并阻塞,notify 唤醒等待任务。适用于生产者-消费者模型。

用户态线程(协程)通过 异步运行时 实现,内核提供轻量级上下文切换(如 swapcontext)和非阻塞 I/O 系统调用(如 read_async)。Rust 的 async/await 语法糖将协程编译为状态机,运行时调度器根据事件(如 I/O 完成)切换协程执行。

内存安全 通过 Rust 的所有权系统和 Send/Sync trait 保障。Send 允许数据跨线程转移所有权,Sync 允许数据被多线程共享引用。编译器静态检查数据竞争,确保并发代码的安全性