0%

MIT6.828 Lab2

Before start

以下是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
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
/*
* Virtual memory map: Permissions
* kernel/user
*
* 4 Gig --------> +------------------------------+
* | | RW/--
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* : . :
* : . :
* : . :
* |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~| RW/--
* | | RW/--
* | Remapped Physical Memory | RW/--
* | | RW/--
* KERNBASE, ----> +------------------------------+ 0xf0000000 --+
* KSTACKTOP | CPU0's Kernel Stack | RW/-- KSTKSIZE |
* | - - - - - - - - - - - - - - -| |
* | Invalid Memory (*) | --/-- KSTKGAP |
* +------------------------------+ |
* | CPU1's Kernel Stack | RW/-- KSTKSIZE |
* | - - - - - - - - - - - - - - -| PTSIZE
* | Invalid Memory (*) | --/-- KSTKGAP |
* +------------------------------+ |
* : . : |
* : . : |
* MMIOLIM ------> +------------------------------+ 0xefc00000 --+
* | Memory-mapped I/O | RW/-- PTSIZE
* ULIM, MMIOBASE --> +------------------------------+ 0xef800000
* | Cur. Page Table (User R-) | R-/R- PTSIZE
* UVPT ----> +------------------------------+ 0xef400000
* | RO PAGES | R-/R- PTSIZE
* UPAGES ----> +------------------------------+ 0xef000000
* | RO ENVS | R-/R- PTSIZE
* UTOP,UENVS ------> +------------------------------+ 0xeec00000
* UXSTACKTOP -/ | User Exception Stack | RW/RW PGSIZE
* +------------------------------+ 0xeebff000
* | Empty Memory (*) | --/-- PGSIZE
* USTACKTOP ---> +------------------------------+ 0xeebfe000
* | Normal User Stack | RW/RW PGSIZE
* +------------------------------+ 0xeebfd000
* | |
* | |
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* . .
* . .
* . .
* |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|
* | Program Data & Heap |
* UTEXT --------> +------------------------------+ 0x00800000
* PFTEMP -------> | Empty Memory (*) | PTSIZE
* | |
* UTEMP --------> +------------------------------+ 0x00400000 --+
* | Empty Memory (*) | |
* | - - - - - - - - - - - - - - -| |
* | User STAB Data (optional) | PTSIZE
* USTABDATA ----> +------------------------------+ 0x00200000 |
* | Empty Memory (*) | |
* 0 ------------> +------------------------------+ --+
*
* (*) Note: The kernel ensures that "Invalid Memory" is *never* mapped.
* "Empty Memory" is normally unmapped, but user programs may map pages
* there if desired. JOS user programs map pages temporarily at UTEMP.
*/

总体的虚拟内存布局是如上的一个状态。

在inc/mmu.h文件的注释中可以看到对于线性地址的结构描述如下,是按二级页表的方式进行地址转换的。前十位是一级页表的索引,中间十位是二级页表索引,最后的12位表示的是4K页面内部的偏移量。

1
2
3
4
5
6
7
8
// A linear address 'la' has a three-part structure as follows:
//
// +--------10------+-------10-------+---------12----------+
// | Page Directory | Page Table | Offset within Page |
// | Index | Index | |
// +----------------+----------------+---------------------+
// \--- PDX(la) --/ \--- PTX(la) --/ \---- PGOFF(la) ----/
// \---------- PGNUM(la) ----------/

在inc/memlayout.h中可以看到PageInfo的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
struct PageInfo {
// Next page on the free list.
struct PageInfo *pp_link;

// pp_ref is the count of pointers (usually in page table entries)
// to this page, for pages allocated using page_alloc.
// Pages allocated at boot time using pmap.c's
// boot_alloc do not have valid reference count fields.

uint16_t pp_ref;
};

其中pp_link链接的是free list当中下一个空闲的页面,而pp_ref表示的是指向该页面的指针的个数,当清零的时候说明页面就没有被指向了。在全局是利用一个PageInfo的数组来存放物理页面状态:

1
extern struct PageInfo *pages;

可以发现页面是通过一个PageInfo类型进行描述,指针与pages地址的差值就是页面号,物理地址就直接是一个32-bit的整数,相互转换依照上方的三级结构进行:

1
2
3
4
5
6
7
8
9
10
11
12
13
static inline physaddr_t
page2pa(struct PageInfo *pp)
{
return (pp - pages) << PGSHIFT;
}

static inline struct PageInfo*
pa2page(physaddr_t pa)
{
if (PGNUM(pa) >= npages)
panic("pa2page called with invalid pa");
return &pages[PGNUM(pa)];
}

所以以上两个函数提供了一个在page和物理地址之间进行相互转换的方式。

Exercise 1

boot_alloc

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
// This simple physical memory allocator is used only while JOS is setting
// up its virtual memory system. page_alloc() is the real allocator.
//
// If n>0, allocates enough pages of contiguous physical memory to hold 'n'
// bytes. Doesn't initialize the memory. Returns a kernel virtual address.
//
// If n==0, returns the address of the next free page without allocating
// anything.
//
// If we're out of memory, boot_alloc should panic.
// This function may ONLY be used during initialization,
// before the page_free_list list has been set up.
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(npages * PGSIZE < (uint32_t)(nextfree + n - KERNBASE)) // out of memory
panic("boot_alloc: We are out of memory.\n");
result = nextfree;
nextfree = ROUNDUP(nextfree + n, PGSIZE);

return result;
}

