uCoreOs lab2 实验报告

实验目的

  • 理解基于段页式内存地址的转换机制
  • 理解页表的建立和使用方法
  • 理解物理内存的管理方法

实验内容

本次实验包含三个部分。首先了解如何发现系统中的物理内存;然后了解如何建立对物理内存的初步管理,即了解连续物理内存管理;最后了解页表相关的操作,即如何建立页表来实现虚拟内存到物理内存之间的映射,对段页式内存管理机制有一个比较全面的了解。本实验里面实现的内存管理还是非常基本的,并没有涉及到对实际机器的优化,比如针对 cache 的优化等。如果大家有余力,尝试完成扩展练习。

练习介绍

为了实现lab2的目标,lab2提供了3个基本练习和2个扩展练习,要求完成实验报告。

对实验报告的要求:

  • 基于markdown格式来完成,以文本方式为主
  • 填写各个基本练习中要求完成的报告内容
  • 完成实验后,请分析ucore_lab中提供的参考答案,并请在实验报告中说明你的实现与参考答案的区别
  • 列出你认为本实验中重要的知识点,以及与对应的OS原理中的知识点,并简要说明你对二者的含义,关系,差异等方面的理解(也可能出现实验中的知识点没有对应的原理知识点)
  • 列出你认为OS原理中很重要,但在实验中没有对应上的知识点

查看本次实验所有需要填写代码的地方

1
2
3
4
5
6
7
[~/Desktop/ucore_lab/labcodes/lab2_new]
moocos-> grep -rn "LAB2 EXERCISE" *
Binary file kern/mm/.default_pmm.c.swp matches
Binary file kern/mm/.default_pmm.c.swo matches
kern/mm/default_pmm.c:12:// LAB2 EXERCISE 1: YOUR CODE
kern/mm/pmm.c:350: /* LAB2 EXERCISE 2: YOUR CODE
kern/mm/pmm.c:403: /* LAB2 EXERCISE 3: YOUR CODE

练习0:填写已有实验

本实验依赖实验1。请把你做的实验1的代码填入本实验中代码中有“LAB1”的注释相应部分。提示:可采用diff和patch工具进行半自动的合并(merge),也可用一些图形化的比较/merge工具来手动合并,比如meld,eclipse中的diff/merge工具,understand中的diff/merge工具等。

命令行键入meld

设置lab1,lab2两个文件夹

image-20200326141422240

比较合并

其实只要将lab1的 kern/debug/kdebug.c , kern/init/init.c, kern/trap/trap.c复制到lab2即可

练习1:实现 first-fit 连续物理内存分配算法

在实现first fit 内存分配算法的回收函数时,要考虑地址连续的空闲块之间的合并操作。提示:在建立空闲页块链表时,需要按照空闲页块起始地址来排序,形成一个有序的链表。可能会修改default_pmm.c中的default_init,default_init_memmap,default_alloc_pages, default_free_pages等相关函数。请仔细查看和理解default_pmm.c中的注释。

请在实验报告中简要说明你的设计实现过程。请回答如下问题:

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

先直接运行,出现如下错误

image-20200327025022905

错误是在

1
assert((p0 = alloc_page()) == p2 - 1);

最先匹配算法

image-20200326193613549 image-20200326193625241

观察default_pmm.c文件头

1
2
3
4
#include <pmm.h>
#include <list.h>
#include <string.h>
#include <default_pmm.h>

查看./pmm.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#ifndef __KERN_MM_PMM_H__
#define __KERN_MM_PMM_H__

#include <defs.h>
#include <mmu.h>
#include <memlayout.h>
#include <atomic.h>
#include <assert.h>

// pmm_manager is a physical memory management class. A special pmm manager - XXX_pmm_manager
// only needs to implement the methods in pmm_manager class, then XXX_pmm_manager can be used
// by ucore to manage the total physical memory space.
struct pmm_manager {
const char *name; // XXX_pmm_manager's name
void (*init)(void); // initialize internal description&management data structure
// (free block list, number of free block) of XXX_pmm_manager
void (*init_memmap)(struct Page *base, size_t n); // setup description&management data structcure according to
// the initial free physical memory space
struct Page *(*alloc_pages)(size_t n); // allocate >=n pages, depend on the allocation algorithm
void (*free_pages)(struct Page *base, size_t n); // free >=n pages with "base" addr of Page descriptor structures(memlayout.h)
size_t (*nr_free_pages)(void); // return the number of free pages
void (*check)(void); // check the correctness of XXX_pmm_manager
};

pmm_manager是内存分配结构体

包含6个函数指针(的声明)

包含初始化函数init,分配函数alloc_pages,释放函数free_pages,检查函数check。

其定义在./default_pmm.c的末尾

1
2
3
4
5
6
7
8
9
const struct pmm_manager default_pmm_manager = {
.name = "default_pmm_manager",
.init = default_init,
.init_memmap = default_init_memmap,
.alloc_pages = default_alloc_pages,
.free_pages = default_free_pages,
.nr_free_pages = default_nr_free_pages,
.check = default_check,
};

因此可知在default_pmm.c中要完成对连续内存分配中使用到的各个函数的定义。

引用了库文件memlayout.h

查看./memlayout.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* *
* struct Page - Page descriptor structures. Each Page describes one
* physical page. In kern/mm/pmm.h, you can find lots of useful functions
* that convert Page to other data types, such as phyical address.
* */
struct Page {
int ref; // page frame's reference counter
uint32_t flags; // array of flags that describe the status of the page frame
unsigned int property; // the num of free block, used in first fit pm manager
list_entry_t page_link; // free list link
};
/* Flags describing the status of a page frame */
#define PG_reserved 0 // if this bit=1: the Page is reserved for kernel, cannot be used in alloc/free_pages; otherwise, this bit=0
#define PG_property 1 // if this bit=1: the Page is the head page of a free memory block(contains some continuous_addrress pages), and can be used in alloc_pages; if this bit=0: if the Page is the the head page of a free memory block, then this Page and the memory block is alloced. Or this Page isn't the head page.
#define SetPageReserved(page) set_bit(PG_reserved, &((page)->flags))
#define ClearPageReserved(page) clear_bit(PG_reserved, &((page)->flags))
#define PageReserved(page) test_bit(PG_reserved, &((page)->flags))
#define SetPageProperty(page) set_bit(PG_property, &((page)->flags))
#define ClearPageProperty(page) clear_bit(PG_property, &((page)->flags))
#define PageProperty(page) test_bit(PG_property, &((page)->flags))

