6.828Lab2

Lab2: Memory Management

Intro:

在本实验中,您将为您的操作系统编写内存管理代码。内存管理有两个组件。

第一个组件是内核的物理内存分配器,以便内核可以分配内存并在以后释放它。您的分配器将以 4096 字节为单位运行,称为 pages。您的任务是维护数据结构,记录哪些物理页面是空闲的,哪些是已分配的,以及有多少进程共享每个分配的页面。您还将编写例程来分配和释放内存页面。

内存管理的第二个组成部分是虚拟内存,它将内核和用户软件使用的虚拟地址映射到物理内存中的地址。x86 硬件的内存管理单元 (MMU) 在指令使用内存时执行映射,并参考一组页表。您将根据我们提供的规范修改 JOS 以设置 MMU 的页表。

boot_map_region映射的物理页不改变对应的pp_ref,一个物理页被这个函数映射与它是否被使用没有任何关系;而通过page_insert映射的物理页,同时就表示了该物理页被使用了一次,要给pp_ref加1。

切换到lab2

image-20220603213654097

image-20220603213707953

memlayout.h描述了您必须通过修改pmap.c来实现的虚拟地址空间的布局。

memlayout.hpmap.h定义了PageInfo 用于跟踪哪些物理内存页面空闲的结构。

image-20220623205319533

kclock.c and kclock.h manipulate the PC’s battery-backed clock and CMOS RAM hardware, in which the BIOS records the amount of physical memory the PC contains, among other things.

pmap.c中的代码需要读取这个设备硬件,以便计算出有多少物理内存,但是这部分代码是为您完成的:您不需要了解 CMOS 硬件如何工作的细节。

请特别注意memlayout.hpmap.h,因为本实验要求您使用并理解它们包含的许多定义。您可能还想查看inc/mmu.h,因为它还包含许多对本实验有用的定义。

在开始实验之前,不要忘记add -f 6.828获取 6.828 版本的 QEMU。

Part 1: Physical Page Management

您现在将编写物理页分配器。它通过对象的链接列表跟踪哪些页面是空闲的struct PageInfo(与 xv6 不同,这些对象嵌入空闲页面本身),每个对象对应一个物理页面。您需要先编写物理页面分配器,然后才能编写其余的虚拟内存实现,因为您的页表管理代码将需要分配物理内存来存储页表。

Exercise 1

在文件kern/pmap.c中,您必须实现以下函数的代码(可能按照给定的顺序)。

1
2
3
4
5
boot_alloc()
mem_init()(仅限于调用check_page_free_list(1))
page_init()
page_alloc()
page_free()

check_page_free_list() 和 check_page_alloc()测试您的物理页面分配器。

您应该启动 JOS 并查看是否check_page_alloc() 报告成功。

Fix您的代码以使其通过。You may find it helpful to add your own assert()s to verify that your assumptions are correct.

boot_alloc() 是一个内存分配器

