内存管理(页面管理)

内存管理模块分为2部分:物理页面管理和页表管理。前者强调对机器拥有的物理内存的管理,包括建立对应的数据结构、处理分配和回收动作等。而后者主要是强调利用Intel x86系列处理器的页式地址管理功能, 完成(虚拟)线性地址到物理地址的转换,包括建立页目录、页表等。

物理页面管理相关的函数:

  • boot_alloc()
  • page_init()
  • page_alloc()
  • page_free()

背景知识

JOS的启动过程是先把bootsector的内容读到0x7c00处,bootsector中的代码开始执行后,会从磁盘上紧接着自己的第2个扇区开始,一直读8个扇区的内容(一共是8×512=4KB,ELF头的大小)到0x10000(64KB)的地方,然后通过对ELF头的解析,得到kernel模块编译出来后所占的大小,并将kernel读到物理内存0x100000(1MB)开始的地方。

3

然后设置好GDT,并调用i386_init()函数,而i386_init()函数在将自己的BSS区域清零后,调用cons_init()函数设置屏幕显示,为cprintf的运行做 好准备。随后调用i386_detect_memory()函数和i386_vm_init()。后者就是我们内存管理的主要函数了

.bss 节:未初始化的全局变量部分,一部分不会在磁盘有存储空间,因为这些变量并没有被初始化,因此全部默认为0,在将这节装入到内存的时候程序需要为其分配相应大小的初始值为0的内存空间

在调用i386_init()后,系统将重载GDT,新的GDT:

SEG_NULL # null seg
SEG(STA_X|STA_R, -KERNBASE, 0xffffffff) # code seg
SEG(STA_W, -KERNBASE, 0xffffffff) # data seg

新GDT后两项的base,它们是-KERNBASE。如果KERNBASE=0xF0000000,则
GDT的base=0x10000000

页面管理

JOS内核是以页(page)为最小单位,通过链表来管理内存的。它使用MMU来映射,保护每一块被分配出去的内存。操作系统必须要追踪记录哪些内存区域是空闲的,哪些是被占用的。

页面管理相关的数据结构

typedef uint32_t pte_t;
typedef uint32_t pde_t;

struct PageInfo {
    // Next page on the free list.
    // 空闲页面链表next指针
    struct PageInfo *pp_link;

    // pp_ref is the count of pointers (usually in page table entries)
    // to this page, for pages allocated using page_alloc.
    // Pages allocated at boot time using pmap.c's
    // boot_alloc do not have valid reference count fields.

    // 页面状态:空闲/占用
    // 多个不同的虚拟地址被同时映射到相同的物理页上,PageInfo结构体的pp_ref
    // 被位于虚拟地址UTOP之下的虚拟页所映射的次数
    uint16_t pp_ref;
};

下面通过mem_init函数有关页面管理的代码进行简要的分析和流程的梳理。部分代码如下

void mem_init(void)
{
    i386_detect_memory();

    // create initial page directory.
    // kern_pgdir指向操作系统的页目录表,这个页紧跟着操作系统内核之后
    kern_pgdir = (pde_t *) boot_alloc(PGSIZE);
    // 分配一个页的大小,并且将这部分内存清0
    memset(kern_pgdir, 0, PGSIZE);

    // 为页目录表添加第一个页目录表项
    // UVPT:0xef400000,从这个虚拟地址开始,存放的是操作系统的页表kern_pgdir
    // PADDR(kern_pgdir)计算kern_pgdir所对应的真实物理地址
    kern_pgdir[PDX(UVPT)] = PADDR(kern_pgdir) | PTE_U | PTE_P;

    // 分配一块内存,用来存放一个struct PageInfo的数组
    // 数组中的每一个PageInfo代表内存当中的一页,PageInfo为页面链表的节点
    pages = (struct PageInfo *) boot_alloc(sizeof(struct PageInfo) * npages);

    // 内存清0
    memset(pages, 0, npages * sizeof(struct PageInfo));

    page_init();

    check_page_free_list(1);
    check_page_alloc();
}

首先调用i386_detect_memory函数检测现在系统中有多少可用的内存空间。kern_pgdir是指向操作系统的页目录表的指针,操作系统之后工作在虚拟内存模式下时,就需要这个页目录表进行地址转换。

boot_alloc()函数暂时当做页分配器,之后我们使用的真实页分配器是page_alloc()函数而它的核心思想就是维护一个静态变量nextfree,里面存放着下一个可以使用的空闲内存空间的虚拟地址`,所以每次当我们想要分配n个字节的内存时,我们都需要修改这个变量的值

为页目录表添加第一个页目录表项后,分配一块内存,分配npages数目的结构体PageInfo空间,由pages指向该块内存操作系统内核通过这个数组来追踪所有内存页的使用情况。然后运行函数page_init(),这个函数初始化两个信息:pages数组和pages_free_list链表(存放着所有空闲页的信息)

page_init后的内存分布

初始化所有物理内存页的相关数据结构后,check_page_free_list(1)和check_page_alloc(),这两个检查程序检查页面管理是否正确。前者检查page_free_list链表的空闲页,是否都是合法/空闲,后者检查page_alloc(),page_free()两个函数是否能够正确运行