// convert list entry to page
#define le2page(le, member) \
to_struct((le), struct Page, member)

页面page定义

  • ref
    表示这样页被页表的引用记数,应该就是映射此物理页的虚拟页个数。一旦某页表中有一个页表项设置了虚拟页到这个Page管理的物理页的映射关系,就会把Page的ref加一。反之,若是解除,那就减一。
  • flags
    表示此物理页的状态标记,有两个标志位,第一个表示是否被保留,如果被保留了则设为1(比如内核代码占用的空间)。第二个表示此页是否是free的。如果设置为1,表示这页是free的,可以被分配;如果设置为0,表示这页已经被分配出去了,不能被再二次分配。
  • property
    用来记录某连续内存空闲块的大小,这里需要注意的是用到此成员变量的这个Page一定是连续内存块的开始地址(第一页的地址)。
  • page_link
    是便于把多个连续内存空闲块链接在一起的双向链表指针,连续内存空闲块利用这个页的成员变量page_link来链接比它地址小和大的其他连续内存空闲块,page_link所用的空间实际就是页面空间

而页面实际大小为结构体所占的空间大小

有7个宏定义函数

  • SetPageReserved(page) 设置PG_reserved=1,标识的内存是被内核使用的,不能释放
  • SetPageProperty(page) 设置PG_property=1,表示的可分配的意思,0则表示已被使用
  • le2page(le, member) 获得以le为地址起点,member为数据(结构)类型所占用字节的的变量。一个地址可能有不同大小的数据类型存在,一个结构体及其内部第一个成员及其嵌套成员的起始地址是相同的