函数的核心是维护一个静态变量nextfree,代表下一个可以使用的空闲内存空间的虚拟地址

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
static void *
boot_alloc(uint32_t n)
{
static char *nextfree; // virtual address of next byte of free memory
char *result;

// Initialize nextfree if this is the first time.
// 'end' is a magic symbol automatically generated by the linker,
// which points to the end of the kernel's bss segment:
// the first virtual address that the linker did *not* assign
// to any kernel code or global variables.
if (!nextfree) {
extern char end[];
nextfree = ROUNDUP((char *) end, PGSIZE);
}

// 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) { // if n == 0, returns the address of the next free page without allocating anything.
return nextfree;
}
// n > 0 分配足够的连续物理内存页以容纳〞n”个字节。returns a kernel virtual address.
result = nextfree;
nextfree += ROUNDUP(n, PGSIZE); // Round up to the nearest multiple of PGSIZE
return result;
}

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
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
void
mem_init(void)
{
uint32_t cr0;
size_t n;

// Find out how much memory the machine has (npages & npages_basemem).
i386_detect_memory();

// Remove this line when you're ready to test this function.
// panic("mem_init: This function is not finished\n");

//////////////////////////////////////////////////////////////////////
// create initial page directory.
kern_pgdir = (pde_t *) boot_alloc(PGSIZE);
memset(kern_pgdir, 0, PGSIZE);

//////////////////////////////////////////////////////////////////////
// Recursively insert PD in itself as a page table, to form
// a virtual page table at virtual address UVPT.
// (For now, you don't have understand the greater purpose of the
// following line.)

// Permissions: kernel R, user R
kern_pgdir[PDX(UVPT)] = PADDR(kern_pgdir) | PTE_U | PTE_P;

//////////////////////////////////////////////////////////////////////
// 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:
// 创建一个struct PageInfo 的数组
// kernel 使用这个数组来耿总每个物理页
// 对于每一个物理页,都会有一个对应的 struct PageInfo 在数组中
pages = (struct PageInfo *) boot_alloc(npages * sizeof(struct PageInfo));
// npages 是内存中物理页的数量
memset(pages, 0, npages * sizeof(struct PageInfo));

//////////////////////////////////////////////////////////////////////
// Now that we've allocated the initial kernel data structures, we set
// up the list of free physical pages. Once we've done so, all further
// memory management will go through the page_* functions. In
// particular, we can now map memory using boot_map_region
// or page_insert
page_init();

check_page_free_list(1);
check_page_alloc();
check_page();

//////////////////////////////////////////////////////////////////////
// Now we set up virtual memory

//////////////////////////////////////////////////////////////////////
// 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:

//////////////////////////////////////////////////////////////////////
// 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:

//////////////////////////////////////////////////////////////////////
// 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:

// Check that the initial page directory has been set up correctly.
check_kern_pgdir();

// Switch from the minimal entry page directory to the full kern_pgdir
// page table we just created. Our instruction pointer should be
// somewhere between KERNBASE and KERNBASE+4MB right now, which is
// mapped the same way by both page tables.
//
// If the machine reboots at this point, you've probably set up your
// kern_pgdir wrong.
lcr3(PADDR(kern_pgdir));

check_page_free_list(0);

// entry.S set the really important flags in cr0 (including enabling
// paging). Here we configure the rest of the flags that we care about.
cr0 = rcr0();
cr0 |= CR0_PE | CR0_PG | CR0_AM | CR0_WP | CR0_NE | CR0_MP;
cr0 &= ~(CR0_TS | CR0_EM);
lcr0(cr0);

// Some more checks, only possible after kern_pgdir is installed.
check_page_installed_pgdir();
}

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
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?
size_t i;
// 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...)
// 将页 0 标记为使用状态
pages[0].pp_ref = 1;
// 2) The rest of base memory, [PGSIZE, npages_basemem * PGSIZE)
// 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];
}
// 3) Then comes the IO hole [IOPHYSMEM, EXTPHYSMEM), which must
// never be allocated.
// io端口, 不能被分配
for(i = IOPHYSMEM/PGSIZE;i<EXTPHYSMEM/PGSIZE;++i){
pages[i].pp_ref = 1;
}

// 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!
// 找到第一个能分配的页面
// boot_alloc有个 nextfree指针,但是是虚拟地址,我们要将其转换为物理地址 physical address
// PADDR 可以实现地址的转换
size_t first_free_address = PADDR(boot_alloc(0));
// 看看extend physical memory 是不是free
for(i = EXTPHYSMEM/PGSIZE;i<first_free_address/PGSIZE;++i){
pages[i].pp_ref = 1;
}
// 把页面设为空闲,插入链表头部
for (i = first_free_address/PGSIZE; i < npages; i++) {
pages[i].pp_ref = 0;
pages[i].pp_link = page_free_list;
page_free_list = &pages[i];
}
}

page_alloc() 完成页面的分配。

