练习零:填写已有实验

   本实验依赖实验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));
}

成功后会提示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C2kfn85b-1573649373155)(https://i.loli.net/2019/11/13/8mOcTNiKjY2IFk5.jpg)]
然后会在另一个地方断言挂掉,那是接下来的实验了。

Question 1
你的first fit算法是否有进一步的改进空间

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

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

  通过设置页表和对应的页表项,可建立虚拟内存地址和物理内存地址的对应关系。其中的get_pte函数是设置页表项环节中的一个重要步骤。此函数找到一个虚地址对应的二级页表项的内核虚地址,如果此二级页表项不存在,则分配一个包含此项的二级页表。本练习需要补全get_pte函数 in kern/mm/pmm.c,实现其功能。请仔细查看和理解get_pte函数中的注释。get_pte函数的调用关系图如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nENSEoK9-1573649373156)(https://objectkuan.gitbooks.io/ucore-docs/lab2_figs/image001.png)]

  具体是在 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 详解
从低到高,分别是:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rSIAylWW-1573649373157)(https://i.loli.net/2019/11/13/gvFEYamA7KkLu5C.png)]
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函数的调用关系图如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N5SADfLZ-1573649373159)(https://objectkuan.gitbooks.io/ucore-docs/lab2_figs/image002.png)]

  之前断言失败是因为在 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/操作系统-uCore-Lab-2/
[2].https://xr1s.me/2018/05/27/ucore-lab2-report/