查看libs/list.h

包含以下自定义数据结构链表和链表操作函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct list_entry {
struct list_entry *prev, *next;
};

typedef struct list_entry list_entry_t;

static inline void list_init(list_entry_t *elm) __attribute__((always_inline));
static inline void list_add(list_entry_t *listelm, list_entry_t *elm) __attribute__((always_inline));
static inline void list_add_before(list_entry_t *listelm, list_entry_t *elm) __attribute__((always_inline));
static inline void list_add_after(list_entry_t *listelm, list_entry_t *elm) __attribute__((always_inline));
static inline void list_del(list_entry_t *listelm) __attribute__((always_inline));
static inline void list_del_init(list_entry_t *listelm) __attribute__((always_inline));
static inline bool list_empty(list_entry_t *list) __attribute__((always_inline));
static inline list_entry_t *list_next(list_entry_t *listelm) __attribute__((always_inline));
static inline list_entry_t *list_prev(list_entry_t *listelm) __attribute__((always_inline));

static inline void __list_add(list_entry_t *elm, list_entry_t *prev, list_entry_t *next) __attribute__((always_inline));
static inline void __list_del(list_entry_t *prev, list_entry_t *next) __attribute__((always_inline));

练习1实现代码

主要修改代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
 #define free_list (free_area.free_list)
#define nr_free (free_area.nr_free)