分配是基于PageInfo的,只是把页面标记为使用,并未真正的分配页面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// page2kva: page to kernel virtual address
struct PageInfo *
page_alloc(int alloc_flags) {
// Fill this function in
// 超过空闲内存的范围,则返回NULL
// 所分配界面的 pp_link 设置为空, 以便page_free可以检查double-free bug。
// 分配物理页面。如果(alloc_flags & ALLOC_ZERO),用'\0'字节填充整个返回的物理页面。
if(page_free_list == NULL){
return NULL;
}
struct PageInfo* allocated_page = page_free_list;
page_free_list = page_free_list->pp_link;
allocated_page->pp_link = NULL;
if(alloc_flags & ALLOC_ZERO){
memset(page2kva(allocated_page),'\0',PGSIZE);
}
return allocated_page;
}

page_free()

释放一个页面,到page_free_list中

(This function should only be called when pp->pp_ref reaches 0.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Return a page to the free list.
// (This function should only be called when pp->pp_ref reaches 0.)
//
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("Double check failed when dealloc page");
return;
}
pp->pp_link = page_free_list; // 头插
page_free_list = pp;
}

使用 make qemu-nox 运行,发现报了个panic, 需要把panic注释掉。看漏了

image-20220626094749150

image-20220626094809597

image-20220626104146086

Part 2: Virtual Memory

虚拟内存
当 cpu 拿到一个地址并根据地址访问内存时,在 x86架构下药经过至少两级的地址变换:段式变换和页式变换。分段机制的主要目的是将代码段、数据段以及堆栈段分开,保证互不干扰。分页机制则是为了实现虚拟内存。
虚拟内存主要的好处是:

让每个程序都以为自己独占计算机内存空间,概念清晰,方便程序的编译和装载。
通过将部分内存暂存在磁盘上,可以让程序使用比物理内存大得多的虚拟内存,突破物理内存的限制。
通过对不同进程设置不同页表,可以防止进程访问其他进程的地址空间。通过在不同进程之间映射相同的物理页,又可以提供进程间的共享。

虚拟、线性和物理地址

虚拟地址
最原始的地址,也是 C/C++ 指针使用的地址。由前 16bit 段 (segment) 选择器和后 32bit 段内的偏移 (offset) 组成,显然一个段大小为 4GB。通过虚拟地址可以获得线性地址。
线性地址
前 10bit 为页目录项(page directory entry, PDE),即该地址在页目录中的索引。中间 10bit 为页表项(page table entry, PTE),代表在页表中的索引,最后 12bit 为偏移,也就是每页 4kB。通过线性地址可以获得物理地址。

页目录偏移DIR |页表偏移Table|页内偏移Offset

物理地址
经过段转换以及页面转换,最终在 RAM 的硬件总线上的地址。

JOS只有一个段,因此虚拟地址在数值上等于线性地址。

Exercise 4

image-20220626152236756

pgdir 是指向页目录的指针。

pgdir_walk() returns a pointer to page table entry(PTE) for linear address(va)。查找一个虚拟地址对应的页表项地址。

img

