物理内存探测
物理内存的相关概念
我们知道,物理地址访问的通常是一片 DRAM,我们可以把它看成一个以字节为单位的大数组,通过物理地址找到对应的位置进行读写。但是,物理地址并不仅仅只能访问 DRAM,也可以用来访问其他的外设,因此你也可以认为 DRAM 也算是一种外设,物理地址则是一个对可以存储的介质的一种抽象。
而如果访问其他外设要使用不同的指令(如 x86 单独提供了 in
和 out
等指令来访问不同于内存的 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.c
的virt_memmap[]
的定义,可以了解到 QEMU 模拟的 RISC-V Virt 计算机的详细物理内存布局。可以看到,整个物理内存中有不少内存空洞(即含义为 unmapped 的地址空间),也有很多外设特定的地址空间,现在我们看不懂没有关系,后面会慢慢涉及到。目前只需关心最后一块含义为 DRAM 的地址空间,这就是 OS 将要管理的 128 MB 的内存空间。
起始地址 终止地址 含义 0x0 0x100 QEMU VIRT_DEBUG 0x100 0x1000 unmapped 0x1000 0x12000 QEMU MROM 0x12000 0x100000 unmapped 0x100000 0x101000 QEMU VIRT_TEST 0x101000 0x2000000 unmapped 0x2000000 0x2010000 QEMU VIRT_CLINT 0x2010000 0x3000000 unmapped 0x3000000 0x3010000 QEMU VIRT_PCIE_PIO 0x3010000 0xc000000 unmapped 0xc000000 0x10000000 QEMU VIRT_PLIC 0x10000000 0x10000100 QEMU VIRT_UART0 0x10000100 0x10001000 unmapped 0x10001000 0x10002000 QEMU VIRT_VIRTIO 0x10002000 0x20000000 unmapped 0x20000000 0x24000000 QEMU VIRT_FLASH 0x24000000 0x30000000 unmapped 0x30000000 0x40000000 QEMU VIRT_PCIE_ECAM 0x40000000 0x80000000 QEMU VIRT_PCIE_MMIO 0x80000000 0x88000000 DRAM 缺省 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)
;注意到这里,你的输出可能因为实现上的细节并不完全一样。