物理内存管理

在管理虚拟内存之前,我们首先需要能够管理物理内存,毕竟所有虚拟内存页都要对应到物理内存页才能使用。

不妨把我们的内存管理模块划分为物理内存管理和虚拟内存管理两个模块。

物理内存管理应当为虚拟内存管理提供这样的接口:

  • 检查当前还有多少空闲的物理页,返回空闲的物理页数目

  • 给出n,尝试分配n个物理页,可以返回一个起始地址和连续的物理页数目,也可能分配一些零散的物理页,返回一个连起来的链表。

  • 给出起始地址和n,释放n个连续的物理页

kern_init()里,我们调用一个新函数:pmm_init()

// kern/init/init.c
int kern_init(void) {
    extern char edata[], end[];
    memset(edata, 0, end - edata);
    cons_init();  // init the console
    const char *message = "(THU.CST) os is loading ...\0";
    cputs(message);
    print_kerninfo();

    idt_init();  // init interrupt descriptor table
    pmm_init();  // 新东西!
    clock_init();   // init clock interrupt
    intr_enable();  // enable irq interrupt
    /* do nothing */
    while (1)
        ;
}

// kern/mm/pmm.c
/* pmm_init - initialize the physical memory management */
void pmm_init(void) {
    // We need to alloc/free the physical memory (granularity is 4KB or other size).
    // So a framework of physical memory manager (struct pmm_manager)is defined in pmm.h
    // First we should init a physical memory manager(pmm) based on the framework.
    // Then pmm can alloc/free the physical memory.
    init_pmm_manager();

    // detect physical memory space, reserve already used memory,
    // then use pmm->init_memmap to create free page list
    page_init();

    // use pmm->check to verify the correctness of the alloc/free function in a pmm
    check_alloc_page();

    extern char boot_page_table_sv39[]; //我们把汇编里定义的页表所在位置的符号声明进来
    satp_virtual = (pte_t*)boot_page_table_sv39;
    satp_physical = PADDR(satp_virtual);//然后输出页表所在的地址
    cprintf("satp virtual address: 0x%016lx\nsatp physical address: 0x%016lx\n", satp_virtual, satp_physical);
}

check_alloc_page()是对物理内存分配功能的一个测试。我们重点关注page_init()

我们在lab2增加了一些功能,方便我们编程:

  • kern/sync/sync.h:为确保内存管理修改相关数据时不被中断打断,提供两个功能,一个是保存 sstatus寄存器中的中断使能位(SIE)信息并屏蔽中断的功能,另一个是根据保存的中断使能位信息来使能中断的功能

  • libs/list.h:定义了通用双向链表结构以及相关的查找、插入等基本操作,这是建立基于链表方法的物理内存管理(以及其他内核功能)的基础。其他有类似双向链表需求的内核功能模块可直接使用 list.h 中定义的函数。

  • libs/atomic.h:定义了对一个二进制位进行读写的原子操作,确保相关操作不被中断打断。包括set_bit()设置某个二进制位的值为1, change_bit()给某个二进制位取反,test_bit()返回某个二进制位的值。

list.h里面实现了一个简单的双向链表。虽然接口很多,但是只要对链表熟悉,不难理解。如果理解不了,可以先去学学数据结构这门课。

看起来list.h里面定义的list_entry并没有数据域,但是,如果我们把list_entry作为其他结构体的成员,就可以利用C语言结构体内存连续布局的特点,从`list_entry的地址获得它所在的上一级结构体。

于是我们定义了可以连成链表的Page结构体和一系列对它做操作的宏。这个结构体用来管理物理内存。

(抄自rcore tutorial)

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

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

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

物理内存探测

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

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

扩展 Qemu 模拟的 RISC-V virt 计算机中的物理内存

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

起始地址

终止地址

含义

0x0

0x100

QEMU VIRT_DEBUG

0x100

0x1000

unmapped

0x1000

0x12000

QEMU MROM (包括 hard-coded reset vector; device tree)

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 的大小,默认是 128MiB128\text{MiB} 。因此,默认的 DRAM 物理内存地址范围就是 [0x80000000,0x88000000) 。我们直接将 DRAM 物理内存结束地址硬编码到内核中:

但是,有一部分 DRAM 空间已经被占用,不能用来存别的东西了!

  • 物理地址空间 [0x80000000,0x80200000) 被 OpenSBI 占用;

  • 物理地址空间 [0x80200000,KernelEnd) 被内核各代码与数据段占用;

  • 其实设备树扫描结果 DTB 还占用了一部分物理内存,不过由于我们不打算使用它,所以可以将它所占用的空间用来存别的东西。

于是,我们可以用来存别的东西的物理内存的物理地址范围是:[KernelEnd, 0x88000000) 。这里的 KernelEnd 为内核代码结尾的物理地址。在 kernel.ld 中定义的 end 符号为内核代码结尾的虚拟地址。

为了管理物理内存,我们需要在内核里定义一些数据结构,来存储”当前使用了哪些物理页面,哪些物理页面没被使用“这样的信息,使用的是Page结构体。我们将一些Page结构体在内存里排列在内核后面,这要占用一些内存。而摆放这些Page结构体的物理页面,以及内核占用的物理页面,之后都无法再使用了。我们用page_init()函数给这些管理物理内存的结构体做初始化。下面是代码

page_init()的代码里,我们调用了一个函数init_memmap(), 这和我们的另一个结构体pmm_manager有关。虽然C语言基本上不支持面向对象,但我们可以用类似面向对象的思路,把”物理内存管理“的功能集中给一个结构体。我们甚至可以让函数指针作为结构体的成员,强行在C语言里支持了”成员函数“。可以看到,我们调用的init_memmap()实际上又调用了pmm_manager的一个”成员函数“。如果你不熟悉函数指针的用法,可以读一读《The C Programming Language》的相关章节(待补充:第几章第几节?)。

pmm_manager提供了各种接口:分配页面,释放页面,查看当前空闲页面数。但是我们好像始终没看见pmm_manager内部对这些接口的实现,那些接口只是作为函数指针,作为pmm_manager的一部分,我们需要把那些函数指针变量赋值为真正的函数名称。

还记得最早我们在pmm_init()里首先调用了init_pmm_manager(), 在这里面我们把pmm_manager的指针赋值成&default_pmm_manager, 看起来我们在这里实现了那些接口。

到现在,我们距离完整的内存管理, 就只差default_pmm_manager结构体的实现了。我们主要在里面实现了页面分配算法。

最后更新于

这有帮助吗?