本次实验主要完成ucore内核对物理内存的管理。
ucore被启动后,需要探测系统的物理内存布局来了解哪些物理内存空间是可用的。ucore是使用e820h中断来获取内存信息,而这个中断必须在实模式下使用,因此必须在bootloader引导进入保护模式前进行,这些收集到的数据将保存在物理地址0x8000处,通过代码中定义的e820map结构体进行映射。
启动分页机制
ucore在80386中的分页机制实现了基本平坦模型的段页式内存管理,这是为后续的虚拟内存做好准备。
CR开头的寄存器为控制寄存器。
CR0寄存器包含系统控制标志,这些标志控制着处理器的运行模式和状态。分页(CR0 的第 31 位)。置 1 启用分页,置 0 不启用分页。当禁用分页 时,所有的线性地址都当作物理地址对待。
CR3寄存器包含页目录表的物理基地址和二个标志(PCD和PWT)。该寄存器也被称为页目录基地址寄存器(PDBR)。ucore中用boot_cr3(mm/pmm.c)来记录这个值。
程序中使用的地址都是逻辑地址,逻辑地址/虚拟地址通过GDT/LDT可以转换为线性地址,线性地址可以通过页表来转化为物理地址。
lab2在运行中分为了4个阶段:
1、bootloader阶段,此时virt addr = linear addr = phy addr
2、第二个阶段从kern_entry开始,但是还没执行enable_paging,尚未开启分页机制之前,线性地址都是等于物理地址。此时虚拟地址到线性地址的映射更新了,新的映射关系为 virt addr - 0xC0000000 = linear addr = phy addr
3、第三个阶段从enable_paging函数开始,到执行gdt_init函数。执行完enable_paging函数中的加载CR0指令,即让CR0寄存器中的PG位置1,启动分页,接下来就是段页式的映射关系了。此时段映射还没有为分页机制的开启而再次更新调整,所以映射关系比较微妙,如下所示
virt addr - 0xC0000000 = linear addr = phy addr + 0xC0000000# 物理地址 在0~4MB之外的三者映射关系
virt addr - 0xC0000000 = linear addr = phy addr# 物理地址在0~4MB之内的三者的映射关系
如上的特殊关系是因为pmm_init中的一条代码:
boot_pgdir[0] = boot_pgdir[PDX(KERNBASE)];因为页目录表中每一项代表一个页表,一个页表可以管理4MB的内存大小,因为pgdir中每一项表示4MB的物理内存地址。即线性地址从0开始的4MB和从KERNBASE开始的4MB通过分页机制映射到同一段内存空间中。
4、从get_init函数开始,此时段映射更新调整,并取消了临时的映射关系,形成了我们期望的映射关系。映射关系为virt addr = linear addr = phy addr + 0xC0000000
ucore中页的大小为4KB,每个页都是使用Page数据结构来表示。
struct Page{
int ref;//页帧的引用次数,若有一个虚拟页映射到此页上,则加一
uint32_t flags;//表示该页帧的状态
unsigned int property;//连续内存空闲块的大小,连续空闲块的首个页帧才会设置此属性
list_entry_t page_link;//通用数据结构,链表查找用的。
}
ucore使用了free_area_t这个数据结构进行管理大量零散的空闲连续内存块。
typedef struct{
list_entry_t free_list;//首个空闲连续内存快的entry
unsigned int nr_free;//此链表中的连续空闲块的个数
}free_area_t;
npage=maxpa/PGSIZE;//得到需要管理的物理页个数
pages=(struct Page *)ROUNDUP((void *)end,PGSIZE);//这个地址是bootloader加载ucore的结束地址后以PGSIZE对齐后的地址。
uintptr_t freemem = PADDR((uintptr_t)pages + sizeof(struct Page) * npage);//由于管理内存的所有页帧还需要npage个Page数据结构所以还需要腾出内存空间给Page,后续的内存地址即为空闲物理内存空间。
/* pmm_init - initialize the physical memory management */
static void
page_init(void) {
struct e820map *memmap = (struct e820map *)(0x8000 + KERNBASE);//指针寻址使用的是线性虚拟地址。
uint64_t maxpa = 0;
cprintf("e820map:\n");
int i;
//遍历memmap中的每一项
for (i = 0; i < memmap->nr_map; i ++) {
uint64_t begin = memmap->map[i].addr, end = begin + memmap->map[i].size;
cprintf(" memory: %08llx, [%08llx, %08llx], type = %d.\n",
memmap->map[i].size, begin, end - 1, memmap->map[i].type);
if (memmap->map[i].type == E820_ARM) {
if (maxpa < end && begin < KMEMSIZE) {
maxpa = end;
}
}
}
//如果maxpa超过了定义约束的最大可用物理内存空间,则进行调整
if (maxpa > KMEMSIZE) {
maxpa = KMEMSIZE;
}
extern char end[];//end是ucore kernel加载后定义的第二个全局变量,该变量所在内存地址后续空间均没有被使用,因此以该地址为起点,存放用于管理物理内存的Page数据结构。
npage = maxpa / PGSIZE;
pages = (struct Page *)ROUNDUP((void *)end, PGSIZE);
for (i = 0; i < npage; i ++) {
SetPageReserved(pages + i);//遍历每一个物理页,默认标记为保留
}
uintptr_t freemem = PADDR((uintptr_t)pages + sizeof(struct Page) * npage);
for (i = 0; i < memmap->nr_map; i ++) {
uint64_t begin = memmap->map[i].addr, end = begin + memmap->map[i].size;
if (memmap->map[i].type == E820_ARM) {
if (begin < freemem) {
//限制空闲地址的最小值
begin = freemem;
}
if (end > KMEMSIZE) {
//限制空闲地址的最大值
end = KMEMSIZE;
}
if (begin < end) {
//用PGSIZE对齐地址
begin = ROUNDUP(begin, PGSIZE);
end = ROUNDDOWN(end, PGSIZE);
if (begin < end) {
//空闲内存块的映射,将其纳入物理内存管理器中,用于后续的物理内存管理
init_memmap(pa2page(begin), (end - begin) / PGSIZE);
}
}
}
}
}
代码部分
pte_t *
get_pte(pde_t *pgdir, uintptr_t la, bool create) {
// 该函数通过线性地址来找到找到对应的页表项(二级页表项),并返回这个页表项的虚拟地址
// 获得指定页目录表项的地址
pde_t *pdep = &pgdir[PDX(la)];
// 判断当前页目录项的Present存在位是否为1(对应的二级页表是否存在)
if (!(*pdep & PTE_P)) {
// 对应的二级页表不存在
// *page指向的是这个新创建的二级页表基地址
struct Page *page;
if (!create || (page = alloc_page()) == NULL) {
// 如果create参数为false或是alloc_page分配物理内存失败
return NULL;
}
// 二级页表所对应的物理页 引用数为1
set_page_ref(page, 1);
// 获得被page变量管理的页帧的物理地址
uintptr_t pa = page2pa(page);
// 将上述指定页帧全部填满0,函数的参数要求是虚拟地址,因此需要将物理地址转化为虚拟地址
memset(KADDR(pa), 0, PGSIZE);
// la对应的一级页目录项进行赋值,使其指向新创建的二级页表(页表中的数据被MMU直接处理,为了映射效率存放的都是物理地址)
// 或PTE_U/PTE_W/PET_P 标识当前页目录项是用户级别的、可写的、已存在的
*pdep = pa | PTE_U | PTE_W | PTE_P;
}
// 要想通过C语言中的数组来访问对应数据,需要的是数组基址(虚拟地址),而*pdep中页目录表项中存放了对应二级页表的一个物理地址
// 由于内存中的每个页帧都是4KB大小整齐分配,因此每个页帧的物理地址的低12位必然都是0,这低12位有其他作用和二级页表的地址无关
//PDE_ADDR将*pdep的低12位抹零对齐(指向二级页表的起始基地址),再通过KADDR转为内核虚拟地址,进行数组访问
// PTX(la)获得la线性地址的中间10位部分,即二级页表中对应页表项的索引下标。这样便能得到la对应的二级页表项了
return &((pte_t *)KADDR(PDE_ADDR(*pdep)))[PTX(la)];
}
分配物理内存页的功能由default_alloc_pages函数完成
/**
* 接受一个合法的正整数参数n,为其分配N个物理页面大小的连续物理内存空间.
* 并以Page指针的形式,返回最低位物理页(最前面的)。
*
* 如果分配时发生错误或者剩余空闲空间不足,则返回NULL代表分配失败
* */
static struct Page *
default_alloc_pages(size_t n) { assert(n > 0);
if (n > nr_free) {
return NULL;
}
struct Page *page = NULL;
list_entry_t *le = &free_list;
// 遍历空闲链表
while ((le = list_next(le)) != &free_list) {
// 将le节点转换为关联的Page结构
struct Page *p = le2page(le, page_link);
if (p->property >= n) {
// 发现一个满足要求的,空闲页数大于等于N的空闲块
page = p;
break;
}
}
// 如果page != null代表找到了,分配成功。反之则分配物理内存失败
if (page != NULL) {
if (page->property > n) {
// 如果空闲块的大小不是正合适(page->property != n)
// 按照指针偏移,找到按序后面第N个Page结构p
struct Page *p = page + n;
// p其空闲块个数 = 当前找到的空闲块数量 - n
p->property = page->property - n;
SetPageProperty(p);
// 按对应的物理地址顺序,将p加入到空闲链表中对应的位置
list_add_after(&(page->page_link), &(p->page_link));
}
// 在将当前page从空间链表中移除
list_del(&(page->page_link));
// 闲链表整体空闲页数量自减n
nr_free -= n;
// 清楚page的property(因为非空闲块的头Page的property都为0)
ClearPageProperty(page);
}
return page;
}
static inline void
page_remove_pte(pde_t *pgdir, uintptr_t la, pte_t *ptep) {
if (*ptep & PTE_P) {
// 如果对应的二级页表项存在
// 获得*ptep对应的Page结构
struct Page *page = pte2page(*ptep);
// 关联的page引用数自减1
if (page_ref_dec(page) == 0) {
// 如果自减1后,引用数为0,需要free释放掉该物理页
free_page(page);
}
// 清空当前二级页表项(整体设置为0)
*ptep = 0;
// 由于页表项发生了改变,需要TLB快表
tlb_invalidate(pgdir, la);
}
}
释放物理内存页的功能由default_free_pages函数完成
/**
* 释放掉自base起始的连续n个物理页,n必须为正整数
* */
static void
default_free_pages(struct Page *base, size_t n) {
assert(n > 0);
struct Page *p = base;
// 遍历这N个连续的Page页,将其相关属性设置为空闲
for (; p != base + n; p ++) {
assert(!PageReserved(p) && !PageProperty(p));
p->flags = 0;
set_page_ref(p, 0);
}
// 由于被释放了N个空闲物理页,base头Page的property设置为n
base->property = n;
SetPageProperty(base);
// 下面进行空闲链表相关操作
list_entry_t *le = list_next(&free_list);
// 迭代空闲链表中的每一个节点
while (le != &free_list) {
// 获得节点对应的Page结构
p = le2page(le, page_link);
le = list_next(le);
if (base + base->property == p) {
// 如果当前base释放了N个物理页后,尾部正好能和Page p连上,则进行两个空闲块的合并
base->property += p->property;
ClearPageProperty(p);
list_del(&(p->page_link));
}
else if (p + p->property == base) {
// 如果当前Page p能和base头连上,则进行两个空闲块的合并
p->property += base->property;
ClearPageProperty(base);
base = p;
list_del(&(p->page_link));
}
}
// 空闲链表整体空闲页数量自增n
nr_free += n;
le = list_next(&free_list);
// 迭代空闲链表中的每一个节点
while (le != &free_list) {
// 转为Page结构
p = le2page(le, page_link);
if (base + base->property <= p) {
// 进行空闲链表结构的校验,不能存在交叉覆盖的地方
assert(base + base->property != p);
break;
}
le = list_next(le);
}
// 将base加入到空闲链表之中
list_add_before(le, &(base->page_link));
}