在页目录项、页表项中存储的是页表项的物理地址前 20bit 外加 12bit 的 flag。

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
// Given 'pgdir', a pointer to a page directory, pgdir_walk returns
// a pointer to the page table entry (PTE) for linear address 'va'.
// This requires walking the two-level page table structure.
//
// The relevant page table page might not exist yet.
// If this is true, and create == false, then pgdir_walk returns NULL.
// Otherwise, pgdir_walk allocates a new page table page with page_alloc.
// - If the allocation fails, pgdir_walk returns NULL.
// - Otherwise, the new page's reference count is incremented,
// the page is cleared,
// and pgdir_walk returns a pointer into the new page table page.
//
// Hint 1: you can turn a PageInfo * into the physical address of the
// page it refers to with page2pa() from kern/pmap.h.
//
// Hint 2: the x86 MMU checks permission bits in both the page directory
// and the page table, so it's safe to leave permissions in the page
// directory more permissive than strictly necessary.
//
// Hint 3: look at inc/mmu.h for useful macros that manipulate page
// table and page directory entries.
//
// typedef uint32_t pte_t;
// pgdir_walk returns a pointer to the page table entry (PTE) for linear address 'va'.
pte_t *
pgdir_walk(pde_t *pgdir, const void *va, int create)
{
// Fill this function in
// pgdir 页目录项地址
// va 虚拟地址,jos只有一个段,因此虚拟地址等于线性地址
// create 若页目录项不存在是否创建
// return 页表项指针
uint32_t page_dir_index = PDX(va);
uint32_t page_table_index = PTX(va);

pte_t* pgtab;
if(pgdir[page_dir_index] && PTE_P){ // 存在且可写
pgtab = KADDR(PTE_ADDR(pgdir[page_dir_index])); // KADDR->virtual address
}else{ // 不存在
if(create){
// 创建新的页表项
// For page_alloc, zero the returned physical page.
// ALLOC_ZERO = 1<<0,
struct PageInfo* new_pageInfo = page_alloc(ALLOC_ZERO);
if(new_pageInfo){
new_pageInfo->pp_ref+=1;
// 存入数组
// 依次获取 table_index 和 dir_index
// page2kva() page to kernel virtual address
pgtab = (pte_t*)page2kva(new_pageInfo);
pgdir[page_dir_index] = PADDR(pgtab) | PTE_P | PTE_W | PTE_U; // PADDR 虚拟到物理
}else{
return NULL;
}
}else{
return NULL;
};
}
return &pgtab[page_table_index];
}

page_lookup()

// 返回映射到虚拟地址 va 的页面
// pgdir_walk 只查询,不创建,create为0
// pa2page 由物理地址 返回对应的页面描述

1
2
Map [va, va+size) of virtual address space to physical [pa, pa+size) in the page table rooted at pgdir.  Size is a multiple of PGSIZE, and va and pa are both page-aligned.
This function is only intended to set up the ``static'' mappings above UTOP. As such, it should *not* change the pp_ref field on the mapped pages.
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
// Return the page mapped at virtual address 'va'.
// If pte_store is not zero, then we store in it the address
// of the pte for this page. This is used by page_remove and
// can be used to verify page permissions for syscall arguments,
// but should not be used by most callers.
//
// Return NULL if there is no page mapped at va.
//
// Hint: the TA solution uses pgdir_walk and pa2page.
//
// 返回映射到虚拟地址 va 的页面
// pgdir_walk 只查询,不创建,create为0
// pa2page 由物理地址 返回对应的页面描述
struct PageInfo *
page_lookup(pde_t *pgdir, void *va, pte_t **pte_store)
{
// Fill this function in
// pdgir 页目录地址
// va 虚拟地址
// pte_store 指向页表指针的指针 the address of the pte for this page
// If pte_store is not zero, then we store in it the address of the pte for this page.
pde_t* find_pgtab = pgdir_walk(pgdir, va, 0); // 根据va,返回一个指向page table entry的指针
if(!find_pgtab){ // 没找到
return NULL;
}
// 找到了
// 再找page table的虚拟地址
if(pte_store){
*pte_store = find_pgtab; // 保存下
}
// 返回页面描述 struct PageInfo *
return pa2page(PTE_ADDR(*find_pgtab)); // PTE_ADDR 将页表指针指向的内容转为物理地址
}

page_remove()