static void
default_init(void) { //初始化页面分配管理
list_init(&free_list); //初始化空闲页面列表
nr_free = 0; //初始化空闲页面个数为0
}
//下面这个函数根据现有的内存情况构建空闲块列表的初始状态
//以base为基址,选择n个连续页面
static void
default_init_memmap(struct Page *base, size_t n) {
assert(n > 0);
struct Page *p = base;
for (; p != base + n; p ++) {//找到n个页面,并初始化页面各项参数
assert(PageReserved(p));
p->flags = p->property = 0;
set_page_ref(p, 0);
}
base->property = n;
SetPageProperty(base);
nr_free += n;
// 将生成的连续n页空闲空间加入空闲列表
list_add(&free_list, &(base->page_link));
}
//用最先匹配算法分配大小为n页的空间
static struct Page *
default_alloc_pages(size_t n) {
//判断空闲地址空间是否大于所需空间
assert(n > 0);
if (n > nr_free) {
return NULL;
}
//从free_list开始,遍历链表,直到找到第一块不小于所需空间大小(n)的内存块,赋值给page
struct Page *page = NULL;
list_entry_t *le = &free_list; //le是空闲块链表头指针
// 查找n个或以上空闲页块,若找到,则判断是否大过n,则将其拆分,并将拆分后的剩下的空闲页块加回到链表中
while ((le = list_next(le)) != &free_list) { //从第一个节点开始遍历
// 此处 le2page 就是将 le 的地址 - page_link 在 Page 的偏移 从而找到 Page 的地址
struct Page *p = le2page(le, page_link); //获取节点所在基于Page数据结构的变量
if (p->property >= n) {
page = p;
break;
}
}
//如果可以找到长度大于等于n页的块,
//从链表中删除此内存块
//如果长度大于n,分配连续的n页,修改标志位,如果有剩余的小的内存块,重新插入链表
if (page != NULL) {
//此时空间分布如下
//(&page) (&(p=page+n))
//|<-----------------------page---------------------->|
//|<---------------------property-------------------->|
//|<------------n------------->|<----(property-n)---->|
//
//节点添加思路
//原来 :pre_link<-->page_link
//添加p :pre_link<-->page_link<-->p
//删除page:pre_link<-->p
//将list_del(&(page->page_link))后置是因为删除了该节点便会在后续添加p中无法找到前继pre_link
//亦可用新建一个变量先保存pre_link地址,但这会耗费额外的空间
- list_del(&(page->page_link));
if (page->property > n) {
struct Page *p = page + n;
p->property = page->property - n;
+ SetPageProperty(p);//设置其为未分配的空闲空间的起始地址
// 将多出来的插入到被分配掉的页块后面
- list_add(&free_list, &(p->page_link));
+ list_add(&(page->page_link), &(p->page_link));//连接可分配的空闲区域
}
// 最后在空闲页链表中删除掉原来的空闲页
+ list_del(&(page->page_link));
nr_free -= n;
ClearPageProperty(page);
}
return page;
}
//下面函数负责已分配内存的释放,基址为base,大小为n页
static void
default_free_pages(struct Page *base, size_t n) {
assert(n > 0);//断言n>0
struct Page *p = base;
for (; p != base + n; p ++) {
//这两个变量主要就是用来确保页空间能符合释放条件。
//第一个PG_reserved为1标识的内存是被内核使用的,不能释放;
//第二个PG_property表示的可分配的意思,如果为1表示以这个页开始存在一片空闲页,
//这种情况也无需free操作,而且如果存在这种情况说明这个待释放的空间其实是有问题的。
//所以只有这两个标志位都为0,才能释放这个页。
assert(!PageReserved(p) && !PageProperty(p));
p->flags = 0;
set_page_ref(p, 0);//页面引用次数置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;//空闲页面数+n
// 将合并好的合适的页块添加回空闲页块链表
// 找到适合位置时,le刚好在base的后面
+ 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(&free_list, &(base->page_link));
+ list_add_before(le, &(base->page_link));
//也可以用list_add(list_prev(le),&(base->page_link));
}

运行结果

image-20200327035458593

可以看到分配函数alloc_page()测试成功,且出现了新的错误,需要等到练习2解决

Q:你的first fit算法是否有进一步的改进空间

可以看出,page_link数据结构用的是线性结构链表,程序运行时间主要花在查找方面(循环查找符合条件的节点),算法时间复杂度为O(n)O(n),改进方面可以使用非线性结构(例如线段树)来存储页面空间,提高查找效率,使用二分查找的话可以使时间复杂度降至 O(log2n)O(log_{2}n)

练习2:实现寻找虚拟地址对应的页表项

通过设置页表和对应的页表项,可建立虚拟内存地址和物理内存地址的对应关系。其中的get_pte函数是设置页表项环节中的一个重要步骤。此函数找到一个虚地址对应的二级页表项的内核虚地址,如果此二级页表项不存在,则分配一个包含此项的二级页表。本练习需要补全get_pte函数 in kern/mm/pmm.c,实现其功能。请仔细查看和理解get_pte函数中的注释。get_pte函数的调用关系图如下所示:

img 图1 get_pte函数的调用关系图

请在实验报告中简要说明你的设计实现过程。请回答如下问题:

  • 请描述页目录项(Page Directory Entry)和页表项(Page Table Entry)中每个组成部分的含义以及对ucore而言的潜在用处。
  • 如果ucore执行过程中访问内存,出现了页访问异常,请问硬件要做哪些事情?

段页式机制

image-20200327230020872
名称 描述
页目录索引表(PDT) 一级索引
页表索引表(PTT) 二级索引
页表项(PTE) 页表项 PDT(1024项PDE),PTT(1024项PTE)
  • 虚拟地址映射关系:

    1
    虚拟 = 物理 + 0xC0000000

pmm.h中有下列的宏定义函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//分配1页空间
#define alloc_page() alloc_pages(1)
//释放1页空间
#define free_page(page) free_pages(page, 1)
/* *
* PADDR - takes a kernel virtual address (an address that points above KERNBASE),
* where the machine's maximum 256MB of physical memory is mapped and returns the
* corresponding physical address. It panics if you pass it a non-kernel virtual address.
* */
//将内核虚地址转化为物理地址,若非内核虚地址会报错
#define PADDR(kva) ({ \
uintptr_t __m_kva = (uintptr_t)(kva); \
if (__m_kva < KERNBASE) { \
panic("PADDR called with invalid kva %08lx", __m_kva); \
} \
__m_kva - KERNBASE; \
})

