练习零:填写已有实验
本实验依赖实验1。请把你做的实验1的代码填入本实验中代码中有“LAB1”的注释相应部分。提示:可采用diff和patch工具进行半自动的合并(merge),也可用一些图形化的比较/merge工具来手动合并,比如meld,eclipse中的diff/merge工具,understand中的diff/merge工具等。
其实 lab1 中只有 kern/debug/kdebug.c kern/init/init.c kern/trap/trap.c 被改过了 所以只用把这三个文件复制到lab2中就可以了。
练习一:实现 first-fit 连续物理内存分配算法(需要编程)
在实现first fit 内存分配算法的回收函数时,要考虑地址连续的空闲块之间的合并操作。提示:在建立空闲页块链表时,需要按照空闲页块起始地址来排序,形成一个有序的链表。可能会修改default_pmm.c中的default_init,default_init_memmap,default_alloc_pages, default_free_pages等相关函数。请仔细查看和理解default_pmm.c中的注释。
查看注释后,发现代码已经写了,于是make qemu 了一发,发现错了,断言失败。仔细看了后,代码是不完整的。
断言失败之前的检查都是针对页分配成功、失败的测试,从失败的断言这里开始是对分配的页的位置进行的检查。
实际上,最初的实现 default_*_pages 只是简单的插入到链表,根本没有关心被插入到了哪里,所以在多次 alloc/free 之后页块在链表中的位置乱得一塌糊涂。如果需要保序,需要对每个改动链表的地方注意一下位置。
struct Page { // 页帧的 引用计数 int ref; // 页帧的状态 Reserve 表示是否被内核保留 另一个是 表示是否 可分配 uint32_t flags; // 记录连续空闲页块的数量 只在第一块进行设置 unsigned int property; // 用于将所有的页帧串在一个双向链表中 这个地方很有趣 直接将 Page 这个结构体加入链表中会有点浪费空间 因此在 Page 中设置一个链表的结点 将其结点加入到链表中 还原的方法是将 链表中的 page_link 的地址 减去它所在的结构体中的偏移 就得到了 Page 的起始地址 list_entry_t page_link; }; // 初始化空闲页块链表 static void default_init(void) { list_init(&free_list); nr_free = 0; // 空闲页块一开始是0个 } // 初始化n个空闲页块 static void default_init_memmap(struct Page *base, size_t n) { assert(n > 0); struct Page *p = base; for (; p != base + n; p ++) { assert(PageReserved(p)); // 看看这个页是不是被内核保留的 p->flags = p->property = 0; set_page_ref(p, 0); } base->property = n; // 头一个空闲页块 要设置数量 SetPageProperty(base); nr_free += n; // 初始化玩每个空闲页后 将其要插入到链表每次都插入到节点前面 因为是按地址排序 list_add_before(&free_list, &(base->page_link)); } // 分配n个页块 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; // 查找 n 个或以上 空闲页块 若找到 则判断是否大过 n 则将其拆分 并将拆分后的剩下的空闲页块加回到链表中 while ((le = list_next(le)) != &free_list) { // 此处 le2page 就是将 le 的地址 - page_link 在 Page 的偏移 从而找到 Page 的地址 struct Page *p = le2page(le, page_link); if (p->property >= n) { page = p; break; } } if (page != NULL) { if (page->property > n) { struct Page *p = page + n; p->property = page->property - n; SetPageProperty(p); // 将多出来的插入到 被分配掉的页块 后面 list_add(&(page->page_link), &(p->page_link)); } // 最后在空闲页链表中删除掉原来的空闲页 list_del(&(page->page_link)); nr_free -= n; ClearPageProperty(page); } return page; } // 释放掉 n 个 页块 static void default_free_pages(struct Page *base, size_t n) { assert(n > 0); struct Page *p = base; for (; p != base + n; p ++) { assert(!PageReserved(p) && !PageProperty(p)); p->flags = 0; set_page_ref(p, 0); } base->property = n; SetPageProperty(base); list_entry_t *le = list_next(&free_list); // 合并到合适的页块中 while (le != &free_list) { p = le2page(le, page_link); le = list_next(le); if (base + base->property == p) { base->property += p->property; ClearPageProperty(p); list_del(&(p->page_link)); } else if (p + p->property == base) { p->property += base->property; ClearPageProperty(base); base = p; list_del(&(p->page_link)); } } nr_free += n; le = list_next(&free_list); // 将合并好的合适的页块添加回空闲页块链表 while (le != &free_list) { p = le2page(le, page_link); if (base + base->property <= p) { break; } le = list_next(le); } list_add_before(le, &(base->page_link)); }
成功后会提示:
然后会在另一个地方断言挂掉,那是接下来的实验了。
Question 1
你的first fit算法是否有进一步的改进空间
当然啦,这种东西(对于一个区间中某些数字的存在性维护)用线段树可以实现 alloc 和 free 达到 O(log n) 级别的时间复杂度,不过空间(复杂度虽然还是 O(n) 不变)要增加一倍。
练习二:实现寻找虚拟地址对应的页表项(需要编程)
通过设置页表和对应的页表项,可建立虚拟内存地址和物理内存地址的对应关系。其中的get_pte函数是设置页表项环节中的一个重要步骤。此函数找到一个虚地址对应的二级页表项的内核虚地址,如果此二级页表项不存在,则分配一个包含此项的二级页表。本练习需要补全get_pte函数 in kern/mm/pmm.c,实现其功能。请仔细查看和理解get_pte函数中的注释。get_pte函数的调用关系图如下所示:
具体是在 check_pgdir 函数中一个断言 get_pte 返回值失败的,看到 get_pte 函数,就是第二个练习需要编写修改的地方。
get_pte ,是给出 Page Directory 基址和物理地址,求对应的 Page Table Entry 地址。
pte_t *get_pte(pde_t *pgdir, uintptr_t la, bool create) { pde_t *pdep = &pgdir[PDX(la)]; // 找到 PDE 这里的 pgdir 可以看做是 页目录表的基址 if (!(*pdep & PTE_P)) { // 看看 PDE 指向的页表 是否存在 struct Page* page = alloc_page(); // 不存在就申请一页物理页 /* 通过 default_alloc_pages() 分配的页 的地址 并不是真正的页分配的地址 实际上只是 Page 这个结构体所在的地址而已 故而需要 通过使用 page2pa() 将 Page 这个结构体 的地址 转换成真正的物理页地址的线性地址 然后需要注意的是 无论是 * 或是 memset 都是对虚拟地址进行操作的 所以需要将 真正的物理页地址再转换成 内核虚拟地址 */ if (!create || page == NULL) { return NULL; } set_page_ref(page, 1); uintptr_t pa = page2pa(page); memset(KADDR(pa), 0, PGSIZE); // 将这一页清空 此时将 线性地址转换为内核虚拟地址 *pdep = pa | PTE_U | PTE_W | PTE_P; // 设置 PDE 权限 } return &((pte_t *)KADDR(PDE_ADDR(*pdep)))[PTX(la)]; }
首先注释中的 pdep ( Page Directory Entry Pointer ),就是 PD 基质加上 PDE 编号,编号就是 LA 的高十位( x86 的约定),可以通过 PDX 宏获取。
然后检查是否设置了 Present 位,也就是 PDE_P 位。不过实际上并没有 PDE_P 这个宏,使用注释里告诉我们的等价的 PTE_P 来检查(注释里告诉我们这个位是 PDE 和 PTE 通用的,虽然这三个位的确是通用的,不过毕竟 PD 和 PT 的保留位还是有所不同的)。
如果没有设置而且 bool create 设置为需要申请,就需要按注释 (3) ~ (7) 中提示一般的 alloc 一个页、设置引用计数、获得线性地址、使用 memset 清空页内容、设置 PDE 项中权限位。
最后再从 la 中获取一下中间十位,也就是 PTE 编号,从 PD 获取一下基址,相加就是 PTE 的线性地址了,用 KADDR 处理一下即可。
请回答如下问题:
请描述页目录项(Pag Director Entry)和页表(Page Table Entry)中每个组成部分的含义和以及对ucore而言的潜在用处。
PDE 详解
从低到高,分别是:
P (Present) 位:表示该页保存在物理内存中。
R (Read/Write) 位:表示该页可读可写。
U (User) 位:表示该页可以被任何权限用户访问。
W (Write Through) 位:表示 CPU 可以直写回内存。
D (Cache Disable) 位:表示不需要被 CPU 缓存。
A (Access) 位:表示该页被写过。
S (Size) 位:表示一个页 4MB 。
9-11 位保留给 OS 使用。
12-31 位指明 PTE 基质地址。
PTE 详解
从低到高,分别是:
0-3 位同 PDE。
C (Cache Disable) 位:同 PDE D 位。
A (Access) 位:同 PDE 。
D (Dirty) 位:表示该页被写过。
G (Global) 位:表示在 CR3 寄存器更新时无需刷新 TLB 中关于该页的地址。
9-11 位保留给 OS 使用。
12-31 位指明物理页基址。
如果ucore执行过程中访问内存,出现了页访问异常,请问硬件要做哪些事情?
进行换页操作 首先 CPU 将产生页访问异常的线性地址 放到 cr2 寄存器中 ;然后就是和普通的中断一样 保护现场 将寄存器的值压入栈中 ;然后压入 error_code 中断服务例程将外存的数据换到内存中来 ;最后 退出中断回到进入中断前的状态。
练习3:释放某虚地址所在的页并取消对应二级页表项的映射
当释放一个包含某虚地址的物理内存页时,需要让对应此物理内存页的管理数据结构Page做相关的清除处理,使得此物理内存页成为空闲;另外还需把表示虚地址与物理地址对应关系的二级页表项清除。请仔细查看和理解page_remove_pte函数中的注释。为此,需要补全在 kern/mm/pmm.c中的page_remove_pte函数。page_remove_pte函数的调用关系图如下所示:
之前断言失败是因为在 page_insert 中调用了 page_remove_pte ,而 page_remove_pte 并没有修改引用计数所以导致了对于引用计数的断言失败。
static inline void page_remove_pte(pde_t *pgdir, uintptr_t la, pte_t *ptep) { if ((*ptep & PTE_P)) { struct Page *page = pte2page(*ptep); if (page_ref_dec(page) == 0) { // 若引用计数减一后为0 则释放该物理页 free_page(page); } *ptep = 0; // 清空 PTE tlb_invalidate(pgdir, la); // 刷新 tlb } }
首先判断一下 ptep 是不是合法——检查一下 Present 位就是了;然后通过注释中所说的,通过 pte2page(*ptep) 获取相应页,减少引用计数并决定是否释放页;最后把 TLB 中该页的缓存刷掉就可以了。
数据结构Page的全局变量(其实是一个数组)的每一项与页表中的页目录项和页表项有无对应关系?如果有,其对应关系是啥?
可以参照 kern/mm/pmm.h 中的转换函数。其实就是 Page 全局数组中以 Page Directory Index 和 Page Table Index 的组合 PPN (Physical Page Number) 为索引的那一项。
剩下的不会;
参考链接:
[1].https://yuerer.com/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F-uCore-Lab-2/
[2].https://xr1s.me/2018/05/27/ucore-lab2-report/