移除一个虚拟地址与对应物理地址的映射关系

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
//
// Unmaps the physical page at virtual address 'va'.
// If there is no physical page at that address, silently does nothing.
//
// Details:
// - The ref count on the physical page should decrement.
// - The physical page should be freed if the refcount reaches 0.
// - The pg table entry corresponding to 'va' should be set to 0.
// (if such a PTE exists)
// - The TLB must be invalidated if you remove an entry from
// the page table.
//
// Hint: The TA solution is implemented using page_lookup,
// tlb_invalidate, and page_decref.
//
// 移除一个虚拟地址与对应物理地址的映射关系
void
page_remove(pde_t *pgdir, void *va)
{
// Fill this function in
// pgdir 页目录地址
// va 虚拟地址
// 首先要找到 va对应的物理地址, 使用 page_lookup
// page_lookup(pde_t *pgdir, void *va, pte_t **pte_store)
pte_t* pgtab;
pte_t** pte_store = &pgtab;
struct PageInfo* pInfo = page_lookup(pgdir, va, pte_store);
if(!pInfo){ // 空的
return;
}
page_decref(pInfo); // 减少页上的引用计数,如果没有引用则释放该计数。
*pgtab = 0;
// tlb_invalidate(pde_t *pgdir, void *va)
tlb_invalidate(pgdir, va); // 使TLB条目无效,但前提是正在编辑的页表是当前处理器正在使用的页表。
}

page_insert()

建立一个虚拟地址与物理页的映射,与page_remove() 对应。

The permissions (the low 12 bits) of the page table entry should be set to ‘perm|PTE_P’.

requirement:

// 如果已经有一个页面映射到’va’,it should be page_remove()d.

// 如果有必要,应按需分配一个页表,并插入到’pgdir’。

// 如果插入成功,pp->pp_ref应该加1。

// TLB必须无效,如果 ‘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
26
27
28
29
30
// 建立一个虚拟地址与物理页的映射,与page_remove() 对应
int
page_insert(pde_t *pgdir, struct PageInfo *pp, void *va, int perm)
{
// Fill this function in
// pgdir 页目录指针
// pp 页描述结构体 指针
// va 虚拟地址
// perm 权限
pte_t* pgtab = pgdir_walk(pgdir, va, 1); // 查询该虚拟地址对应的页表项(struct PageInfo),不存在则建立
if(!pgtab){ // 建立失败
return -E_NO_MEM;
}
// 建立成功
if(*pgtab && PTE_P){ // 可以写入
// 若该虚拟地址va已经映射到了其他物理页
if(page2pa(pp) == PTE_ADDR(*pgtab)){ // PTE_ADDR Address in page table or page directory entry
// 更改权限,不增加引用
*pgtab = page2pa(pp) | perm | PTE_P;
return 0;
}else{
// 更新映射的物理页,则需要删除之前的映射关系
page_remove(pgdir, va);
}
}
// 删除后建立新的物理页
*pgtab = page2pa(pp) | perm | PTE_P;
pp->pp_ref++;
return 0;
}

boot_map_region()

映射一片虚拟页到制定物理页,大小为size, size是PGSIZE的倍数

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
//
// Map [va, va+size) of virtual address space to physical [pa, pa+size)
// in the page table rooted at pgdir. Size is a multiple of PGSIZE, and
// va and pa are both page-aligned.
// Use permission bits perm|PTE_P for the entries.
//
// This function is only intended to set up the ``static'' mappings
// above UTOP. As such, it should *not* change the pp_ref field on the
// mapped pages.
//
// Hint: the TA solution uses pgdir_walk
// 映射一片虚拟页到指定物理页,大小为size, size是PGSIZE的倍数
// va -> pa
static void
boot_map_region(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm)
{
// Fill this function in
// *pgdir 页目录指针
// va 虚拟地址
// size size是PGSIZE的倍数,
// pa 物理地址
// perm 权限
// 直接使用页数来分配,避免溢出
pte_t* pgtab;
size_t pg_count = PGNUM(size); // size能分成多少页
// pte_t* pgdir_walk(pde_t *pgdir, const void *va, int create)
for(size_t i = 0;i<pg_count;++i){
pgtab = pgdir_walk(pgdir, (void*)va, 1);
// pgdir_walk returns a pointer to the page table entry (PTE) for linear address 'va'.
*pgtab = pa | perm | PTE_P; // 权限
va+=PGSIZE;
pa+=PGSIZE;
}

}

