Lab2 Memory Management

简介

实验地址:https://pdos.csail.mit.edu/6.828/2018/labs/lab2/

lab2我们将建立操作系统对内存的管理,其中内存管理分为了两个部分:物理内存管理和虚拟内存管理

Part 1: Physical Page Management

JOS 通过一个链表来管理物理内存,写一个极其简易的物理页分配器,这个部分的最大的难度在于对 JOS 内存布局的理解。

Exercise 1

在文件 kern/pmap.c 中,实现下面的所有函数。

boot_alloc()

根据函数的解释说明,我们得知,boot_alloc() 是 JOS 操作系统在设置虚拟内存系统的时候使用的物理内存分配器。

从代码中我们可以看到,在设置虚拟内存系统的时候,维护了一个全局的 nextfree 指针,该指针指着下一个空闲的地址,而且这个指针只的刚好是 extend memory 里边儿的 kernel.ld 中指定的内核 bss 的结束地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
   // Allocate a chunk large enough to hold 'n' bytes, then update
// nextfree. Make sure nextfree is kept aligned
// to a multiple of PGSIZE.
//
// LAB 2: Your code here.

if(n == 0){
return nextfree;
}

result = nextfree;
nextfree = ROUNDUP(nextfree + n, PGSIZE);

if((uint32_t)nextfree - KERNBASE > (npages * PGSIZE)){
panic("Out of memory!\n");
}

return result;

因此我们参照具体的内容,当调用值 n 的时候,按照页的大小,进行分配并且返回该内存页的起始地址。

mem_init()

1
2
3
4
5
6
7
8
9
//////////////////////////////////////////////////////////////////////
// Allocate an array of npages 'struct PageInfo's and store it in 'pages'.
// The kernel uses this array to keep track of physical pages: for
// each physical page, there is a corresponding struct PageInfo in this
// array. 'npages' is the number of physical pages in memory. Use memset
// to initialize all fields of each struct PageInfo to 0.
// Your code goes here:
pages = (struct PageInfo*)boot_alloc(sizeof(struct PageInfo) * npages);
memset(pages, 0 ,sizeof(pages));

创建一个数组,这个数组也就是所有的物理页了,然后初始化所有的 pages 为 0。

page_init()

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
void
page_init(void)
{
// The example code here marks all physical pages as free.
// However this is not truly the case. What memory is free?
// 1) Mark physical page 0 as in use.
// This way we preserve the real-mode IDT and BIOS structures
// in case we ever need them. (Currently we don't, but...)
// 2) The rest of base memory, [PGSIZE, npages_basemem * PGSIZE)
// is free.
// 3) Then comes the IO hole [IOPHYSMEM, EXTPHYSMEM), which must
// never be allocated.
// 4) Then extended memory [EXTPHYSMEM, ...).
// Some of it is in use, some is free. Where is the kernel
// in physical memory? Which pages are already in use for
// page tables and other data structures?
//
// Change the code to reflect this.
// NB: DO NOT actually touch the physical memory corresponding to
// free pages!
size_t i;
page_free_list = NULL;

//page 0 in use
pages[0].pp_ref = 1;
pages[0].pp_link = NULL;

//[PGSIZE, npages_basemem) is free
for(i = 1; i < npages_basemem; i++){
pages[i].pp_ref = 0;
pages[i].pp_link = page_free_list;
page_free_list = &pages[i];
}

//IO hole [IOPHYSMEM, EXTPYSMEM) must never be allocated
for(;i < EXTPHYSMEM / PGSIZE; i++){
pages[i].pp_ref = 1;
pages[i].pp_link = NULL;
}

//extended memory [EXTPHYSMEM, nextfree) is in use
//tip boot_alloc algned to PGSIZE
for(; i < PADDR(boot_alloc(0)) / PGSIZE; i++){
pages[i].pp_ref = 1;
pages[i].pp_link = NULL;
}

//rest extended memory is free
for(; i < npages; i++){
pages[i].pp_ref = 0;
pages[i].pp_link = page_free_list;
page_free_list = &pages[i];
}

}

page_alloc()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Hint: use page2kva and memset
struct PageInfo *
page_alloc(int alloc_flags)
{
// Fill this function in
if (page_free_list == NULL)
{
return NULL;
}

struct PageInfo* page = page_free_list;
page_free_list = page->pp_link;
page->pp_ref = 0;
page->pp_link = NULL;

if(alloc_flags & ALLOC_ZERO){
memset(page2kva(page), '\0', PGSIZE);
}

return page;
}

page_free()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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){
panic("page->pp_ref is nonzero!\n");
}
if(pp->pp_link){
panic("page->pp_link is not NULL!\n");
}