/* *
* KADDR - takes a physical address and returns the corresponding kernel virtual
* address. It panics if you pass an invalid physical address.
* */
//将物理地址转化为内核虚地址,若非物理地址会报错,对la所在的二级页表的物理地址进行虚拟化
#define KADDR(pa) ({ \
uintptr_t __m_pa = (pa); \
size_t __m_ppn = PPN(__m_pa); \
if (__m_ppn >= npage) { \
panic("KADDR called with invalid pa %08lx", __m_pa); \
} \
(void *) (__m_pa + KERNBASE); \
})
//对页表项进行4K对齐(截断低12位,低十二位变为0,高位不变)
#define PTE_ADDR(pte) ((uintptr_t)(pte) & ~0xFFF)
//对页目录项进行4K对齐(截断低12位)
#define PDE_ADDR(pde) PTE_ADDR(pde)
1
2
3
4
#define PDXSHIFT        22                      // offset of PDX in a linear address
#define PTXSHIFT 12 // offset of PTX in a linear address
#define PDX(la) ((((uintptr_t)(la)) >> PDXSHIFT) & 0x3FF) //一级页目录表的下标
#define PTX(la) ((((uintptr_t)(la)) >> PTXSHIFT) & 0x3FF) //

get_pte函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//get_pte - get pte and return the kernel virtual address of this pte for la
// - if the PT contians this pte didn't exist, alloc a page for PT
// parameter:
// pgdir: the kernel virtual base address of PDT
// la: the linear address need to map
// create: a logical value to decide if alloc a page for PT
// return vaule: the kernel virtual address of this pte
pte_t *
get_pte(pde_t *pgdir, uintptr_t la, bool create) {
/* LAB2 EXERCISE 2: YOUR CODE
*
* If you need to visit a physical address, please use KADDR()
* please read pmm.h for useful macros
*
* Maybe you want help comment, BELOW comments can help you finish the code
*
* Some Useful MACROs and DEFINEs, you can use them in below implementation.
* MACROs or Functions:
* PDX(la) = the index of page directory entry of VIRTUAL ADDRESS la.
* KADDR(pa) : takes a physical address and returns the corresponding kernel virtual address.
* set_page_ref(page,1) : means the page be referenced by one time
* page2pa(page): get the physical address of memory which this (struct Page *) page manages
* struct Page * alloc_page() : allocation a page
* memset(void *s, char c, size_t n) : sets the first n bytes of the memory area pointed by s
* to the specified value c.
* DEFINEs:
* PTE_P 0x001 // page table/directory entry flags bit : Present
* PTE_W 0x002 // page table/directory entry flags bit : Writeable
* PTE_U 0x004 // page table/directory entry flags bit : User can access
*/
#if 0
pde_t *pdep = NULL; // (1) find page directory entry
if (0) { // (2) check if entry is not present
// (3) check if creating is needed, then alloc page for page table
// CAUTION: this page is used for page table, not for common data page
// (4) set page reference
uintptr_t pa = 0; // (5) get linear address of page
// (6) clear page content using memset
// (7) set page directory entry's permission
}
return NULL; // (8) return page table entry
#endif
}

pgdir: PDT(页目录索引表)的内核虚地址
la: 需要映射的线性地址
create: 一个决定是否要分配一个页面给PT(页表)的逻辑值

按照注释提示在#endif后添加如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
   pde_t *pdep = &pgdir[PDX(la)]; // (1)获取页目录项,*pdep是la所在的二级页表的物理地址,PDX(la)一级页目录表的下标,此运算其实是pgdir基址+PDX(la)偏移量,由在一级页目录表中查找得到二级页表的地址