从34行开始为补充的代码部分,nextfree作为static变量只会初始化一次,用来表示往后第一个没有被分配的virtual address。

由于在注释中要求要对于对于out of memory的情况需要触发panic,所以这里在34行进行了一个分配内容是否超过物理内存限制的检查。如果一切正常的话就进行分配,采用已经定义好的ROUNDUP宏来进行页面对齐。

如果n为0的时候,37行代码不会产生任何改变,符合注释中所描述的代码逻辑。

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(struct PageInfo)*npages);

这里所需要进行的就是对于pages这样一个存储PageInfo的数组进行空间分配,并且初始化为0。首先先利用boot_alloc进行空间的分配,之后利用memset进行清零就可以了。

这里可以看到,pages实际上就是在页目录之后进行分配的一串物理地址。

page_init

在memlayout.h中可以看到

1
2
3
4
5
// At IOPHYSMEM (640K) there is a 384K hole for I/O.  From the kernel,
// IOPHYSMEM can be addressed at KERNBASE + IOPHYSMEM. The hole ends
// at physical address EXTPHYSMEM.
#define IOPHYSMEM 0x0A0000
#define EXTPHYSMEM 0x100000

IOPHYSMEM对应的是640K的位置,EXTPHYSMEM对应的是1M的位置。在lab1当中内核代码就是被加载到了1M的后面,之后再之前的mem_init()当中,我们又在上面进行了pages的分配,当前可以使用的free空间应当是从之前分配的内容后面开始。boot_alloc()返回的是一个kernel virtual address,需要将其转换得到对应的physical address

从pmap.h文件中可以看到从PA向KVA的转换如下:

1
2
3
4
5
6
7
8
9
10
11
/* This macro takes a physical address and returns the corresponding kernel
* virtual address. It panics if you pass an invalid physical address. */
#define KADDR(pa) _kaddr(__FILE__, __LINE__, pa)

static inline void*
_kaddr(const char *file, int line, physaddr_t pa)
{
if (PGNUM(pa) >= npages)
_panic(file, line, "KADDR called with invalid pa %08lx", pa);
return (void *)(pa + KERNBASE);
}

那么从KVA向PA的转换只需要进行一个逆操作。

最后的代码如下:

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
//
// Initialize page structure and memory free list.
// After this is done, NEVER use boot_alloc again. ONLY use the page
// allocator functions below to allocate and deallocate physical
// memory via the page_free_list.
//
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;
uint32_t pa_free_start = (uint32_t)((char *)boot_alloc(0) - KERNBASE);
// case 1:
pages[0].pp_ref = 1;
pages[0].pp_link = NULL;
// case 2, 3, 4:
for (i = 1; i < npages; i++) {
if(IOPHYSMEM <= i * PGSIZE && i * PGSIZE < pa_free_start)
{
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];
}
}
}

pa_free_start表示,这个物理地址后面的空间在当前都是空闲的,所以3和4的一部分都需要被设置成已经分配的状态。剩下的内容都被设置成空闲页面,加入到page_free_list当中。

page_alloc

这里完成的是对于页面的分配,根据alloc_flags来判断是否对于页面进行初始化。如果进行分配的话,那么就将page_free_list的头一个页面取出进行分配即可,初始化利用page2kva得到对应的地址,然后进行初始化操作。

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
//
// Allocates a physical page. If (alloc_flags & ALLOC_ZERO), fills the entire
// returned physical page with '\0' bytes. Does NOT increment the reference
// count of the page - the caller must do these if necessary (either explicitly
// or via page_insert).
//
// Be sure to set the pp_link field of the allocated page to NULL so
// page_free can check for double-free bugs.
//
// Returns NULL if out of free memory.
//
// Hint: use page2kva and memset
struct PageInfo *
page_alloc(int alloc_flags)
{
// Fill this function in
struct PageInfo* alloc_page = page_free_list;
if(alloc_page == NULL)
return NULL;
page_free_list = alloc_page->pp_link;
alloc_page->pp_link = NULL;
if(alloc_flags && ALLOC_ZERO)
memset(page2kva(alloc_page), 0, PGSIZE);
return alloc_page;
}

page_free

这里做的操作是释放页面,将页面插入page_free_list的头部就可以,但是首先需要检查是否pp_ref为0且pp_link为_NULL,前者不为0表示对于仍在使用的页面进行了释放的操作,后者不为NULL说明它本身就已经是被释放的页面,进行了double free的操作,都要触发panic。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//
// 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("page_free: pp_ref or pp_link is not zero!\n");
pp->pp_link = page_free_list;
page_free_list = pp;
return;
}

到这里可以得到如下的输出内容

1
2
check_page_free_list() succeeded!
check_page_alloc() succeeded!

说明关于page_free_list的维护和page_alloc的操作都没有问题。

Exercise 2

主要是关于Intel 80386手册的描述,其中第五章主要是对分段机制的描述,第六章是对分页机制的描述。由于在JOS当中将整个空间看做一个段,所以段偏移量就是线性地址,只需要明白关于分页机制以及对应的线性地址转换到物理地址的过程就好。

Exercise 3

主要是GDB和QEMU命令的熟悉。

Exercise 4

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 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.
//
pte_t *
pgdir_walk(pde_t *pgdir, const void *va, int create)
{
// Fill this function in
uint32_t pdx = PDX(va);
uint32_t ptx = PTX(va);
if(pgdir[pdx] == 0)
{
if(create)
{
struct PageInfo* newpte = page_alloc(1);
if(newpte == NULL)
return NULL;
++(newpte->pp_ref);
pgdir[pdx] = page2pa(newpte) | PTE_P | PTE_W | PTE_U;
}
else
return NULL;
}
physaddr_t pte = PTE_ADDR(pgdir[pdx]) | (ptx << 2);
return KADDR(pte);
}

