地址空间概述
在前面的实验中,我们将所有用户程序直接加载到物理内存的固定位置(APP_BASE_ADDRESS + i * APP_SIZE_LIMIT)。随着系统复杂度的增加,这种直接操作物理地址的方式暴露出了诸多局限性。本章将引入虚拟内存(Virtual Memory)机制,为每个任务提供独立的地址空间(Address Space)。
为什么需要地址空间?
Section titled “为什么需要地址空间?”直接使用物理地址进行内存管理存在以下主要问题:
| 问题 | 说明 |
|---|---|
| 隔离性差 | 缺乏硬件级别的隔离,程序可以随意读写其他程序甚至内核的内存数据,导致系统不稳定。 |
| 内存使用效率低 | 程序必须连续存放,且每个程序的大小上限固定(如 APP_SIZE_LIMIT),导致内存碎片化和浪费。 |
| 重定位困难 | 程序必须在编译/链接时确定运行地址。如果地址被占用,程序将无法运行,难以实现动态加载。 |
地址空间通过在应用程序与物理内存之间引入一层虚拟地址到物理地址的映射,解决了上述问题。在虚拟内存系统中,每个程序都拥有一个独立的、连续的虚拟地址空间。操作系统负责将这些虚拟地址映射到物理内存的非连续页面上。
此机制带来了显著的优势:
- 隔离保护:程序只能访问自己的虚拟地址空间,无法干扰其他程序或内核。
- 简化编程:编译器和链接器只需关注虚拟地址,无需考虑物理内存的实际分布。
- 高效利用:物理内存可以按页按需分配,避免了连续分配带来的碎片问题。
RISC-V SV39 分页机制
Section titled “RISC-V SV39 分页机制”RISC-V 架构提供了多种分页模式。本实验采用 SV39 模式(Sv39 = Supervisor mode, 39-bit virtual address),这是 64 位 RISC-V 系统中常用的分页方案。
当开启分页机制后,MMU (Memory Management Unit,内存管理单元) 会在硬件层面上自动处理地址转换。具体来说,当 CPU 执行指令进行访存时,MMU 会自动根据页表(Page Table)的设置遍历查找,将虚拟地址转换为物理地址。这一过程无须软件干预,从而为不同程序之间的地址隔离提供了坚实的硬件基础。
虚拟地址格式
Section titled “虚拟地址格式”在 SV39 模式中,虚拟地址与物理地址格式如下:
具体而言,64 位的虚拟地址被划分为以下几个部分:
- 扩展位 (Bits 63-39): 必须是第 38 位的符号扩展(即如果 Bit 38 是 0,则高 25 位全为 0;如果 Bit 38 是 1,则全为 1)。否则 CPU 会抛出 Page Fault。
- 虚拟页号 VPN (Bits 38-12): 共 27 位,分为三段(VPN[0], VPN[1], VPN[2]),每段 9 位,分别用于索引三级页表。
- 页内偏移 Offset (Bits 11-0): 共 12 位,直接对应物理页内的偏移量。
物理地址(Physical Address)在 SV39 标准下为 56 位:
分页机制将内存空间划分为大小固定的块:
- 页(Page):虚拟地址空间中的块。
- 页帧(Page Frame):物理内存中的块。
在 SV39 中,标准的页大小为 4 KiB(2^12 字节)。由于页内偏移量为 12 位([0, 4095]),这正好对应了 4 KiB 的大小。
三级页表结构
Section titled “三级页表结构”为了映射 39 位的虚拟地址空间,SV39 采用了三级页表结构。虚拟页号(VPN)被通过 9 位一组划分为三段,分别作为三级页表的索引:
每一级页表占用一个物理页(4 KiB),包含 512 个页表项(每项 8 字节,512 * 8 = 4096)。这种多级结构允许按需分配页表,极大地节省了内存空间。
硬件控制:satp 寄存器
Section titled “硬件控制:satp 寄存器”CPU 通过 satp (Supervisor Address Translation and Protection) 寄存器来控制分页机制的开启与配置:
- MODE (60-63位): 设置为
8表示启用 SV39 分页模式。设置为0表示禁用分页(裸机模式)。 - ASID (44-59位): 地址空间标识符,用于 TLB 优化(本实验暂时忽略)。
- PPN (0-43位): 根页表所在的物理页号。
当操作系统将根页表的 PPN 写入 satp 并开启 MODE 后,CPU 的内存访问请求(除了访问物理内存的指令外)都将经过 MMU (Memory Management Unit) 进行地址转换。
Rust 代码中对应:
本章地址空间布局
Section titled “本章地址空间布局”用户地址空间
Section titled “用户地址空间”TRAMPOLINE = usize::MAX - PAGE_SIZE + 1,因此位于最高虚拟页(0xFFFF_FFFF_FFFF_F000)。TRAP_CONTEXT_BASE = TRAMPOLINE - PAGE_SIZE,因此 TrapContext 紧贴在 Trampoline 下方一页(0xFFFF_FFFF_FFFF_E000)。- 用户栈来自
from_elf的user_stack_bottom..user_stack_top区间,并显式预留了一页 guard page(不映射)用于防止栈越界静默破坏内存。
内核地址空间
Section titled “内核地址空间”跳板页(Trampoline) 是内核和所有用户程序地址空间中都映射到同一物理地址的特殊页。
当 Trap 从用户态切换到内核态时,CPU 会切换页表,但这不影响
__alltraps 和 __restore 代码。
更具体地说:
- 进入用户态前,内核把
stvec设为TRAMPOLINE,因此一旦用户态发生异常/中断,硬件总是先跳到这页中的__alltraps。 __alltraps会先保存寄存器,再切换到内核页表与内核栈,随后进入 Rust 的trap_handler。- 返回用户态时,
trap_return计算__restore在 Trampoline 页内的虚拟地址并跳转执行,最后sret回到用户程序。
之所以必须“内核页表和用户页表都映射同一跳板页”,是为了保证页表切换前后,Trap 入口/出口代码始终可取指执行,不会在最关键的过渡瞬间丢失执行流。