// 找到PDE这里的pgdir可以看做是页目录表的基址
if (!(*pdep & PTE_P)) { // (2)假设页目录项不存在
struct Page* page = alloc_page();
/* 通过 default_alloc_pages()分配的页的地址并不是真正的页分配的地址,
实际上只是Page这个结构体所在的地址而已,因而需要通过使用page2pa()将Page这个结构体的地址
转换成真正的物理页地址的线性地址,并且需要注意的是无论是*或是memset都是对虚拟地址进行操作的,
所以需要将真正的物理页地址再转换成内核虚拟地址
*/
if (!create || page == NULL) { return NULL; } //假如不需要分配或是分配失败
set_page_ref(page, 1); // (4)设置被引用1次
uintptr_t pa = page2pa(page); // (5)得到该页物理地址
memset(KADDR(pa), 0, PGSIZE); // (6)物理地址转虚拟地址,并初始化
// 将这一页清空此时将线性地址转换为内核虚拟地址
*pdep = pa | PTE_U | PTE_W | PTE_P; // (7)设置PDE权限,设置可读,可写,有效
}
return &((pte_t *)KADDR(PDE_ADDR(*pdep)))[PTX(la)];
//由于此时*pdep是查到的页目录项,其实也就是la所在的二级页表的物理地址,用PDE_ADDR对其后12位进行截断后使用KADDR进行虚化,得到PTT(页表索引表)的虚地址,用PTX(la)截取中间0x3ff(10位)的二级页表的下标,两者结合得到所寻的页表项PTE

make clean后重新make clean,得到如下结果

image-20200327134230400

可以发现get_pte的错误消失,显示了新的错误,需在练习3解决