pgdir_walk()函数做的内容实际上是通过虚拟地址va来进行一个地址翻译,找到所对应的pte。这里传入的三个参数,pgdir是一个指向页目录基址的指针,va是要进行翻译的虚拟地址,create是一个标志,如果非0说明对于对应的va不存在pte的话需要进行分配。

那么首先检查就是页目录中对应的页表到底存在不存在,如果存在的话,直接取出然后进行pte的计算。那么如果不存在的话,就需要page_alloc来分配一个物理页面用来存储页表,并且将该物理页面的引用添加,之后由于关于权限的确认在后面的pte项当中也会进行,所以这里关于页表就可以直接提供全部的权限,将其填入对应的页目录的项中。

那创建了页表之后,就如同之前一样进行进一步的地址转换。但是由于这里物理地址是不能直接进行解引用操作的,所以利用KADDR宏将得到的物理地址转换成remap过的虚拟地址,这样可以通过解引用来获得对应的物理地址也能对于所存储的内容进行修改。

boot_map_region

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
//
// 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
static void
boot_map_region(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm)
{
// Fill this function in
while(size > 0)
{
pte_t* pte = pgdir_walk(pgdir, (void *)va, 1);
if(pte == NULL)
panic("boot_map_region: Fail to alloc new page, run out of memory!\n");
*pte = pa | perm | PTE_P;
size -= PGSIZE;
va += PGSIZE, pa += PGSIZE;
}
}

这个函数的作用是将一串连续的虚拟地址映射到一串连续的物理地址,其中映射的地址的大小是页面大小的整数倍。那么可以知道,直接的想法就是通过虚拟地址进行地址查询,然后将页表中对应的表项修改为映射到的物理地址就可以了。那么以每个页面单位来进行这样的操作。

首先通过pgdir_walk()来找到虚拟地址对应的表项,如果对应的二级页表不存在那么就进行空间的分配,如果分配失败则进行报错,出发一个panic。

之后就将物理地址以及对应的权限填到表项里面,然后对下一个需要映射的页进行相同的操作。

page_lookup

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//
// 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.
//
struct PageInfo *
page_lookup(pde_t *pgdir, void *va, pte_t **pte_store)
{
// Fill this function in
pte_t* pte = pgdir_walk(pgdir, va, 0);
if(pte == NULL)
return NULL;
physaddr_t pa = PTE_ADDR(*pte);
if(pte_store)
*pte_store = pte;
return pa2page(pa);
}

这个地方的page_lookup()想要做的是通过虚拟地址va来查找对应的映射页的PageInfo结构,这边的操作就是首先去找pte,如果找到说明该虚拟地址被映射到了一个页面,得到映射页面的物理页面首地址,再通过pa2page()完成转换。那么如果没有找到的话,说明这个虚拟地址并没有映射到任何物理页面。如果传入的pte_store非空的话,就将其进行保存。

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
//
// 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
pte_t* pte_store;
struct PageInfo* pp = page_lookup(pgdir, va, &pte_store);
if(pp == NULL)
return;
*pte_store = 0;
page_decref(pp);
tlb_invalidate(pgdir, va);
}

page_remove()所做的操作是将va映射到的物理页面给取消映射。要完成remove的操作需要做两件事情,一个就是将页表项中的对应内容给修改,另外一个就是对于PageInfo结构的修改,需要将其引用数减少,如果引用数为0,那么就将其加入空闲链表。在25行处的page_decref()函数做的实际上就是上述这个减少引用的操作。

那么一开始利用page_lookup()来找到对应的页面和pte,在24行修改pte,在25行修改链表结构,在26行调用tlb_invalidate()函数把TLB里面的内容给标注为无效。

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
29
30
31
32
33
34
35
36
37
38
39
//
// Map the physical page 'pp' at virtual address 'va'.
// The permissions (the low 12 bits) of the page table entry
// should be set to 'perm|PTE_P'.
//
// Requirements
// - If there is already a page mapped at 'va', it should be page_remove()d.
// - If necessary, on demand, a page table should be allocated and inserted
// into 'pgdir'.
// - pp->pp_ref should be incremented if the insertion succeeds.
// - The TLB must be invalidated if a page was formerly present at 'va'.
//
// Corner-case hint: Make sure to consider what happens when the same
// pp is re-inserted at the same virtual address in the same pgdir.
// However, try not to distinguish this case in your code, as this
// frequently leads to subtle bugs; there's an elegant way to handle
// everything in one code path.
//
// RETURNS:
// 0 on success
// -E_NO_MEM, if page table couldn't be allocated
//
// Hint: The TA solution is implemented using pgdir_walk, page_remove,
// and page2pa.
//
int
page_insert(pde_t *pgdir, struct PageInfo *pp, void *va, int perm)
{
// Fill this function in
pte_t* pte = pgdir_walk(pgdir, va, 1);
if(pte == NULL)
return -E_NO_MEM;
physaddr_t pa = page2pa(pp);
++(pp->pp_ref);
if(*pte)
page_remove(pgdir, va);
*pte = pa | perm | PTE_P;
return 0;
}

