0%

MIT-6.828-JOS 笔记 - Lab 2: Memory Management

本实验将进行物理内存和虚拟内存的管理,并提供内核地址空间映射的能力。

本实验代码:https://github.com/epis2048/MIT_6.828_2018/tree/lab2

一、物理页管理

操作系统必须知道物理RAM的哪些部分是空闲的,哪些部分当前正在使用。JOS以页面为单位管理物理内存,以便它可以使用MMU映射和保护分配的每一块内存。
现在将编写物理页面分配器。它通过struct PageInfo对象的链表跟踪哪些页面是空闲的(与xv6不同,这些对象不嵌入空闲页面本身),每个对象对应于一个物理页面。在编写虚拟内存实现的其余部分之前,需要先编写物理页分配器,因为页表管理代码将需要分配物理内存来存储页表。

Exercise 1
从lab1知道,进入内核后首先调用的是i386_init(),该函数会调用mem_init()。mem_init()调用其他工具函数实现内核内存管理。
mem_init()先调用boot_alloc()生成一个页的大小的空间用于保存页表,将页表存储在kern_pgdir中(暂时不用)。
boot_alloc()中维护了一个静态指针nextfree,初始值是在/kern/kernel.ld中定义的符号,指向bss段末尾。

1
2
3
4
5
6
7
// kern/pmap.c
// LAB 2: Your code here.
// 返回一个地址,并更新nextFree
result = (char *)nextfree;
nextfree = ROUNDUP(result + n, PGSIZE);
cprintf("boot_alloc memory at %x, next memory allocate at %x\n", result, nextfree);
return result;

然后为pages数组分配空间,该数组是关于PageInfo的数组,每个PageInfo记录了一个物理页的信息。修改mem_init()中的代码

1
2
3
4
5
// kern/pmap.c
// Your code goes here:
// 为pages数组分配空间,pages数组是关于PageInfo的数组,对应一个物理页
pages = (struct PageInfo*) boot_alloc(sizeof(struct PageInfo) * npages);
memset(pages, 0, sizeof(struct PageInfo) * npages);

然后调用page_init,用于初始化pages数组并初始化page_free_list链表。

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
// kern/pmap.c
void
page_init(void)
{
// 初始化pages数组,建立page_free_list链表
// 已使用的物理页包括如下几部分:
// 1)第一个物理页是IDT所在
// 2)[IOPHYSMEM, EXTPHYSMEM)称为IO hole的区域
// 3)EXTPHYSMEM是内核加载的起始位置,终止位置由boot_alloc(0)给出(boot_alloc()分配的内存是内核的最尾部)
size_t i;
size_t io_hole_start_page = (size_t)IOPHYSMEM / PGSIZE;
size_t kernel_end_page = PADDR(boot_alloc(0)) / PGSIZE; //boot_alloc返回的是虚拟地址,需要转为物理地址

for (i = 0; i < npages; i++) {
if (i == 0) {
pages[0].pp_ref = 1;
pages[0].pp_link = NULL;
}
else if (i >= io_hole_start_page && i < kernel_end_page) {
pages[i].pp_ref = 1;
pages[i].pp_link = NULL;
}
else {
pages[i].pp_ref = 0;
pages[i].pp_link = page_free_list;
page_free_list = &pages[i];
}
}
}

接着实现page_alloc(),用于从page_free_list中取空闲空间,根据flags决定要不要初始化0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// kern/pmap.c
struct PageInfo *
page_alloc(int alloc_flags)
{
// Fill this function in
struct PageInfo* ret = page_free_list;
if(ret == NULL){
cprintf("page_alloc: out of memory\n");
return NULL;
}
page_free_list = ret->pp_link;
ret->pp_link = NULL;
if(alloc_flags & ALLOC_ZERO){
memset(page2kva(ret), 0, PGSIZE);
}
return ret;
}

最后是page_free(),其与page_alloc相对应,用于将该空间挂在page_free_list头部。

1
2
3
4
5
6
7
8
9
10
11
12
13
// kern/pmap.c
void
page_free(struct PageInfo *pp)
{
// Fill this function in
// Hint: You may want to panic if pp->pp_ref is nonzero or
// pp->pp_link is not NULL.
if(pp->pp_ref != 0 || pp->pp_link != NULL){
panic("page_free: pp->pp_ref is nonzero or pp->pp_link is not NULL.\n");
}
pp->pp_link = page_free_list;
page_free_list = pp;
}