请描述页目录项(Page Directory 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执行过程中访问内存,出现了页访问异常,请问硬件要做哪些事情?

使能页机制

image-20200328093548814

Cr0系列的控制寄存器:

寄存器名称 描述
CR0 包含处理器标志控制位,如PE,PG,WP等
CR1 保留
CR2 专门用于保存缺页异常时的线性地址
CR3 保存进程页目录地址
CR4 扩展功能(如判断物理地址扩展模式等),Pentium系列(包括486的后期版本)处理器中才实现

如果出现了页访问异常,那么硬件将引发页访问异常的地址将被保存在cr2寄存器中,设置错误代码,然后触发Page Fault异常。

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

当释放一个包含某虚地址的物理内存页时,需要让对应此物理内存页的管理数据结构Page做相关的清除处理,使得此物理内存页成为空闲;另外还需把表示虚地址与物理地址对应关系的二级页表项清除。请仔细查看和理解page_remove_pte函数中的注释。为此,需要补全在 kern/mm/pmm.c中的page_remove_pte函数。page_remove_pte函数的调用关系图如下所示:

img

图2 page_remove_pte函数的调用关系图

请在实验报告中简要说明你的设计实现过程。请回答如下问题:

  • 数据结构Page的全局变量(其实是一个数组)的每一项与页表中的页目录项和页表项有无对应关系?如果有,其对应关系是啥?
  • 如果希望虚拟地址与物理地址相等,则需要如何修改lab2,完成此事? 鼓励通过编程来具体完成这个问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//page_remove_pte - free an Page sturct which is related linear address la
// - and clean(invalidate) pte which is related linear address la
//note: PT is changed, so the TLB need to be invalidate
static inline void
page_remove_pte(pde_t *pgdir, uintptr_t la, pte_t *ptep) {
/* LAB2 EXERCISE 3: YOUR CODE
*
* Please check if ptep is valid, and tlb must be manually updated if mapping is updated
*
* Maybe you want help comment, BELOW comments can help you finish the code
*
* Some Useful MACROs and DEFINEs, you can use them in below implementation.
* MACROs or Functions:
* struct Page *page pte2page(*ptep): get the according page from the value of a ptep
* free_page : free a page
* page_ref_dec(page) : decrease page->ref. NOTICE: ff page->ref == 0 , then this page should be free.
* tlb_invalidate(pde_t *pgdir, uintptr_t la) : Invalidate a TLB entry, but only if the page tables being
* edited are the ones currently in use by the processor.
* DEFINEs:
* PTE_P 0x001 // page table/directory entry flags bit : Present
*/
#if 0
if (0) { //(1) check if page directory is present
struct Page *page = NULL; //(2) find corresponding page to pte
//(3) decrease page reference
//(4) and free this page when page reference reachs 0
//(5) clear second page table entry
//(6) flush tlb
}
#endif
}

PTE 详解

pmm.h部分函数定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typedef uintptr_t pte_t;
//页表/目录条目标志位:P (Present) 位:表示该页保存在物理内存中。
#define PTE_P 0x001 // Present
//释放1页空间
#define free_page(page) free_pages(page, 1)
//从ptep的值获取相应的页面
static inline struct Page *
pte2page(pte_t pte) {
if (!(pte & PTE_P)) {
panic("pte2page called with invalid pte");
}
return pa2page(PTE_ADDR(pte));
}
//页面引用计数减1
static inline int
page_ref_dec(struct Page *page) {
page->ref -= 1;
return page->ref;
}

按照注释在#endif后添加如下代码:

1
2
3
4
5
6
7
8
if ((*ptep & PTE_P)) {//判断页表入口是否存在,P (Present) 位:表示该页保存在物理内存中。
struct Page *page = pte2page(*ptep);//从ptep的值获取相应的页面
if (page_ref_dec(page) == 0) { // 若引用计数减一后为0 则释放该物理页
free_page(page);
}
*ptep = 0; // 清空 PTE
tlb_invalidate(pgdir, la); // 刷新 tlb
}

make clean 后重新make qemu得到如下结果

image-20200327144600830

另外,对于lab1的challenge1的代码,似乎不能在lab2正常运行

当键入0时,会出现缺页异常

image-20200327193848787

遂将其注释便运行正常

image-20200327193749004

数据结构Page的全局变量(其实是一个数组)的每一项与页表中的页目录项和页表项有无对应关系?如果有,其对应关系是啥?

有。

查看pmm.h PDE, PTE与page之间的转化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
static inline ppn_t
page2ppn(struct Page *page) {
return page - pages;
}

static inline uintptr_t
page2pa(struct Page *page) {
return page2ppn(page) << PGSHIFT;
}

static inline struct Page *
pa2page(uintptr_t pa) {
if (PPN(pa) >= npage) {
panic("pa2page called with invalid pa");
}
return &pages[PPN(pa)];
}

static inline struct Page *
pte2page(pte_t pte) {
if (!(pte & PTE_P)) {
panic("pte2page called with invalid pte");
}
return pa2page(PTE_ADDR(pte));
}

static inline struct Page *
pde2page(pde_t pde) {
return pa2page(PDE_ADDR(pde));
}

VPN: 虚拟分页地址 virtual page number

PPN: 物理分页地址 physical page number

页目录项或页表项的前20位表示它对应的是哪个Page。

如果希望虚拟地址与物理地址相等,则需要如何修改lab2,完成此事?

虚地址和物理地址之间有一个偏移,即存在映射关系:

1
phy addr + KERNBASE = virtual addr

KERNBASE为虚拟地址空间中的内核基址,即偏移量。
查看lab2/kern/mm/memlayout.h,得知其定义的KERNBASE是:

1
#define KERNBASE 0xC0000000

将其改为0x0即可使得虚拟地址与物理地址相等

通过ld工具形成的ucore的起始虚拟地址从0xC0100000开始,而bootloader把ucore放在了起始物理地址为0x100000的物理内存空间。要使得虚拟地址与物理地址相等:

tools/kernel.ld

1
2
3
4
SECTIONS {
/* Load the kernel at this address: "." means the current address */
. = 0xC0100000;
...

修改链接脚本,将内核起始虚拟地址0xC0100000修改为0x100000

实验小结

本次实验是关于物理内存管理,主要有物理内存分配和建立页表的相关实验,了解了如何对物理内存进行初步管理和对页表的相关操作。所有的编程内容涉及到的代码量繁多,需要查找分析代码里宏定义和函数定义来完成对已有代码的理解和要完成代码的使用。通过本次实验我深入理解了段页式内存管理机制,对课程内容有了更深的理解。