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
调用时,用户程序将文件描述符、缓冲区地址和长度分别存入 a0
、a1
、a2
,内核通过 a0
返回实际写入的字节数。
为确保安全性,内核通过 内存保护机制 阻止用户程序直接访问内核空间。例如,用户程序若尝试访问高于 0x80000000
的内核地址,将触发页错误异常(scause=13
),内核根据异常类型终止进程或按需分配物理页。
2. 批处理系统
批处理系统的核心设计是 顺序执行多个用户程序,每个程序独占 CPU 直至完成。内核在编译阶段将多个应用程序的 ELF 文件链接到内核镜像中,并在固定物理地址(如 0x80400000
)依次加载。程序加载时,内核执行以下步骤:
- 内存清零:清除前一程序残留的数据,防止信息泄漏。
- 页表隔离:为每个程序创建独立的页表,仅映射其代码段、数据段和用户栈,避免越界访问。
- 上下文初始化:设置程序的入口地址(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
使能)触发调度器执行以下流程:
- 递减当前任务的时间片计数器,若归零则标记为就绪状态。
- 从就绪队列中选择下一个任务(如按优先级或轮询策略)。
- 调用
__switch
切换上下文,并更新satp
以切换地址空间。
协作式调度 依赖任务主动调用 sys_yield
,适用于 I/O 密集型任务减少切换开销;抢占式调度 则通过中断强制切换,适合 CPU 密集型任务提升吞吐量。内核需处理 临界区竞争,例如在修改任务队列时关闭中断,防止并发修改导致数据结构损坏。
4. 地址空间
地址空间通过 Sv39 分页机制 实现虚拟内存管理,支持 512GB 的虚拟地址范围。虚拟地址被划分为三级页表索引(VPN[2]、VPN[1]、VPN[0]),每级页表包含 512 个条目(8 字节/条目)。内核维护 全局页帧分配器,按需分配物理页帧并更新页表条目(PTE)的权限位(如 R/W/X
和 U
位)。
用户程序访问虚拟地址时,若触发缺页异常(scause=12/13/15
),内核执行以下处理:
- 检查访问地址是否合法(如位于用户代码段或堆栈范围内)。
- 分配物理页帧,建立虚拟地址到物理地址的映射。
- 刷新 TLB(通过
sfence.vma
指令),确保后续访问使用新页表。
内核自身采用 恒等映射(虚拟地址等于物理地址),简化直接内存访问(如操作外设寄存器)。用户程序通过动态映射访问特定设备(如 MMIO 区域),需在内核中注册设备内存范围并配置页表权限(如设置为不可执行)。
5. 进程与进程管理
进程是资源分配的基本单位,其控制块(PCB)包含以下信息:
- 进程标识符(PID):唯一标识进程的整数。
- 地址空间:指向页表的指针(
satp
值)和内存映射信息。 - 文件描述符表:记录打开的文件、管道或设备。
- 父子关系:父进程 PID 和子进程链表。
进程创建 通过 fork
系统调用实现,内核复制父进程的地址空间(写时复制优化)和文件描述符表,并为子进程分配新的 PID。exec
系统调用则替换当前进程的地址空间,加载新程序的代码和数据。
进程调度 基于状态机模型:
- 运行 → 就绪:时间片耗尽或被高优先级任务抢占。
- 运行 → 阻塞:等待 I/O 完成或信号量资源。
- 阻塞 → 就绪:资源可用或事件触发。
进程退出时,内核回收其物理内存、关闭打开的文件,并通过 waitpid
通知父进程回收终止状态。若父进程已终止,子进程由 init 进程 接管以避免僵尸进程。
6. 文件系统与重定向
rCore 的虚拟文件系统(VFS)抽象了 文件、目录和设备 的统一接口,支持以下操作:
- 文件读写:通过
File trait
的read
和write
方法实现,具体由设备驱动(如 UART)或块设备(如 virtio-blk)完成。 - 目录管理:维护目录项(dentry)链表,支持
mkdir
和readdir
操作。
重定向 通过复制文件描述符实现。例如,执行 echo hello > output.txt
时,shell 进程执行以下步骤:
- 打开
output.txt
并获取文件描述符fd
。 - 调用
sys_dup
复制fd
到标准输出(fd=1
)。 - 子进程继承修改后的文件描述符表,
sys_write
输出到文件而非控制台。
文件系统通过 页缓存 优化性能,将频繁访问的数据缓存在内存中,减少磁盘 I/O 操作。索引节点(inode)记录文件的元数据(如大小、权限和物理块地址),并通过 日志机制 确保崩溃一致性。
7. 进程间通信(IPC)
IPC 机制包括以下实现方式:
- 共享内存:内核分配物理页帧,并映射到多个进程的地址空间。进程通过信号量或自旋锁同步访问。
- 管道:基于环形缓冲区的 FIFO 队列,内核维护
pipe
结构体记录读写位置和等待队列。sys_pipe
创建管道后返回两个文件描述符(读端和写端),进程通过read
和write
系统调用传输数据。 - 信号:内核为每个进程维护信号处理函数表。当进程收到信号(如
SIGKILL
)时,内核修改其 Trap 上下文,强制跳转到注册的处理函数。信号处理完成后,通过sigreturn
系统调用恢复原执行流程。
消息队列 是另一种 IPC 方式,内核维护消息缓冲区,进程通过 msgsnd
和 msgrcv
发送/接收结构化数据,支持阻塞和非阻塞模式。
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
允许数据被多线程共享引用。编译器静态检查数据竞争,确保并发代码的安全性