物理内存探测

优质
小牛编辑
138浏览
2023-12-01

物理内存探测

物理内存的相关概念

我们知道,物理地址访问的通常是一片 DRAM,我们可以把它看成一个以字节为单位的大数组,通过物理地址找到对应的位置进行读写。但是,物理地址并不仅仅只能访问 DRAM,也可以用来访问其他的外设,因此你也可以认为 DRAM 也算是一种外设,物理地址则是一个对可以存储的介质的一种抽象。

而如果访问其他外设要使用不同的指令(如 x86 单独提供了 inout 等指令来访问不同于内存的 IO 地址空间),会比较麻烦;于是,很多指令集架构(如 RISC-V、ARM 和 MIPS 等)通过 MMIO(Memory Mapped I/O)技术将外设映射到一段物理地址,这样我们访问其他外设就和访问物理内存一样了。

我们先不管那些外设,来看物理内存。

物理内存探测

操作系统怎样知道物理内存所在的那段物理地址呢?在 RISC-V 中,这个一般是由 bootloader,即 OpenSBI 固件来完成的。它来完成对于包括物理内存在内的各外设的扫描,将扫描结果以 DTB(Device Tree Blob)的格式保存在物理内存中的某个地方。随后 OpenSBI 固件会将其地址保存在 a1 寄存器中,给我们使用。

这个扫描结果描述了所有外设的信息,当中也包括 QEMU 模拟的 RISC-V Virt 计算机中的物理内存。

[info] QEMU 模拟的 RISC-V Virt 计算机中的物理内存

通过查看 QEMU 代码中 hw/riscv/virt.cvirt_memmap[] 的定义,可以了解到 QEMU 模拟的 RISC-V Virt 计算机的详细物理内存布局。可以看到,整个物理内存中有不少内存空洞(即含义为 unmapped 的地址空间),也有很多外设特定的地址空间,现在我们看不懂没有关系,后面会慢慢涉及到。目前只需关心最后一块含义为 DRAM 的地址空间,这就是 OS 将要管理的 128 MB 的内存空间。

起始地址终止地址含义
0x00x100QEMU VIRT_DEBUG
0x1000x1000unmapped
0x10000x12000QEMU MROM
0x120000x100000unmapped
0x1000000x101000QEMU VIRT_TEST
0x1010000x2000000unmapped
0x20000000x2010000QEMU VIRT_CLINT
0x20100000x3000000unmapped
0x30000000x3010000QEMU VIRT_PCIE_PIO
0x30100000xc000000unmapped
0xc0000000x10000000QEMU VIRT_PLIC
0x100000000x10000100QEMU VIRT_UART0
0x100001000x10001000unmapped
0x100010000x10002000QEMU VIRT_VIRTIO
0x100020000x20000000unmapped
0x200000000x24000000QEMU VIRT_FLASH
0x240000000x30000000unmapped
0x300000000x40000000QEMU VIRT_PCIE_ECAM
0x400000000x80000000QEMU VIRT_PCIE_MMIO
0x800000000x88000000DRAM 缺省 128MB,大小可配置

不过为了简单起见,我们并不打算自己去解析这个结果。因为我们知道,QEMU 规定的 DRAM 物理内存的起始物理地址为 0x80000000 。而在 QEMU 中,可以使用 -m 指定 RAM 的大小,默认是 128 MB 。因此,默认的 DRAM 物理内存地址范围就是 [0x80000000, 0x88000000)。

因为后面还会涉及到虚拟地址、物理页和虚拟页面的概念,为了进一步区分而不是简单的只是使用 usize 类型来存储,我们首先建立一个 PhysicalAddress 的类,然后对其实现一系列的 usize 的加、减和输出等等操作,由于这部分实现偏向于 Rust 语法而非 OS,这里不贴出代码,请参考 os/src/memory/address.rs 文件。

然后,我们直接将 DRAM 物理内存结束地址硬编码到内核中,同时因为我们操作系统本身也用了一部分空间,我们也记录下操作系统用到的地址结尾(即 linker script 中的 kernel_end)。

os/src/memory/config.rs

lazy_static! {
    /// 内核代码结束的地址,即可以用来分配的内存起始地址
    ///
    /// 因为 Rust 语言限制,我们只能将其作为一个运行时求值的 static 变量,而不能作为 const
    pub static ref KERNEL_END_ADDRESS: PhysicalAddress = PhysicalAddress(kernel_end as usize);
}

extern "C" {
    /// 由 `linker.ld` 指定的内核代码结束位置
    ///
    /// 作为变量存在 [`KERNEL_END_ADDRESS`]
    fn kernel_end();
}

这里使用了 lazy_static 库,由于 Rust 语言的限制,我们能对编译时 kernel_end 做一个求值然后赋值到 KERNEL_END_ADDRESS 中;所以,lazy_static! 宏帮助我们在第一次使用 lazy_static! 宏包裹的变量时自动完成这些求值工作。

最后,我们在各级文件中加入模块调用,并在 os/src/main.rs 尝试输出。

os/src/main.rs

/// Rust 的入口函数
///
/// 在 `_start` 为我们进行了一系列准备之后,这是第一个被调用的 Rust 函数
#[no_mangle]
pub extern "C" fn rust_main() -> ! {
    // 初始化各种模块
    interrupt::init();
    memory::init();

    // 注意这里的 KERNEL_END_ADDRESS 为 ref 类型,需要加 *
    println!("{}", *memory::config::KERNEL_END_ADDRESS);

    panic!()
}

最后运行,可以看到成功显示了我们内核使用的结尾地址 PhysicalAddress(0x8020b220);注意到这里,你的输出可能因为实现上的细节并不完全一样。