image-20220627153213219

Part 3: Kernel Address Space

image-20220627204004274

JOS将处理器的32位线性地址划分为 用户地址() 和 内核地址(),二者以ULIM划分。

计算可得出一个物理页大小是4MB

ULIM = (MMIOLIM - PTSIZE) = (KSTACKTOP - PTSIZE - PTSIZE) = 0xF0000000 - 0x00400000 - 0x00400000 = 0xef800000

查看memlayout.h 可以看到,的确为0xef800000

image-20220627204422272

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
//////////////////////////////////////////////////////////////////////
// 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:
// UPAGES是JOS记录物理页面使用情况的数据结构,只有kernel能够访问
// 但是现在需要让用户空间能够读取这段线性地址,因此需要建立映射,将用户空间的一块内存映射到存储该数据结构的物理地址上
// boot_map_region() 建立映射关系
boot_map_region(kern_pgdir, (uintptr_t)UPAGES, npages*sizeof(struct PageInfo), PADDR(pages), PTE_U | PTE_P);
// 目前建立了一个页目录,kernel_pgdir
// pgdir为页目录指针, UPAGES为虚拟地址,npages*sizeof(struct* PageInfo)为映射的内存块大小
// PADDR(pages) 为物理地址, PTE_U | PTE为权限 (PTE_U 表示用户可读)


//////////////////////////////////////////////////////////////////////
// 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:
// kernel 内核栈
// kernel stack 从虚拟地址 KSTACKTOP 开始,向低地址增长,所以KSTACKTOP实际上是栈顶
// KSTACKTOP = 0xf0000000,
// KSTKSIZE = (8*PGSIZE) = 8*4096(bytes) = 32KB
// 只需要映射 [KSTACKTOP, KSTACKTOP - KSTKSIZE) 范围的虚拟地址
boot_map_region(kern_pgdir, (uintptr_t)(KSTACKTOP - KSTKSIZE), KSTKSIZE, PADDR(bootstack), PTE_W | PTE_P);
// PTE_W 开启了写权限,但是并未打开 PTE_U, 因此用户没有权限,只有内核有权限


//////////////////////////////////////////////////////////////////////
// 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:
// 内核部分
// KERNBASE = 0xF0000000, VA大小为 2^32 - KERNBASE
// ROUNDUP(a,n) 将a四舍五入到最接近n的倍数
boot_map_region(kern_pgdir, (uintptr_t)KERNBASE, ROUNDUP(0xffffffff - KERNBASE + 1, PGSIZE), 0, PTE_W | PTE_P);

image-20220628111506229