page_insert()的操作就是将va映射到pp所指向的物理页面上去,而对应的权限通过perm来进行表示。那么利用pgdir_walk()函数来获得pte, 如果没有就进行创建。那么这个情况下如果返回为NULL,只有可能是空间不足无法创建,于是返回-E_NO_MEM。那么如果能够得到pte,就对于对应的物理页面进行处理,添加引用数,然后把本来pte可能存在的映射关系给消除,之后再进行映射。

这里存在的一个问题是,如果我这里提供的pp本来就是va映射的对象,可能会出现问题。考虑将34行进行引用数增加的内容移到36行后面,那么他首先进行了page_remove。如果之前的引用数为1,那这个页面将被加入空闲链表。而之后再给他加了一个引用数,这就相当于空闲链表中存在着不空闲的页面,他可能会被二次分配。存在一个bug。而将该语句保存在34行的位置,就可以确保remove之后,如果本来就是va映射的页面,也不会被加入到空闲链表中,规避了之前所说的那种bug的出现。

Exercise 5

1
2
3
4
5
6
7
8
9
// 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:
n = ROUNDUP(npages*sizeof(struct PageInfo), PGSIZE);
for (i = 0; i < n; i += PGSIZE)
page_insert(kern_pgdir, pa2page(PADDR(pages) + i), (void *)(UPAGES + i), PTE_U | PTE_P);

这里是要将pages个映射到UPAGES以上的内容,那么这里要考虑到pages这整个内容实际上是对应着许多PageInfo结构的,在进行映射的同时需要对于PageInfo内部的引用数进行修改,这里采用一个for循环将所有页面依次进行映射。权限位由于在注释中说明,需要内核和用户都可读,所以标注成PTE_U|PTE_P。

1
2
3
4
5
6
7
8
9
10
11
// 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);

这里进行的是一个连续地址的映射,完成的是内核栈的一个映射。这个地方被划分成了[KSTACKTOP-KSTKSIZE, KSTACKTOP)[KSTACKTOP-PTSIZE, KSTACKTOP-KSTKSIZE)两个部分,通过注释内容可以知道,前一段是需要映射到物理地址的,后一段是不需要的。所以我们要做的只是将前一段进行映射。这里bootstack是已经知道的,通过PADDR将其转换为物理地址,然后映射以KSTACKTOP-KSTKSIZE为起点,KSTKSIZE大小的内容。权限由于在boot_map_region()当中会自动加上PTE_P,所以这里只要标注PTE_W。

1
2
3
4
5
6
7
8
9
// 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:
//cprintf("kernbase: %x 2^32-kernbase: %x", KERNBASE, (~KERNBASE)+1);
boot_map_region(kern_pgdir, KERNBASE, (~KERNBASE) + 1, 0, PTE_W);

同样的是一个进行简单的连续地址映射的操作,那么这个地方也是采用boot_map_region()来进行,但是这里需要得到2^32,而32位大小是表示不出这么大的数的,所以这里采用(~KERNBASE)+1来得到需要进行映射的大小。这里的权限同样由于boot_map_region()会自动加上PTE_P,所以只需要标注PTE_W就可以了。

到这里为止,通过执行

1
make grade

可以得到如下的结果:

1
2
3
4
5
6
running JOS: (2.8s)
Physical page allocator: OK
Page management: OK
Kernel page directory: OK
Page management 2: OK
Score: 70/70

说明已经满足所有check函数的需求,完成了虚拟内存系统的一个初始化。

Questions:

这里的value应当是一个虚拟地址,在程序里面,并不能直接对于物理地址进行操控,所有的指针都应当是虚拟地址。

这里需要注意的是:JOS将从0开始的所有物理内存映射到虚拟地址0xf0000000就是为了让内核能够读写只知道物理地址的内容。那么为了完成从物理地址到虚拟地址的转换,对于只知道物理地址的,就将其物理地址加上0xf0000000,就可以得到对应的虚拟地址了。利用定义好的宏KADDA(pa)可以做到,而宏PADDA(va)就是这个的逆操作。在Exercise4当中这两个宏能够有效地进行虚拟地址物理地址之间的转换,从而使的解引用等操作可以进行执行。

  • 表格如下:
Entry Base Virtual Address Points to (logically)
960 0xf0000000 以上映射到物理地址从0开始的位置
959 0xefff8000 内核栈
958 0xef800000 页表(UVPT)
957 0xef400000 pages数组(UPAGES)
. . .
0 0x00000000 [see next question]
  • We have placed the kernel and user environment in the same address space. Why will user programs not be able to read or write the kernel’s memory? What specific mechanisms protect the kernel memory?

因为在表项中存在权限为,只有PTE_U被设置成1的时候才可以让user访问,kernel memory只需要修改权限为就可以不被user读写。

  • What is the maximum amount of physical memory that this operating system can support? Why?

在kern/pmap.c中的mem_init()函数中可以看到将pages数组映射到了线性地址的UPAGES上方。那么在inc/memlayout.h的图中可以看到,给只读的pages数组分配的空间为4M大小(一个PTSIZE)。

1
2
3
*    UVPT      ---->  +------------------------------+ 0xef400000
* | RO PAGES | R-/R- PTSIZE
* UPAGES ----> +------------------------------+ 0xef000000

一个PageInfo的大小是8Byte,一个页面的大小是4K。所以可以得到4M的pages数组对应的物理内存大小是:

422084210=2230\frac{4*2^{20}}{8}*4*2^{10} = 2*2^{30}