二、虚拟内存

一个虚拟地址如何被映射到物理地址,将实现一些函数来操作页目录和页表从而达到映射的目的。
x86中虚拟地址、线性地址和物理地址之间的关系:

1
2
3
4
5
6
7
8
           Selector  +--------------+         +-----------+
---------->| | | |
| Segmentation | | Paging |
Software | |-------->| |----------> RAM
Offset | Mechanism | | Mechanism |
---------->| | | |
+--------------+ +-----------+
Virtual Linear Physical

JOS内核有时候在仅知道物理地址的情况下,想要访问该物理地址,但是没有办法绕过MMU的线性地址转换机制,所以没有办法用物理地址直接访问。JOS将虚拟地址0xf0000000映射到物理地址0x0处的一个原因就是希望能有一个简便的方式实现物理地址和线性地址的转换。在知道物理地址pa的情况下可以加0xf0000000得到对应的线性地址,可以用KADDR(pa)宏实现。在知道线性地址va的情况下减0xf0000000可以得到物理地址,可以用宏PADDR(va)实现。

Exercise 4
实现一些函数。
pgdir_walk(),返回一个指针指向虚拟地址va对应的页表条目(PTE),页目录地址由pgdir给出。首先要获取va所在的页目录地址。如果页表还没有分配,那还得先为其创建一个页。之后返回其页目录地址+页表地址即可。

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
// kern/pmap.c
// 获取va所在的PTE(页表)地址
pte_t *
pgdir_walk(pde_t *pgdir, const void *va, int create)
{
// 获取va所在页目录地址
pde_t *pde_ptr = pgdir + PDX(va);
if (!(*pde_ptr & PTE_P)) { // 页表未分配
if (create) { // 创建页
struct PageInfo* pp = page_alloc(1);
if (pp == NULL) return NULL;
else {
pp->pp_ref++;
//更新页目录项
//page2pa将pp转化为物理地址
*pde_ptr = (page2pa(pp)) | PTE_P | PTE_U | PTE_W;
}
}
else { // 算了
return NULL;
}
}
// KADDR:物理地址转换为线性地址
return (pte_t *)KADDR(PTE_ADDR(*pde_ptr)) + PTX(va);
}

boot_map_region(),通过修改pgdir指向的树,将[va, va+size)对应的虚拟地址空间映射到物理地址空间[pa, pa+size)。va和pa都是页对齐的。先计算这段空间包含多少页,然后以页为单位调整。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// kern/pmap.c
// 通过修改pgdir指向的树,将[va, va+size)对应的虚拟地址空间映射到物理地址空间[pa, pa+size)。
static void
boot_map_region(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm)
{
// 计算有多少页,并向上取整
size_t pages_num = size / PGSIZE;
if (size % PGSIZE != 0) pages_num++;
// 分别对每页调整
for (int i = 0; i < pages_num; i++) {
pte_t *pte = pgdir_walk(pgdir, (void *)va, 1); // 获取va对应的页表地址
if (pte == NULL) {
panic("boot_map_region(): out of memory\n");
}
//修改va对应的页表PTE的值
*pte = pa | perm | PTE_P;
pa += PGSIZE;
va += PGSIZE;
}
}

page_insert(),将va映射到pp对应的物理页处。若va已经映射过其他地方,则应先清除映射。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int
page_insert(pde_t *pgdir, struct PageInfo *pp, void *va, int perm)
{
// 获取va对应的页表的地址,如果还没分配,则先分配个物理页
pte_t *pte = pgdir_walk(pgdir, va, 1);
if (pte == NULL) return -E_NO_MEM;
pp->pp_ref++;
if ((*pte) & PTE_P) { // 如果该地址已经被映射过,则释放
page_remove(pgdir, va);
}
// 将PageInfo结构转换为对应物理页的首地址,并修改PTE
physaddr_t pa = page2pa(pp);
*pte = pa | perm | PTE_P;
pgdir[PDX(va)] |= perm;
return 0;
}

