练习零:填写已有实验

其实 lab1 中只有 kern/debug/kdebug.c kern/init/init.c kern/trap/trap.c 被改过了
所以只用把这三个文件复制到lab2中就可以了。

练习一:实现 first-fit 连续物理内存分配算法(需要编程)

  查看注释后,发现代码已经写了,于是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));
}

成功后会提示:

OS课程 ucore_lab2实验报告-LMLPHP

然后会在另一个地方断言挂掉,那是接下来的实验了。

当然啦,这种东西(对于一个区间中某些数字的存在性维护)用线段树可以实现 alloc 和 free 达到 O(log n) 级别的时间复杂度,不过空间(复杂度虽然还是 O(n) 不变)要增加一倍。

练习二:实现寻找虚拟地址对应的页表项(需要编程)

  具体是在 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 处理一下即可。

PDE 详解

从低到高,分别是:

OS课程 ucore_lab2实验报告-LMLPHP

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 详解

OS课程 ucore_lab2实验报告-LMLPHP

从低到高,分别是:

0-3 位同 PDE。

C (Cache Disable) 位:同 PDE D 位。

A (Access) 位:同 PDE 。

D (Dirty) 位:表示该页被写过。

G (Global) 位:表示在 CR3 寄存器更新时无需刷新 TLB 中关于该页的地址。

9-11 位保留给 OS 使用。

12-31 位指明物理页基址。

  进行换页操作 首先 CPU 将产生页访问异常的线性地址 放到 cr2 寄存器中 ;然后就是和普通的中断一样 保护现场 将寄存器的值压入栈中 ;然后压入 error_code 中断服务例程将外存的数据换到内存中来 ;最后 退出中断回到进入中断前的状态。

练习3:释放某虚地址所在的页并取消对应二级页表项的映射

  之前断言失败是因为在 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 中该页的缓存刷掉就可以了。

  可以参照 kern/mm/pmm.h 中的转换函数。其实就是 Page 全局数组中以 Page Directory Index 和 Page Table Index 的组合 PPN (Physical Page Number) 为索引的那一项。

剩下的不会;

参考链接:

[1].https://yuerer.com/操作系统-uCore-Lab-2/

[2].https://xr1s.me/2018/05/27/ucore-lab2-report/

05-11 11:27