即操作系统能够支持的物理内存大小不会超过2G,理由如上。

  • How much space overhead is there for managing memory, if we actually had the maximum amount of physical memory? How is this overhead broken down?

如果所有虚拟地址都被映射的话,那么页表的开销,一级页表需要1个page,二级页表需要1024个page。总共需要1025个page。所以页表上的开销为10254=4100KB1025*4=4100KB

采用大页可以减少开销,这样只需要一级页表就可以进行索引,需要一个page也就是4KB就可以了。

  • Revisit the page table setup in kern/entry.S and kern/entrypgdir.c. Immediately after we turn on paging, EIP is still a low number (a little over 1MB). At what point do we transition to running at an EIP above KERNBASE? What makes it possible for us to continue executing at a low EIP between when we enable paging and when we begin running at an EIP above KERNBASE? Why is this transition necessary?

在27行处利用jmp *%eax进行了跳转,完成了在高地址执行的转换。

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
.globl entry
entry:
movw $0x1234,0x472 # warm boot

# We haven't set up virtual memory yet, so we're running from
# the physical address the boot loader loaded the kernel at: 1MB
# (plus a few bytes). However, the C code is linked to run at
# KERNBASE+1MB. Hence, we set up a trivial page directory that
# translates virtual addresses [KERNBASE, KERNBASE+4MB) to
# physical addresses [0, 4MB). This 4MB region will be
# sufficient until we set up our real page table in mem_init
# in lab 2.

# Load the physical address of entry_pgdir into cr3. entry_pgdir
# is defined in entrypgdir.c.
movl $(RELOC(entry_pgdir)), %eax
movl %eax, %cr3
# Turn on paging.
movl %cr0, %eax
orl $(CR0_PE|CR0_PG|CR0_WP), %eax
movl %eax, %cr0

# Now paging is enabled, but we're still running at a low EIP
# (why is this okay?). Jump up above KERNBASE before entering
# C code.
mov $relocated, %eax
jmp *%eax
relocated:

# Clear the frame pointer register (EBP)
# so that once we get into debugging C code,
# stack backtraces will be terminated properly.
movl $0x0,%ebp # nuke frame pointer

# Set the stack pointer
movl $(bootstacktop),%esp

# now to C code
call i386_init

同时在低EIP和高EIP访问的原因是,我们将虚拟地址的[0, 4MB)和[KERNBASE, KERNBASE+4MB)都映射到了物理地址的[0, 4MB),所以无论从低地址还是高地址都可以进行访问。

1
2
3
4
5
6
7
8
9
__attribute__((__aligned__(PGSIZE)))
pde_t entry_pgdir[NPDENTRIES] = {
// Map VA's [0, 4MB) to PA's [0, 4MB)
[0]
= ((uintptr_t)entry_pgtable - KERNBASE) + PTE_P,
// Map VA's [KERNBASE, KERNBASE+4MB) to PA's [0, 4MB)
[KERNBASE>>PDXSHIFT]
= ((uintptr_t)entry_pgtable - KERNBASE) + PTE_P + PTE_W
};

从前面的内容可以看到,他在完成分页之后还有在低地址执行的语句,如果不同时将高地址和低地址都映射到物理地址的最低4M的话,那么在低地址运行的代码会出错。

Challenge

可以在inc/mmu.h当中找到关于PTE/PDE flag的描述,具体内容如下:

1
2
3
4
5
6
7
8
9
10
// Page table/directory entry flags.
#define PTE_P 0x001 // Present
#define PTE_W 0x002 // Writeable
#define PTE_U 0x004 // User
#define PTE_PWT 0x008 // Write-Through
#define PTE_PCD 0x010 // Cache-Disable
#define PTE_A 0x020 // Accessed
#define PTE_D 0x040 // Dirty
#define PTE_PS 0x080 // Page Size
#define PTE_G 0x100 // Global

第九行所示的就是PTE_PS位,是用来调整Page Size大小的。

采用大页只有一级页表,对应的地址翻译方式如下:

image-20200310182755807

通过Intel IA32手册3.6.1节关于Page Option的描述可以知道,需要开启cr4里面的PSE标志位,来说明提供对于大页的支持,在mem_init()当中添加如下代码进行实现:

1
2
3
4
// Set CR4_PSE
cr4 = rcr4();
cr4 |= CR4_PSE;
lcr4(cr4);

考虑到lab整体要对于这种大小页混合的方式进行适配的话,需要对于页面相关的许多函数进行重写。所以这里只考虑虚拟地址高256M到物理地址低256M的映射采用大页实现,只对于boot_map_region及其相关函数进行修改。

修改pgdir_walk()函数如下,其中normal状态是针对仅存在4K大小页的情况,而ex表示的是大小页混合状态的情况:

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
pte_t *
pgdir_walk_normal(pde_t *pgdir, const void *va, int create)
{
// Fill this function in
uint32_t pdx = PDX(va);
uint32_t ptx = PTX(va);
if(pgdir[pdx] == 0)
{
if(create)
{
struct PageInfo* newpte = page_alloc(1);
if(newpte == NULL)
return NULL;
++(newpte->pp_ref);
pgdir[pdx] = page2pa(newpte) | PTE_P | PTE_W | PTE_U;
}
else
return NULL;
}
physaddr_t pte = PTE_ADDR(pgdir[pdx]) | (ptx << 2);
return KADDR(pte);
}