pp->pp_link = page_free_list;
page_free_list = pp;
}

check_page_free_list()/check_page_alloc()

Exercise 1 result

Part 2:Virtual Memory

Exercise 2

阅读 intel 80386 手册,对 x86 的段、页式管理有一个初步的理解。

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

在 X86 的逻辑中,虚拟地址包含一个段选择子和偏移地址,线性地址是段地址转换过后但是没有在页地址转换之前的地址。物理地址是最后得到的地址,可以通过硬件地址总线访问 RAM。

Virtual,Linear,and Physical Addresses

Page Translation

Exercise 3

在 JOS 中,一个 C 的指针就是虚拟地址的 offset。在 boot/boots.S 中,GDT 将所有的段的地址都以 0 为起始地址,0xffffffff 为界限,这样就可以有效的让段地址转换变得较为简单。这样就可以让线性地址就是虚拟地址的偏移地址,JOS 的老师通过这样的方式让我们专注于页的转换。

打开 QEMU monitor 过后,Ctrl-a c 打开控制界面,我用的 WSL 还打开了一堆 QQ 之类的软件儿,可能有快捷键冲突,因此可以使用这条命令来试一试:

1
2
make
qemu-system-i386 -hda obj/kern/kernel.img -monitor stdio -gdb tcp::26000 -D qemu.log

这个时候,在按照相应的命令来进行操作:

1
2
3
4
5
xp /Nx paddr 	可以看到物理内存地址
x /Nx addr 可以看到虚拟内存地址
info registers 可以看到寄存器内容
info mem 可以看到已经有的页表映射的虚拟地址空间,以及访问他们的优先级
info pg 可以看到当前页表的结构

Reference counting

在未来的实验中,通常会同时将相同的物理页面映射到多个虚拟地址(或多个环境的地址空间中)。 我们将在与物理页对应的 struct PageInfo 的 pp_ref 字段中保存对每个物理页的引用次数的计数。 当物理页的计数变为零时,该页可以被释放,因为它不再被使用。

一般来说,这个计数应该等于所有页表中物理页出现在 UTOP 下面的次数(UTOP 以上的映射大多是在引导时由内核设置的,不应该被释放,所以不需要引用计数)。我们还将使用它来跟踪我们保留的指向页目录页面的指针数量,进而跟踪页目录对页表页面的引用数量。

使用 page_alloc 时要小心。 它返回的页面的引用计数始终为 0,因此一旦您对返回的页面执行了某些操作(例如将其插入页表),pp_ref 就应该递增。 有时这是由其他函数(例如 page_insert)处理的,有时调用 page_alloc 的函数必须直接执行此操作。

Page Table Management

这个部分开始对页表进行管理,写一些例程来管理 page table,插入和删除 linear-to-physical 的映射,并且创建 page table pages 当需要的时候。

Exercise 4

要实现的函数在 kern/pmap.c 文件中,在实现下列函数之前,需要弄懂页表管理以及多个宏的含义。

pgdir_walk()

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
pte_t *
pgdir_walk(pde_t *pgdir, const void *va, int create)
{
// Fill this function in
pde_t *pde;
pte_t *pte;
struct PageInfo *page;

pde = &pgdir[PDX(va)];

if(!(*pde & PTE_P)){
if(create == false){
return NULL;
}else{
page = page_alloc(ALLOC_ZERO);
if(page == NULL){
return NULL;
}
page->pp_ref ++;
*pde = page2pa(page) | PTE_P | PTE_W | PTE_U;
}
}

pte = (pte_t*)KADDR(PTE_ADDR(*pde));

return &pte[PTX(va)];
}

boot_map_region()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static void
boot_map_region(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm)
{
// Fill this function in
size_t i;
pte_t *pte;
size_t numpage = ROUNDUP(size, PGSIZE) / PGSIZE;

for(i = 0; i < numpage; i ++){
pte = pgdir_walk(pgdir, (void*) va, 1);
if(pte == NULL){
panic("boot_map_region: out of memory!\n");
}

*pte = (pa | perm | PTE_P);

va += PGSIZE;
pa += PGSIZE;
}
}

page_lookup()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct PageInfo *
page_lookup(pde_t *pgdir, void *va, pte_t **pte_store)
{
// Fill this function in
pde_t *pde;
pte_t *pte;
struct PageInfo *page;

pte = pgdir_walk(pgdir, va, 0);

if(pte == NULL){
return NULL;
}

if(*pte_store){
*pte_store = pte;
}

page = pa2page(PTE_ADDR(*pte));
return page;
}