问题

  1. 此时,页面目录中填写了哪些条目(行)?他们映射什么地址,指向哪里?换句话说,尽可能多地填写这张表格:

    入口 基本虚拟地址 指向(逻辑上):
    1023 0xffc00000 page table for [252,256)MB of physical address
    1022 0xff900000 page table for [248,252)MB of physical address
    960 0xf0000000 page table for [0,4)MB of physical address
    959 0xefc00000
    958 0xef800000 ULIM
    957 0xef400000 State register (UVPT)
    956 0xef000000 UPAGES, array of PageInfo
    955 0xeec00000 UPAGES, array of PageInfo
    NULL
    1 0x00400000 NULL
    0 0x00000000 same as 960
    1
    2
    3
    4
    // User read-only virtual page table (see 'uvpt' below)
    #define UVPT (ULIM - PTSIZE)
    // Read-only copies of the Page structures
    #define UPAGES (UVPT - PTSIZE)
  2. 我们已将内核和用户环境放置在同一地址空间中。为什么用户程序无法读取或写入内核的内存?哪些特定机制可以保护内核内存?

    页表内的标记位可以设置权限,PTE_U设置为1,用户才有权利读写。

  3. 这个操作系统可以支持的最大物理内存量是多少?为什么?

    UPAGES 大小是4096bytes,即4MB,每个结构体 PageInfo 占8bytes。指针占4字节,uint16_t占两字节,对齐后8字节。

    那么共有 4MB / 8B = 2^19 页,

    每页的大小PGSIZE = 4096 bytes

    那么最多使用 2^19 * 4096 = 2^31 = 2GB 的物理内存

    image-20220628152236712

  4. 如果我们真的有最大数量的物理内存,有多少空间来管理内存?这个开销是怎么分解的?

    为2GB的最大内存时,UPAGES的大小为4MB,page table directory 的大小为4MB, 一共8MB。

  5. 重新访问kern/entry.Skern/entrypgdir.c中的页面表设置。在我们打开分页后,EIP仍然是一个低数字(略高于1MB)。我们什么时候过渡到KERNBASE上方的EIP运行?when we enable paging and when we begin running at an EIP above KERNBASE,是什么使我们能够继续以低EIP执行?为什么需要这种过渡?

    EIP寄存器存储着CPU读取的下一条指令的地址,相当于PC计数器。在8086中,EIP=PC。

    image-20220628163341015

在jmp处打上断点,向后执行一步,产生了映射,分页机制启动

image-20220628163655193

把虚拟地址的[0,4M) [KERNBASE, KERNBASE+4M)两个区间都映射到同一个物理地址区间[0,4M)的原因在于不要让指令的寻址受到地址空间变化的影响。

  • Display in a useful and easy-to-read format all of the physical page mappings (or lack thereof) that apply to a particular range of virtual/linear addresses in the currently active address space. For example, you might enter 'showmappings 0x3000 0x5000' to display the physical page mappings and corresponding permission bits that apply to the pages at virtual addresses 0x3000, 0x4000, and 0x5000.
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
// 添加lab2 中的映射函数,以显示pa和va的对应关系
int
mon_showmappings(int argc, char **argv, struct Trapframe *tf){
// 检查参数个数
if(argc != 3){
cprintf("invalid arguments num. \n");
return -1;
}
// 参数个数符合,那么依次提取pa和va
char *errChar;
uintptr_t start_addr = strtol(argv[1], &errChar, 16);
// typedef uint32_t uintptr_t; uintptr_t represents the numerical value of virtual address.
if(*errChar){
cprintf("invalid virtual address. \n");
}
uintptr_t end_addr = strtol(argv[2], &errChar, 16);
if(*errChar){
cprintf("invalid virtual address. \n");
}

if(start_addr > end_addr){
cprintf("address 1 should be lower than address 2\n");
return -1;
}

// 按页对齐
start_addr = ROUNDDOWN(start_addr, PGSIZE);
end_addr = ROUNDUP(end_addr, PGSIZE);

// 依次访问
uintptr_t cur_addr = start_addr;
while(cur_addr <= end_addr){
// 查询当前地址,没有则创建
pte_t *cur_pte = pgdir_walk(kern_pgdir, (void*)cur_addr, 0);
// pgdir_walk() returns a pointer to the page table entry (PTE) for linear address 'va'.
if(!cur_pte || !(cur_pte && PTE_P)){
cprintf("virtual address [%08x] - not mapped\n", cur_addr);
}else{
cprintf("virtual address [%08x] - physical address [%08x], permission", cur_addr, PTE_ADDR(*cur_pte));
// 查询三种权限
char perm_PS = (*cur_pte & PTE_PS)?'S':'-';
char perm_W = (*cur_pte & PTE_W)?'W':'-';
char perm_U = (*cur_pte & PTE_U)?'U':'-';
cprintf("=%c===%c%cP\n", perm_PS, perm_W, perm_U);
}
cur_addr += PGSIZE;
}
return 0;
}

image-20220628213911940

参考ref:https://www.jianshu.com/u/6913c26d8b2c