pte_t *
pgdir_walk_ex(pde_t *pgdir, const void *va, int create)
{
uint32_t pdx = PDX(va);
if(pgdir[pdx] == 0)
{
if(create == 1)
{
struct PageInfo* newpte = page_alloc(1);
if(newpte == NULL)
return NULL;
++(newpte->pp_ref);
pgdir[pdx] = page2pa(newpte) | PTE_P | PTE_W | PTE_U;
}
else if(create == 2)
{
pgdir[pdx] = PTE_PS;
}
else
return NULL;
}
else if(create == 2 && (!(pgdir[pdx] & PTE_PS)))
{
struct PageInfo * pp = pa2page(PTE_ADDR(pgdir[pdx]));
page_decref(pp);
tlb_invalidate(pgdir, (void*)va);
pgdir[pdx] = PTE_PS;
}
uint32_t pde = pgdir[pdx];
if(pde & PTE_PS)
{
return pgdir + pdx;
}
else
{
return KADDR(PTE_ADDR(pgdir[pdx]) | (PTX(va) << 2));
}
}

pte_t *
pgdir_walk(pde_t *pgdir, const void *va, int create)
{
uint32_t size_ex = rcr4() & CR4_PSE;
if(size_ex)
return pgdir_walk_normal(pgdir, va, create);
else
return pgdir_walk_ex(pgdir, va, create);
}

这里仅仅对ex函数进行讨论,首先create可能为0、1或者2,不同于normal函数只存在0、1两种情况。0的时候表示不进行额外的分配。1的情况表示是一个小页,2的情况表示是一个大页,只要非0都是表示若不存在则进行分配。

那么如果当前当做一个大页的话,进行分配的情况不需要再去分配一个页面作为二级页表,只需要标记PTE_PS位返回填充对应的物理地址基址就好了。但是存在一个情况在于,原本这个pde指向的是一个二级页表,但是当前是采用大页进行分配的。所以在45行处有针对这种情况的特判。需要做的是将对应的二级页表的页面给清空,然后当做一个新分配的大页进行返回就可以了。

之后考虑的是boot_map_region()函数,同样重写:

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
static void
boot_map_region_normal(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm)
{
// Fill this function in
while(size > 0)
{
pte_t* pte = pgdir_walk(pgdir, (void *)va, 1);
if(pte == NULL)
panic("boot_map_region: Fail to alloc new page, run out of memory!\n");
*pte = pa | perm | PTE_P;
size -= PGSIZE;
va += PGSIZE, pa += PGSIZE;
}
}

static void
boot_map_region_ex(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm)
{
while(size > 0)
{
pte_t* pte = pgdir_walk_ex(pgdir, (void *)va, 2);
if(pte == NULL)
panic("boot_map_region: Fail to alloc new page, run out of memory!\n");
*pte = pa | perm | PTE_P | PTE_PS;
size -= PTSIZE;
va += PTSIZE, pa+= PTSIZE;
}
}

static void
boot_map_region(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm)
{
uint32_t size_ex = rcr4() & CR4_PSE;
if(size_ex)
{
if(ROUNDUP(pa, PTSIZE) < ROUNDDOWN(pa+size, PTSIZE))
{
boot_map_region_normal(pgdir, va, ROUNDUP(pa, PTSIZE) - pa, pa, perm);
boot_map_region_ex(pgdir, va+ROUNDUP(pa, PTSIZE)-pa, ROUNDDOWN(pa+size, PTSIZE) - ROUNDUP(pa, PTSIZE), ROUNDUP(pa, PTSIZE), perm);
boot_map_region_normal(pgdir, va+ROUNDDOWN(pa+size, PTSIZE)-pa, pa+size - ROUNDDOWN(pa+size, PTSIZE), ROUNDDOWN(pa+size, PTSIZE), perm);
}
else
boot_map_region_normal(pgdir, va, size, pa, perm);
}
else
boot_map_region_normal(pgdir, va, size, pa, perm);
}

这里boot_map_region也被分成了两种情况,normal表示的是以4K为一个页面进行映射构造,ex表示的是以4M为一个页面大小进行映射构造。

在36行处的判断说明,仅当cr4被标识成拓展的页面大小且进行映射的区间内存在连续的4M空间的时候,对可以采用大页进行分配的部分采用大页,即调用boot_map_region_ex()函数。所以像是内核栈这种大小只有几十K的映射,在cr4设置之后仍然是采用原有的小页方法进行映射的。

这个时候采用原有的check函数会产生错误,原因在于原有的check函数所进行的地址转换方法是二级的。

重写原来给定的va2pa函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static physaddr_t
check_va2pa(pde_t *pgdir, uintptr_t va)
{
pte_t *p;
pgdir = &pgdir[PDX(va)];
if(!(*pgdir & PTE_P))
return ~0;
if((*pgdir & PTE_PS))
{
return ((*pgdir) & 0xffc00000) | (va & 0x3ff000);
}
else
{
p = (pte_t*) KADDR(PTE_ADDR(*pgdir));
if (!(p[PTX(va)] & PTE_P))
return ~0;
return PTE_ADDR(p[PTX(va)]);
}

}

根据PTE_PS标志位来决定采用一级寻址还是二级寻址,这样就可以得到正常的结果:

1
2
3
4
5
6
7
8
check_page_free_list() succeeded!
check_page_alloc() succeeded!
check_page() succeeded!
check_kern_pgdir() succeeded!
check_page_installed_pgdir() succeeded!
Welcome to the JOS kernel monitor!
Type 'help' for a list of commands.
K>