page_lookup(),返回va对应的PTE所指向的物理地址对应的PageInfo结构地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// kern/pmap.c
struct PageInfo *
page_lookup(pde_t *pgdir, void *va, pte_t **pte_store)
{
struct PageInfo *pp;
pte_t *pte = pgdir_walk(pgdir, va, 0); // 这里如果不存在就不创建了
if (pte == NULL) return NULL; // 不存在
if (!(*pte) & PTE_P) return NULL; // 状态不是present
// 获取对应的物理地址并转化为PageInfo
physaddr_t pa = PTE_ADDR(*pte);
pp = pa2page(pa);
if (pte_store != NULL) {
*pte_store = pte;
}
return pp;
}

page_remove(),解除va的映射关系。

1
2
3
4
5
6
7
8
9
10
11
12
// kern/pmap.c
void
page_remove(pde_t *pgdir, void *va)
{
// 获取va对应的PTE的地址以及pp结构
pte_t *pte_store;
struct PageInfo *pp = page_lookup(pgdir, va, &pte_store);
if (pp == NULL) return; // 如果还没映射就不用解除
page_decref(pp); //将pp->pp_ref减1,如果pp->pp_ref为0,需要释放该PageInfo结构(将其放入page_free_list链表中)
*pte_store = 0; //将PTE清空
tlb_invalidate(pgdir, va); //失效化TLB缓存
}

三、内存地址映射

这里主要是继续修改mem_init()中的代码,使用(二)提供的函数,对JOS中内存进行映射。将虚拟地址的UPAGES映射到物理地址pages数组开始的位置,以及对内核栈进行映射。

Exercise 5

1
2
3
4
5
6
7
8
9
10
// kern/pmap.c
// 把UPAGES虚拟内存指向 pages。映射大小是PTSIZE,一个页表的大小4M。
boot_map_region(kern_pgdir, UPAGES, PTSIZE, PADDR(pages), PTE_U);
// 使用物理内存bootstack指向内核的栈,内核的栈从KSTACKTOP开始向下增长。
// 分了两块,第一块[KSTACKTOP-KSTKSIZE, KSTACKTOP),这一块需要映射。
// [KSTACKTOP-PTSIZE, KSTACKTOP-KSTKSIZE)这一块不映射,这样如果炸栈了就直接报RE错误,而不是覆盖低地址的数据。
// 因为是从高到底,所以映射就从 KSTACKTOP-KSTKSIZE 到 KSTACKTOP。
boot_map_region(kern_pgdir, KSTACKTOP-KSTKSIZE, KSTKSIZE, PADDR(bootstack), PTE_W);
// 这个就是内核态,里面可以通用的内存,总共256M
boot_map_region(kern_pgdir, KERNBASE, 0xffffffff - KERNBASE, 0, PTE_W);

Question
下面是抄的
2. 到目前为止页目录表中已经包含多少有效页目录项?他们都映射到哪里?
3BD号页目录项,指向的是kern_pgdir
3BC号页目录项,指向的是pages数组
3BF号页目录项,指向的是bootstack
3C0~3FF号页目录项,指向的是kernel
3. 如果我们把kernel和user environment放在一个相同的地址空间中。为什么用户程序不同读取,写入内核的内存空间?用什么机制保护内核的地址范围。
用户程序不能去随意修改内核中的代码,数据,否则可能会破坏内核,造成程序崩溃。
正常的操作系统通常采用两个部件来完成对内核地址的保护,一个是通过段机制来实现的,但是JOS中的分段功能并没有实现。二就是通过分页机制来实现,通过把页表项中的Supervisor/User位置0,那么用户态的代码就不能访问内存中的这个页。
4. 这个操作系统的可以支持的最大数量的物理内存是多大?
由于这个操作系统利用一个大小为4MB的空间UPAGES来存放所有的页的PageInfo结构体信息,每个结构体的大小为8B,所以一共可以存放512K个PageInfo结构体,所以一共可以出现512K个物理页,每个物理页大小为4KB,自然总的物理内存占2GB。
5. 如果现在的物理内存页达到最大个数,那么管理这些内存所需要的额外空间开销有多少?
首先需要存放所有的PageInfo,需要4MB,需要存放页目录表,kern_pgdir,4KB,还需要存放当前的页表,大小为2MB。所以总的开销就是6MB + 4KB。

总结

该实验大体上做三件事:

  1. 提供管理物理内存的数据结构和函数。
  2. 提供修改页目录和页表树结构的函数,从而达到虚拟页到物理页映射的目的。
  3. 用前面两部分的函数建立内核的线性地址空间。