page_remove()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void
page_remove(pde_t *pgdir, void *va)
{
// Fill this function in
pte_t *pte;
struct PageInfo *page;

page = page_lookup(pgdir, va, &pte);
if(page == NULL){
return;
}

page_decref(page);
tlb_invalidate(pgdir, va);
*pte = 0;
}

page_insert()

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
int
page_insert(pde_t *pgdir, struct PageInfo *pp, void *va, int perm)
{
// Fill this function in
pte_t *pte;
struct PageInfo *page;

pte = pgdir_walk(pgdir, va, 1);

if(!pte){
return -E_NO_MEM;
}

if(*pte & PTE_P){
if(PTE_ADDR(*pte) == page2pa(pp)){
pp->pp_ref --;
}else{
tlb_invalidate(pgdir, va);
page_remove(pgdir, va);
}
}

pp->pp_ref ++;

*pte = page2pa(pp) | perm | PTE_P;

return 0;
}

check_page()

virtual memory result

Part 3:Kernel Address Space

JOS 将处理器的 32-bit 线性地址空间划分为两个部分。在 inc/memlayout.h 中可以看到。

我们将在实验 3 中开始加载和运行的用户环境(进程)将控制下部的布局和内容,而内核始终保持对上部的完全控制。

分割线是由 inc/memlayout.h 中的符号 ULIM 随意定义的,为内核保留了大约 256MB 的虚拟地址空间。 这解释了为什么我们需要在实验 1 中为内核提供如此高的链接地址:否则内核的虚拟地址空间中将没有足够的空间来同时映射到其下方的用户环境。

Permissions and Fault Isolation

由于内核和用户内存都存在于每个环境的地址空间中,因此我们必须使用 x86 页表中的权限位来允许用户代码仅访问地址空间的用户部分。 否则,用户代码中的错误可能会覆盖内核数据,从而导致崩溃或更细微的故障; 用户代码还可能窃取其他环境的私有数据。 请注意,可写权限位(PTE_W)会影响用户和内核代码!

用户环境将没有权限访问 ULIM 以上的任何内存,而内核将能够读写该内存。 对于地址范围 [UTOP,ULIM),内核和用户环境都具有相同的权限:可以读取但不能写入该地址范围。 该地址范围用于向用户环境公开某些只读的内核数据结构。 最后,UTOP下面的地址空间是供用户环境使用的; 用户环境将设置访问该内存的权限。

Initializing the Kernel Address Space

现在将在 UTOP 之上设置地址空间:地址空间的内核部分。
inc/memlayout.h 显示应该使用的布局。
使用刚刚编写的函数来设置适当的线性到物理映射。

Exercise 5

完善 mem_init()中缺失的代码,位置在 check_page()之后。

mem_init()

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
//////////////////////////////////////////////////////////////////////
// Map 'pages' read-only by the user at linear address UPAGES
// Permissions:
// - the new image at UPAGES -- kernel R, user R
// (ie. perm = PTE_U | PTE_P)
// - pages itself -- kernel RW, user NONE
// Your code goes here:
boot_map_region(kern_pgdir, UPAGES, PTSIZE, PADDR(pages), PTE_U | PTE_P);
//////////////////////////////////////////////////////////////////////
// Use the physical memory that 'bootstack' refers to as the kernel
// stack. The kernel stack grows down from virtual address KSTACKTOP.
// We consider the entire range from [KSTACKTOP-PTSIZE, KSTACKTOP)
// to be the kernel stack, but break this into two pieces:
// * [KSTACKTOP-KSTKSIZE, KSTACKTOP) -- backed by physical memory
// * [KSTACKTOP-PTSIZE, KSTACKTOP-KSTKSIZE) -- not backed; so if
// the kernel overflows its stack, it will fault rather than
// overwrite memory. Known as a "guard page".
// Permissions: kernel RW, user NONE
// Your code goes here:
boot_map_region(kern_pgdir, KSTACKTOP - KSTKSIZE, KSTKSIZE, PADDR(bootstack), PTE_W);

//////////////////////////////////////////////////////////////////////
// Map all of physical memory at KERNBASE.
// Ie. the VA range [KERNBASE, 2^32) should map to
// the PA range [0, 2^32 - KERNBASE)
// We might not have 2^32 - KERNBASE bytes of physical memory, but
// we just set up the mapping anyway.
// Permissions: kernel RW, user NONE
// Your code goes here:
boot_map_region(kern_pgdir, KERNBASE, 0xffffffff - KERNBASE, 0, PTE_W);

check_kern_pgdir()/check_page_installed_pgdir()

Exercise 5 result

make grade


Lab2 Memory Management
https://www.bencorn.com/2021/08/13/Lab2-Memory-Management/
作者
Bencorn
发布于
2021年8月13日
许可协议