这里在清空cr4之后都是采用normal的方式来进行映射和寻址,所以会保持和原来相同的行为。

Challenge2

showmappings

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
int 
mon_showmappings(int argc, char **argv, struct Trapframe *tf)
{
if(argc != 3)
{
cprintf("showmappings: should input 3 arguments!\n");
return 0;
}
uint32_t lowerbound = strtol(argv[1], '\0', 16);
uint32_t upperbound = strtol(argv[2], '\0', 16);
uint32_t va;
cprintf("Virtual Address\tPhysical Address\turw\n");
for(va = ROUNDDOWN(lowerbound, PGSIZE); va <= ROUNDUP(upperbound, PGSIZE); va += PGSIZE)
{

pte_t * pte = pgdir_walk(kern_pgdir, (void *)va, 0);
if(pte && ((*pte) & PTE_P))
{
physaddr_t pa = PTE_ADDR(*pte);
char perm_U = ((*pte) & PTE_U) ? 'u' : '-';
char perm_P = ((*pte) & PTE_P) ? 'r' : '-';
char perm_W = ((*pte) & PTE_W) ? 'w' : '-';
cprintf(" 0x%08x\t 0x%08x\t%c%c%c\n" , va, pa, perm_U, perm_P, perm_W);
}
else
cprintf(" 0x%08x\t 0x--------\t---\n", va);
}
return 0;
}

代码如上,可以采用如下形式进行[start_va, end_va]区间内虚拟地址到物理地址页面映射的查询:

1
showmappings <start_va> <end_va>

结果如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
K> showmappings 0xefff0000 0xf0000000
Virtual Address Physical Address urw
0xefff0000 0x-------- ---
0xefff1000 0x-------- ---
0xefff2000 0x-------- ---
0xefff3000 0x-------- ---
0xefff4000 0x-------- ---
0xefff5000 0x-------- ---
0xefff6000 0x-------- ---
0xefff7000 0x-------- ---
0xefff8000 0x00117000 -rw
0xefff9000 0x00118000 -rw
0xefffa000 0x00119000 -rw
0xefffb000 0x0011a000 -rw
0xefffc000 0x0011b000 -rw
0xefffd000 0x0011c000 -rw
0xefffe000 0x0011d000 -rw
0xeffff000 0x0011e000 -rw
0xf0000000 0x00000000 -rw

setperm

提供权限位的设置方法,代码如下:

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
int
mon_setperm(int argc, char **argv, struct Trapframe * tf)
{
if(argc == 3)
{
uint32_t va = strtol(argv[1], '\0', 16);
int perm_U = 0;
int perm_P = 0;
int perm_W = 0;
int i;
for(i = 0; argv[2][i]; ++i)
{
if(argv[2][i] == 'u')
perm_U = 1;
else if(argv[2][i] == 'p')
perm_P = 1;
else if(argv[2][i] == 'w')
perm_W = 1;
}
pte_t * pte = pgdir_walk(kern_pgdir, (void *)va, 0);
if(pte)
{
if(perm_U)
*pte = (*pte) | PTE_U;
if(perm_P)
*pte = (*pte) | PTE_P;
if(perm_W)
*pte = (*pte) | PTE_W;
}
else
{
cprintf("The virtual address 0x%08x is unmapped\n", va);
}
}
else if(argc == 4)
{
uint32_t lowerbound = strtol(argv[1], '\0', 16);
uint32_t upperbound = strtol(argv[2], '\0', 16);
int perm_U = 0;
int perm_P = 0;
int perm_W = 0;
int i, va;
for(i = 0; argv[3][i]; ++i)
{
if(argv[3][i] == 'u')
perm_U = 1;
else if(argv[3][i] == 'p')
perm_P = 1;
else if(argv[3][i] == 'w')
perm_W = 1;
}
for(va = ROUNDDOWN(lowerbound, PGSIZE); va <= ROUNDUP(upperbound, PGSIZE); va += PGSIZE)
{
pte_t * pte = pgdir_walk(kern_pgdir, (void *)va, 0);
if(pte)
{
if(perm_U)
*pte = (*pte) | PTE_U;
if(perm_P)
*pte = (*pte) | PTE_P;
if(perm_W)
*pte = (*pte) | PTE_W;
}
else
{
cprintf("The virtual address 0x%08x is unmapped\n", va);
}
}
}
else
{
cprintf("setperm: should give one address or an address range!\n");
}
return 0;
}

支持输入单个虚拟地址对对应的页表项进行更改,或者对一个虚拟地址区间进行修改:

1
2
setperm <va> <perm>
setperm <start_va> <end_va> <perm>

样例如下:

1
2
3
4
5
6
7
8
9
K> showmappings 0xf0000000 0xf0001000
Virtual Address Physical Address urw
0xf0000000 0x00000000 -rw
0xf0001000 0x00001000 -rw
K> setperm 0xf0000000 u
K> showmappings 0xf0000000 0xf0001000
Virtual Address Physical Address urw
0xf0000000 0x00000000 urw
0xf0001000 0x00001000 -rw

可以发现确实对权限位进行了设置。

clearperm

对权限位进行清空,整体框架和setperm相同,只需要将修改部分的代码改成:

1
2
3
4
5
6
if(perm_U)
*pte = (*pte) & (~PTE_U);
if(perm_P)
*pte = (*pte) & (~PTE_P);
if(perm_W)
*pte = (*pte) & (~PTE_W);

测试结果如下:

1
2
3
4
5
6
7
8
9
K> showmappings 0xf0000000 0xf0001000
Virtual Address Physical Address urw
0xf0000000 0x00000000 -rw
0xf0001000 0x00001000 -rw
K> clearperm 0xf0000000 w
K> showmappings 0xf0000000 0xf0001000
Virtual Address Physical Address urw
0xf0000000 0x00000000 -r-
0xf0001000 0x00001000 -rw

changeperm

对权限位进行修改,整体框架和setperm相同,只需要将修改部分的代码改成:

1
2
3
4
5
6
if(perm_U)
*pte = (*pte) ^ PTE_U;
if(perm_P)
*pte = (*pte) ^ PTE_P;
if(perm_W)
*pte = (*pte) ^ PTE_W;

测试结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
K> showmappings 0xf0000000 0xf0001000
Virtual Address Physical Address urw
0xf0000000 0x00000000 -rw
0xf0001000 0x00001000 -rw
K> changeperm 0xf0000000 w
K> showmappings 0xf0000000 0xf0001000
Virtual Address Physical Address urw
0xf0000000 0x00000000 -r-
0xf0001000 0x00001000 -rw
K> changeperm 0xf0000000 w
K> showmappings 0xf0000000 0xf0001000
Virtual Address Physical Address urw
0xf0000000 0x00000000 -rw
0xf0001000 0x00001000 -rw

content

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
int
mon_content(int argc, char **argv, struct Trapframe *tf)
{
if(argc != 4)
{
cprintf("content: type \"help\" to see the example!\n");
return 0;
}
physaddr_t pa;
if(argv[1][1] == 'v')
{
uint32_t base_va = strtol(argv[2], '\0', 16);
pte_t * pte = pgdir_walk(kern_pgdir, (void *)base_va, 0);
pa = PTE_ADDR(*pte) | PGOFF(base_va);
}
else if(argv[1][1] == 'p')
{
pa = strtol(argv[2], '\0', 16);
}
else
{
cprintf("content: -p means physical address, -v means virtual address!\n");
return 0;
}
uint32_t count = strtol(argv[3], '\0', 10);
int check;
for(check = 0; count > 0; --count, ++check)
{
if(check == 0)
cprintf("0x%08x:", pa);
cprintf(" 0x%08x", *(uint32_t*)(KADDR(pa)));
pa += 4;
if(check == 3)
{
check = -1;
cprintf("\n");
}
}
if(check)
cprintf("\n");
return 0;
}

代码如上,用来查看虚拟地址或者物理地址对应的具体内容,可以利用如下的命令形式进行查询:

1
2
content -p <pa> <number>
content -v <va> <number>

其中利用-p或者-v来表明查询的是物理地址还是虚拟地址,number表示要查询的多少,示例结果如下:

1
2
3
4
5
6
K> content -p 0x0 8
0x00000000: 0xf000ff53 0xf000ff53 0xf000e2c3 0xf000ff53
0x00000010: 0xf000ff53 0xf000ff54 0xf000ff53 0xf000ff53
K> content -v 0xf0000000 8
0x00000000: 0xf000ff53 0xf000ff53 0xf000e2c3 0xf000ff53
0x00000010: 0xf000ff53 0xf000ff54 0xf000ff53 0xf000ff53

之前可以知道,KERNBASE以上的虚拟地址映射到的是从零开始的虚拟地址,所以上面得到的结果是完全相同的。利用qemu的指令进行检查:

1
2
3
(qemu) xp /8x 0x0
0000000000000000: 0xf000ff53 0xf000ff53 0xf000e2c3 0xf000ff53
0000000000000010: 0xf000ff53 0xf000ff54 0xf000ff53 0xf000ff53

可以发现得到的结果完全相同,说明指令运行没有问题。

Challenge3&4

没写代码,感觉两个Challenge是递进的关系。challenge3可以考虑只保存包括内核自身的页目录,以及内核栈地址用来往内核栈写入参数保存信息,中断向量表等陷入内核态需要的信息。这样可能只需要几个page就足够了。陷入内核之后通过内核自身的页目录来完成地址映射进行寻址以及执行。这几个和内核相关的页面都应该是内核可读写,用户没有权限。

之后Challenge4由于Challenge3已经将内核相关的地址空间大小缩小了。如果进程想要对于这些地址进行分配的话,那么由于权限不够,会触发异常。处理的手段就是将这部分内容放到暂时还没有使用的地址,并且对相应的地址链接等内容进行修改,然后再次进行分配操作。我感觉这可能是bouncing kernel的意思,找了很久也没有找到bouncing kernel相关的资料或者论文。由于在challenge3当中把需要内核相关的内容缩小到了几个page,所以就可以大大减少需要触发弹跳机制的频率,降低为了更大地址空间所带来的额外时间开销。

Challenge5

我觉得可以考虑采用类似ICS中malloc lab里面的方法,在PageInfo里面加入前后page的链接以及这个连续页面的大小(是PGSIZE的整数倍)。之后利用first-fit或者best-fit的方式进行适配,删除的时候考虑前后的合并。这样应该就可以完成连续地址的分配。对于比较大的连续分配还可以结合Challenge1当中的大页从而节省掉二级页表的空间。

题面中所说的"power-of-two allocation unit sizes from 4KB up to some reasonable maximum of your choice."应该就是伙伴系统了。感觉要完全实现除去修改自己写的函数之外需要修改check_page_free_list()以及kern/pmap.h当中的宏以及辅助函数,不知道会不会引发什么其他地方未知的错误,没有进行代码实现。