Introduction

该 lab 主要需要编写操作系统的内存管理部分。内存管理分为两个部分:

内核的物理内存分配器 (physical memory allocator)

使得内核可以分配、释放内存。该分配器以页为单位,JOS 中一页是 4kB。本次 lab 的任务是维护一个数据结构,该数据结构记录了物理内存分配与释放,以及多少个进程正在共享各个已分配的页。

虚拟内存 (virtual memory)

将内核和用户程序使用的虚拟地址映射到物理内存的地址中。x86 的内存管理单元 (MMU) 会在指令用到内存时完成这个映射,查询一系列页表。

在 lab2 中,新加入了几个源文件:

inc/memlayout.h // 描述虚拟地址空间的布局

kern/pmap.c // 读取物理内存大小,对虚拟地址空间进行布局

kern/pmap.h

kern/kclock.h // 操纵 PC 的时钟以及 CMOS RAM 等设备

kern/kclock.c // 这些设备中记录了物理内存大小

重点需要阅读 memlayout.h 以及 pmap.h,还需参考 inc/mmu.h。

处理冲突

在git merge lab1时,几乎必然出现冲突,以 conf/lab.mk 为例:

~/OS/lab$ more conf/lab.mk

<<<<<<< HEAD

LAB=2

PACKAGEDATE=Wed Sep 21 11:13:24 EDT 2016

=======

LAB=1

PACKAGEDATE=Wed Sep 14 12:18:32 EDT 2016

>>>>>>> lab1

其中,=======是分隔符,容易看出之上属于现在的 HEAD 即 lab2 的内容,之下属于 lab1 的内容,我们显然选择 lab2 的内容。

即手动修改 conf/lab.mk,仅保留:

LAB=2

PACKAGEDATE=Wed Sep 21 11:13:24 EDT 2016

其他冲突文件也如此处理后,直接 git add .,git commit -a提交。

Exercise 1

In the file kern/pmap.c, you must implement code for the following functions (probably in the order given).

boot_alloc()

mem_init() (only up to the call to check_page_free_list(1))

page_init()

page_alloc()

page_free()

**check_page_free_list() and check_page_alloc() test your physical page allocator. **

操作系统必需跟踪哪些物理 RAM 是空闲的,哪些正在使用。这个 exercise 主要编写物理页面分配器。它利用一个 PageInfo 结构体组成的链表记录哪些页面空闲,每个结构体对应一个物理页。因为页表的实现需要分配物理内存来存储页表,在虚拟内存的实现之前,我们需要先编写物理页面分配器。

boot_alloc 函数

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) {

return nextfree;

}

result = nextfree;

nextfree += ROUNDUP(n, PGSIZE);

return result;

}

其中,需要注意的一个是 end 到底是什么,另一个是 ROUNDUP 这个宏。其中,end 指向内核的 bss 段的末尾。利用 objdump -h kernel可以看出,bss 段已经是内核的最后一段。因此,end 指向的是第一个未使用的虚拟内存地址。而 ROUNDUP 定义在 inc/types.h 中。

~/OS/lab/obj/kern$ objdump -h kernel

kernel: file format elf32-i386

Sections:

Idx Name Size VMA LMA File off Algn

0 .text 000019f1 f0100000 00100000 00001000 2**4

CONTENTS, ALLOC, LOAD, READONLY, CODE

1 .rodata 000007f0 f0101a00 00101a00 00002a00 2**5

CONTENTS, ALLOC, LOAD, READONLY, DATA

2 .stab 00004105 f01021f0 001021f0 000031f0 2**2

CONTENTS, ALLOC, LOAD, READONLY, DATA

3 .stabstr 00001be6 f01062f5 001062f5 000072f5 2**0

CONTENTS, ALLOC, LOAD, READONLY, DATA

4 .data 0000a300 f0108000 00108000 00009000 2**12

CONTENTS, ALLOC, LOAD, DATA

5 .bss 00000650 f0112300 00112300 00013300 2**5

ALLOC

6 .comment 00000034 00000000 00000000 00013300 2**0

CONTENTS, READONLY

mem_init 函数

这里需要用到 PageInfo 这个结构体了,首先在 inc/memlayout.h 中找到其定义:

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_ref 表示有多少个指针指向该页,pp_link 表示空闲内存列表中的下一页。注意,非空闲页的 pp_link 总是为 NULL。

mem_init 函数中需要添加以下两行:

//

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

memset(pages, 0, npages * sizeof(struct PageInfo));

需要注意的是分配内存用的是 boot_alloc。这是一个仅用于 JOS 设置自身虚拟内存系统时使用的物理内存分配器,仅用于 mem_init 函数。当初始化页面以及空闲内存列表后,不再使用 boot_alloc,而使用 page_alloc。

page_init 函数

void

page_init(void)

{

// NB: DO NOT actually touch the physical memory corresponding to

// free pages!

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...)

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.

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?

size_t first_free_address = PADDR(boot_alloc(0));

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];

}

}

需要注意的是,下面代码的作用是把页面设为空闲,并插入链表头:

pages[i].pp_ref = 0;

pages[i].pp_link = page_free_list;

page_free_list = &pages[i];

可以在 inc/memlayout.h 中找到 IO hole 的定义,可回顾lab 1:

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

第四种情况略有难度,实际需要利用 boot_alloc 函数来找到第一个能分配的页面。相同的思想在已经写好的check_free_page_list函数中也可以找到。关键代码:

size_t first_free_address = PADDR(boot_alloc(0));

尤其需要注意的是,由于 boot_alloc 返回的是内核虚拟地址 (kernel virtual address),一定要利用 PADDR 转为物理地址。在 kern/pmap.h 中可以找到 PADDR 的定义,实际就是减了一个 F0000000:

/* This macro 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.

*/

// KERNBASE 在 inc/memlayout.h 中被定义为 0xF0000000

#define PADDR(kva) _paddr(__FILE__, __LINE__, kva)

static inline physaddr_t

_paddr(const char *file, int line, void *kva)

{

if ((uint32_t)kva < KERNBASE)

_panic(file, line, "PADDR called with invalid kva %08lx", kva);

return (physaddr_t)kva - KERNBASE;

}

page_alloc 函数

这个函数主要是完成页面的分配。所谓分配是基于 PageInfo,即管理层面的,并没有真正进行内存的分配。更加恰当的说法是标记为已使用。

//

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

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;

}

基本没什么值得说的,按着提示走,不用手动增加引用计数,调用者会做这个事。page2kva 函数的作用就是通过物理页获取其内核虚拟地址。另外分配后的页面需要将 pp_link 指针设置为 NULL。

page_free 函数

释放页面。

//

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

}

唯一需要注意的就是释放后需要加入空闲页列表之中,不用手动将引用清0,调用者会做这件事。

完成以上步骤,编译运行,看到 check_page_alloc() succeeded!则成功。

Exercise 4

Exercise 4. In the file kern/pmap.c, you must implement code for the following functions.

pgdir_walk()

boot_map_region()

page_lookup()

page_remove()

page_insert()

check_page(), called from mem_init(), tests your page table management routines. You should make sure it reports success before proceeding.

这个练习难度就比较高了,首先需要补充一些必需的知识。

虚拟内存

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

虚拟内存主要的好处是:

让每个程序都以为自己独占计算机内存空间,概念清晰,方便程序的编译和装载。

通过将部分内存暂存在磁盘上,可以让程序使用比物理内存大得多的虚拟内存,突破物理内存的限制。

通过对不同进程设置不同页表,可以防止进程访问其他进程的地址空间。通过在不同进程之间映射相同的物理页,又可以提供进程间的共享。

虚拟、线性和物理地址

虚拟地址

最原始的地址,也是 C/C++ 指针使用的地址。由前 16bit 段 (segment) 选择器和后 32bit 段内的偏移 (offset) 组成,显然一个段大小为 4GB。通过虚拟地址可以获得线性地址。

线性地址

前 10bit 为页目录项(page directory entry, PDE),即该地址在页目录中的索引。中间 10bit 为页表项(page table entry, PTE),代表在页表中的索引,最后 12bit 为偏移,也就是每页 4kB。通过线性地址可以获得物理地址。

物理地址

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

在 JOS 中,由于只有一个段,所以虚拟地址数值上等于线性地址。

JOS 内核常常需要读取或更改仅知道物理地址的内存。例如,添加一个到页表的映射要求分配物理内存来存储页目录并初始化内存。然而,内核和其他任何程序一样,无法绕过虚拟内存转换这个步骤,因此不能直接使用物理地址。JOS 将从 0x00000000 开始的物理内存映射到 0xf0000000 的其中一个原因就是需要使内核能读写仅知道物理地址的内存。为了把物理地址转为虚拟地址,内核需要给物理地址加上 0xf0000000。这就是 KADDR 函数做的事。

同样,JOS 内核有时也需要从虚拟地址获得物理地址。内核的全局变量和由 boot_alloc 分配的内存都在内核被加载的区域,即从0xf0000000开始的地方。因此,若需要将虚拟地址转为物理地址,直接减去0xf0000000即可。这就是 PADDR 函数做的事。

inc/mmu.h 中有许多将会用到的宏以及常量,在 exercise 4 中使用到的已经用中文给出注释,如下:

// The PDX, PTX, PGOFF, and PGNUM macros decompose linear addresses as shown.

// To construct a linear address la from PDX(la), PTX(la), and PGOFF(la),

// use PGADDR(PDX(la), PTX(la), PGOFF(la)).

// page number field of address

#define PGNUM(la) (((uintptr_t) (la)) >> PTXSHIFT)

// page directory index

// 取31到22 bit

#define PDX(la) ((((uintptr_t) (la)) >> PDXSHIFT) & 0x3FF)

// page table index

// 取21到12 bit

#define PTX(la) ((((uintptr_t) (la)) >> PTXSHIFT) & 0x3FF)

// offset in page

// 取11到0 bit

#define PGOFF(la) (((uintptr_t) (la)) & 0xFFF)

// construct linear address from indexes and offset

#define PGADDR(d, t, o) ((void*) ((d) << PDXSHIFT | (t) << PTXSHIFT | (o)))

// Page directory and page table constants.

#define NPDENTRIES 1024 // page directory entries per page directory

#define NPTENTRIES 1024 // page table entries per page table

#define PGSIZE 4096 // bytes mapped by a page, 4kB

#define PGSHIFT 12 // log2(PGSIZE)

#define PTSIZE (PGSIZE*NPTENTRIES) // bytes mapped by a page directory entry, 4MB

#define PTSHIFT 22 // log2(PTSIZE)

#define PTXSHIFT 12 // offset of PTX in a linear address

#define PDXSHIFT 22 // offset of PDX in a linear address

// The PTE_AVAIL bits aren't used by the kernel or interpreted by the

// hardware, so user processes are allowed to set them arbitrarily.

#define PTE_AVAIL 0xE00 // Available for software use

// Flags in PTE_SYSCALL may be used in system calls. (Others may not.)

#define PTE_SYSCALL (PTE_AVAIL | PTE_P | PTE_W | PTE_U)

// Address in page table or page directory entry

// 将页目录项的后12位(flag 位)全部置 0 获得对应的页表项物理地址

#define PTE_ADDR(pte) ((physaddr_t) (pte) & ~0xFFF)

还有一些页表以及页目录会用到的标识位,exercise 4 中用得到的用中文注释:

// Page table/directory entry flags.

#define PTE_P 0x001 // 该项是否存在

#define PTE_W 0x002 // 可写入

#define PTE_U 0x004 // 用户有权限读取

#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

pgdir_walk 函数

作用是查找一个虚拟地址对应的页表项地址,需要完成如图的转换,返回对应的页表地址,即红圈圈出的部分的虚拟地址:

转换流程

主要难点在于各类地址的理解。尤其注意,在页目录项、页表项中存储的是页表项的物理地址前 20bit 外加 12bit 的 flag。

pte_t *

pgdir_walk(pde_t *pgdir, const void *va, int create)

{

// 参数1: 页目录项指针

// 参数2: 线性地址,JOS 中等于虚拟地址

// 参数3: 若页目录项不存在是否创建

// 返回: 页表项指针

uint32_t page_dir_idx = PDX(va);

uint32_t page_tab_idx = PTX(va);

pte_t *pgtab;

if (pgdir[page_dir_idx] & PTE_P) {

pgtab = KADDR(PTE_ADDR(pgdir[page_dir_idx]));

} else {

if (create) {

struct PageInfo *new_pageInfo = page_alloc(ALLOC_ZERO);

if (new_pageInfo) {

new_pageInfo->pp_ref += 1;

pgtab = (pte_t *) page2kva(new_pageInfo);

// 修改页目录的flag,根据 check_page 函数中用到的属性。

// 因为分配以页为单位对齐,必然后 12bit 为0

pgdir[page_dir_idx] = PADDR(pgtab) | PTE_P | PTE_W | PTE_U;

} else {

return NULL;

}

} else {

return NULL;

}

}

return &pgtab[page_tab_idx];

}

page_lookup 函数

根据各个函数的依赖关系,下一个编写 page_lookup 函数。作用是查找虚拟地址对应的物理页描述。

struct PageInfo *

page_lookup(pde_t *pgdir, void *va, pte_t **pte_store)

{

// 参数1: 页目录指针

// 参数2: 线性地址,JOS 中等于虚拟地址

// 参数3: 指向页表指针的指针

// 返回: 页描述结构体指针

pte_t *pgtab = pgdir_walk(pgdir, va, 0); // 不创建,只查找

if (!pgtab) {

return NULL; // 未找到则返回 NULL

}

if (pte_store) {

*pte_store = pgtab; // 附加保存一个指向找到的页表的指针

}

return pa2page(PTE_ADDR(*pgtab)); // 返回页面描述

}

此处再次用到了 PTE_ADDR 这个宏。其作用是将页表指针指向的内容转为物理地址。

page_remove 函数

作用是移除一个虚拟地址与对应的物理页的映射。

void

page_remove(pde_t *pgdir, void *va)

{

// Fill this function in

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; // 将内容清0,即无法再根据页表内容得到物理地址。

tlb_invalidate(pgdir, va); // 通知tlb失效。tlb是个高速缓存,用来缓存查找记录增加查找速度。

}

page_insert 函数

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

int

page_insert(pde_t *pgdir, struct PageInfo *pp, void *va, int perm)

{

// 参数1: 页目录指针

// 参数2: 页描述结构体指针

// 参数3: 线性地址,JOS 中等于虚拟地址

// 参数4: 权限

// 返回: 成功(0),失败(-E_NO_MEM)

pte_t *pgtab = pgdir_walk(pgdir, va, 1); // 查找该虚拟地址对应的页表项,不存在则建立。

if (!pgtab) {

return -E_NO_MEM; // 空间不足

}

if (*pgtab & PTE_P) {

// 页表项已经存在,即该虚拟地址已经映射到物理页了

if (page2pa(pp) == PTE_ADDR(*pgtab)) {

// 如果映射到与之前相同的页,仅更改权限,不增加引用

// 记录自己犯的一个错误,这种写法无法减少权限

// *pgtab |= perm;

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

return 0;

} else {

// 如果是更新映射的物理页,则要删除之前的映射关系

page_remove(pgdir, va);

}

}

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

pp->pp_ref++;

return 0;

}

需要注意的是,如果同样的虚拟页映射到了同样的物理页,如果不做特殊处理仍然调用 page_remove 后再增加引用次数,可能会出现以下情况:

当该物理页 ref = 1,经过 page_remove 后会被加入空闲页链表。然而,在函数最后还需要增加其引用计数,导致 page_free_list 中出现了非空闲页。

课程中希望尽量不要做特例处理,即避免使用if,于是可以这么改进:

int

page_insert(pde_t *pgdir, struct PageInfo *pp, void *va, int perm)

{

pte_t *pgtab = pgdir_walk(pgdir, va, 1);

if (!pgtab) {

return -E_NO_MEM;

}

// 这里一定要提前增加引用

pp->pp_ref++;

if (*pgtab & PTE_P) {

page_remove(pgdir, va);

}

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

return 0;

}

boot_map_region 函数

作用是映射一片指定虚拟页到指定物理页。思路就是反复利用pgdir_walk。难度不高,注意此时的 va 类型是 uintptr_t,调用 pgdir_walk 时需要转换为 void *。

static void

boot_map_region(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm)

{

// Fill this function in

pte_t *pgtab;

size_t end_addr = va + size;

for (;va < end_addr; va += PGSIZE, pa += PGSIZE) {

pgtab = pgdir_walk(pgdir, (void *)va, 1);

if (!pgtab) {

return;

}

*pgtab = pa | perm | PTE_P;

}

}

完成以上几个函数,编译运行。出现 check_page() succeeded! 则成功。

Exercise 5

Fill in the missing code in mem_init() after the call to check_page().

JOS 将处理器的 32 位线性地址分为用户环境(低位地址)以及内核环境(高位地址)。分界线在 inc/memlayout.h 中定义为 ULIM:

#define KERNBASE 0xF0000000

// Kernel stack.

#define KSTACKTOP KERNBASE

// Memory-mapped IO.

#define MMIOLIM (KSTACKTOP - PTSIZE)

#define MMIOBASE (MMIOLIM - PTSIZE)

#define ULIM (MMIOBASE)

其中 PTSIZE 被定义为一个页目录项映射的 Byte,一个页目录中有1024个页表项,每个页表项可映射一个物理页。故为 4MB。可算得 ULIM = 0xf0000000 - 0x00400000 - 0x00400000 = 0xef800000,可通过查看 inc/memlayout 确认。

我们还需要给物理页表设置权限以确保用户只能访问用户环境的地址空间。否则,用户的代码可能会覆盖内核数据,造成严重后果。用户环境应该在高于 ULIM 的内存中没有任何权限,而内核则可以读写着部分内存。在 UTOP( 0xeec00000) 到 ULIM 的 12MB 区间中,存储了一些内核数据结构。内核以及用户环境对这部分地址都只具有 read-only 权限。低于 UTOP 的内存则由用户环境自由设置权限使用。

个人感觉,exercise 4 中的 boot_map_region 放到这里更合适,因为在这里才会用到。而且,之前的这个写法,其实存在一个很大的问题,马上揭晓。不知道有没有大牛可以提前看出来。

static void

boot_map_region(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm)

{

// Fill this function in

pte_t *pgtab;

size_t end_addr = va + size;

for (;va < end_addr; va += PGSIZE, pa += PGSIZE) {

pgtab = pgdir_walk(pgdir, (void *)va, 1);

if (!pgtab) {

return;

}

*pgtab = pa | perm | PTE_P;

}

}

该练习中主要映射了三段虚拟地址到物理页上。

UPAGES (0xef000000 ~ 0xef400000) 最多4MB

这是 JOS 记录物理页面使用情况的数据结构,即 exercise 1 中完成的东西,只有 kernel 能够访问。由于用户空间同样需要访问这个数据结构,我们将用户空间的一块内存映射到存储该数据结构的物理内存上。很自然联想到了 boot_map_region 这个函数。

//

// 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(kernel_pgdir, (uintptr_t) UPAGES, npages*sizeof(struct PageInfo), PADDR(pages), PTE_U | PTE_P);

需要注意的是目前只建立了一个页目录,即 kernel_pgdir,所以第一个参数显然为 kernel_pgdir。第二个参数是虚拟地址,UPAGES 本来就是以虚拟地址形式给出的。第三个参数是映射的内存块大小。第四个参数是映射到的物理地址,直接取 pages 的物理地址即可。权限 PTE_U 表示用户有权限读取。

内核栈 ( 0xefff8000 ~ 0xf0000000) 32kB

bootstack 表示的是栈地最低地址,由于栈向低地址生长,实际是栈顶。常数 KSTACKTOP = 0xf0000000,KSTKSIZE = 32kB。在此之下是一块未映射到物理内存的地址,所以如果栈溢出时,只会报错而不会覆盖数据。因此我们只用映射 [KSTACKTOP-KSTKSIZE, KSTACKTOP) 区间内的虚拟地址即可。

//

// 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(kernel_pgdir, (uintptr_t) (KSTACKTOP-KSTKSIZE), KSTKSIZE, PADDR(bootstack), PTE_W | PTE_P);

再次说一下权限问题。这里设置了 PTE_W 开启了写权限,然而并没有开启 PTE_U,于是仅有内核能够读写,用户没有任何权限。

内核 ( 0xf0000000 ~ 0xffffffff ) 256MB

之前在 lab1 中,通过 kernel/entrypgdir.c 映射了 4MB 的内存地址,这里需要映射全部 0xf0000000 至 0xffffffff 共 256MB 的内存地址。

//

// 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, (uintptr_t) KERNBASE, ROUNDUP(0xffffffff - KERNBASE, PGSIZE), 0, PTE_W | PTE_P);

运行到这里,出现了一个不易察觉到问题。注意到,这里的 size 参数做了roundup,也就是说从 0x0fffffff 变为了 0x10000000。在 boot_map_region 中,再利用 va + size,显然会溢出得0。于是就会出现如下现象:

...

va = 0xef035000

va = 0xef036000

va = 0xef037000

va = 0xef038000

va = 0xef039000

va = 0xef03a000

va = 0xef03b000

va = 0xef03c000

va = 0xef03d000

va = 0xef03e000

va = 0xef03f000

size = 32768, 8 pages

va = 0xefff8000

va = 0xefff9000

va = 0xefffa000

va = 0xefffb000

va = 0xefffc000

va = 0xefffd000

va = 0xefffe000

va = 0xeffff000

size = 268435456, 65536 pages

kernel panic at kern/pmap.c:696: assertion failed: check_va2pa(pgdir, KERNBASE + i) == i

...

即 boot_map_region 中的 for 循环一开始就判断 va > end_addr。这是显然的,因为 end_addr = 0xf0000000 + 0x1000000 = 0x00000000。

因此,实际上 boot_map_region 的更佳实现是直接用页数,避免溢出。更改如下:

static void

boot_map_region(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm)

{

// Fill this function in

pte_t *pgtab;

size_t pg_num = PGNUM(size);

cprintf("map region size = %d, %d pages\n",size, pg_num);

for (size_t i=0; i

pgtab = pgdir_walk(pgdir, (void *)va, 1);

if (!pgtab) {

return;

}

//cprintf("va = %p\n", va);

*pgtab = pa | perm | PTE_P;

va += PGSIZE;

pa += PGSIZE;

}

}

编译运行,出现:

check_kern_pgdir() succeeded!

check_page_installed_pgdir() succeeded!

检查通过,实验成功。

Questions

What entries (rows) in the page directory have been filled in at this point? What addresses do they map and where do they point? In other words, fill out this table as much as possible:

Entry

Base Virtual Address

Points to (logically)

1023

0xffc00000

Page table for [252,256) MB of phys memory

...

...

...

961

0xf0400000

Page table for [4,8) MB of phys memory

960

0xf0000000

Page table for [0,4) MB of phys memory

959

0xefc00000

958

0xef800000

ULIM

957

0xef400000

State Register

956

0xef000000

UPAGES, array of PageInfo

955

0xeec00000

UPAGES, array of PageInfo

...

...

NULL

1

0x00400000

NULL

0

0x00000000

same as 960

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 则用户无权限读写。

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

注意到,pages 这个数组只能占用最多 4MB 的空间,而每个 PageInfo 占用 8Byte,也就是说最多只能有512k页,每页容量4kB,总共最多 2GB。

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?

"overhead"在这里指的是开支。当我们达到最高物理内存时,显然1 个 page_dir 和 1024 个 page_table 都在工作,page_dir 和 page_table 每个 entry 都是 4 byte,且都有1024个 entry。所以一共 (1024 + 1) * 4kB = 4100 kB,还要加上 pages 数组所占用的 4MB,一共 8196 kB。如果要削减这个开支,可以使每个页的容量变大,例如变为 8kB 。

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?

# 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

语句jmp *%eax即转到 eax 所存的地址执行,在这里完成了跳转。relocated 部分代码主要设置了栈指针以及调用 kern/init.c。由于在 kern/entrypgdir.c 中将 0~4MB 和 KERNBASE ~ KERNBASE + 4 MB 的虚拟地址都映射到了 0~4MB 的物理地址上,因此无论 EIP 在高位和低位都能执行。必需这么做是因为如果只映射高位地址,那么在开启分页机制的下一条语句就会crash。

Challenge

这部分有意思的题目还是比较多,选一题来加深下印象,对做 Question 1 也有帮助。

Extend the JOS kernel monitor with commands to:

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.

在 monitor 中添加命令的方法可参考 lab1 中的 backtrace 。此处还需要在 kern/monitor.h 中定义一下该函数。

int

mon_showmappings(int argc, char **argv, struct Trapframe *tf)

{

// 参数检查

if (argc != 3) {

cprintf("Requir 2 virtual address as arguments.\n");

return -1;

}

char *errChar;

uintptr_t start_addr = strtol(argv[1], &errChar, 16);

if (*errChar) {

cprintf("Invalid virtual address: %s.\n", argv[1]);

return -1;

}

uintptr_t end_addr = strtol(argv[2], &errChar, 16);

if (*errChar) {

cprintf("Invalid virtual address: %s.\n", argv[2]);

return -1;

}

if (start_addr > end_addr) {

cprintf("Address 1 must 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);

// 记录自己一个错误

// if ( !cur_pte) {

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':'-';

// 进入 else 分支说明 PTE_P 肯定为真了

cprintf( "-%c----%c%cP\n", perm_PS, perm_U, perm_W);

}

cur_addr += PGSIZE;

}

return 0;

}

主要有四个重要的地方:

strtol 函数

long int strtol(const char *nptr,char **endptr,int base);

作用是将字符串转为整数,可以通过 base 指定进制,会将第一个非法字符的指针写入 endptr 中。所以相比 atoi 函数,可以检查是否转换成功。

pgdir_walk 函数的返回情况有几种?

if ( !cur_pte || !(*cur_pte & PTE_P)) 非常容易遗漏第二个条件。注意到,pgdir_walk 这个函数返回值可能为NULL,也可能是一个pte_t *,而 pte_t * 分为两种情况,一种是该二级页表项内容还未插入,所以 PTE_P 这个位为0。另一种是已经插入。

如何输出 permission

这个就自由发挥了,一共有9个flag,我只选了 lab2 需要用到的3个。

如何验证

我选择用 exercise 5 中映射的内存块来验证。例如内核栈:

K> showmappings 0xefff0000 0xf0000000

Virtual address [efff0000] - not mapped

Virtual address [efff1000] - not mapped

Virtual address [efff2000] - not mapped

Virtual address [efff3000] - not mapped

Virtual address [efff4000] - not mapped

Virtual address [efff5000] - not mapped

Virtual address [efff6000] - not mapped

Virtual address [efff7000] - not mapped

Virtual address [efff8000] - physical address [0010d000], permission: -------WP

Virtual address [efff9000] - physical address [0010e000], permission: -------WP

Virtual address [efffa000] - physical address [0010f000], permission: -------WP

Virtual address [efffb000] - physical address [00110000], permission: -------WP

Virtual address [efffc000] - physical address [00111000], permission: -------WP

Virtual address [efffd000] - physical address [00112000], permission: -------WP

Virtual address [efffe000] - physical address [00113000], permission: -------WP

Virtual address [effff000] - physical address [00114000], permission: -------WP

Virtual address [f0000000] - physical address [00000000], permission: -------WP

K>

python编写操作系统实验_6.828 操作系统 lab2 实验报告相关推荐

  1. 操作系统lab2实验总结——Part1

    ​ 这是MIPS虚拟映射布局图,在接下来的实验中,我们需要特别注意的地址分别是kuseg和kseg0区,首先列出这两个区域的意义. MIPS虚存映射布局 32位的MIPS CPU最大寻址空间为4GB( ...

  2. c语言链表最高响应比优先,操作系统--最高响应比优先调度算法实验报告..doc

    操作系统--最高响应比优先调度算法实验报告. 进程调度一.实验题目与要求 编写程序完成批处理系统中的作业调度,要求采用响应比高者优先的作业调度算法.实现具体包括:首先确定作业控制块的内容和组成方式:然 ...

  3. 实验四linux操作系统实验报告(1),操作系统实验报告

    一. 实验目的及实验环境 (一) 实验环境 Linux 操作系统 (二)实验目的 实验1 掌握Linux基本命令 和开发环境 掌握常用的Linux shell命令: 掌握编辑环境VIM: 掌握编译环境 ...

  4. 网上linux实验平台,Linux操作系统实验教程

    第一部分 Linux操作系统基本原理. 第1章 Linux操作系统简介 1.1 Linux的渊源和发展简史 1.2 Linux的基本特性 1.3 Linux内核的开发模式与内核版本号 1.4 Linu ...

  5. python操作系统课程设计_操作系统课程设计.pdf

    计算机科学与通信工程学院 操作系统课程设计报告 题目:linux系统下实现PV 操作 班级: 软件工程1401 姓名: 吴帅帅 学号: 3140608020 指导老师: 牛德姣 2016 年12 月2 ...

  6. 操作系统是如何工作的--------Linux 实验二

    操作系统是如何工作的? 作者:20135108 李泽源 <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 ...

  7. 操作系统形考实验linux,电大操作系统网上形考任务04 实验报告.doc

    电大操作系统网上形考任务04 实验报告 电大 教师评语 教师签字 日期成绩学生姓名学 号班 级分 组项目编号OS-1项目名称Linux系统安装实验实验报告一.实验目的和实验要求 1.实验目的: 学会在 ...

  8. 操作系统实验11:内存管理实验(DAY 62)

    文章目录 1:实验要求: 2:代码实现 3:实验分析: 4:结果分析 1:出现两个进程 2:设置内存空间为256 3:再重复一次上一步操作 4:输入5,展示内存空间 5:输入4,杀死2号进程. 1:实 ...

  9. linux系统进程调度算法实验,操作系统实验三、进程调度算法实验

    实验三.进程调度算法实验 3.1 实验目的 加深对进程调度概念的理解,体验进程调度机制的功能,了解Linux 系统中进程 调度策略的使用方法.练习进程调度算法的编程和调试技术. 3.2 实验说明 在 ...

最新文章

  1. 如何花钱让2000元的月收入工资价值最大化?
  2. lamp黄金组合apache+mysql+php的安装
  3. vb编程的好帮手--资源文件
  4. [Apache] Apache 從 2.2 換至 2.4 httpd.conf 的調整筆記 (windows 環境)
  5. 手机展示海报就用它 再不要羡慕别人
  6. java 版本EChart使用
  7. 为何python不好找工作-学完Python,为什么还找不到工作?现实很残酷!
  8. 分层架构(第一张章)
  9. python数据1-4
  10. 三个一工程_C语言阶段第二阶段部分程序整理
  11. 根据边界坐标提取边界内部数据点坐标
  12. Kotlin中文教程
  13. 通过网络爬虫采集大数据
  14. visio导出pdf只保存绘图区域以及插入符号为灰色、插入异或符号
  15. 小组取什么名字好_给小组取个好名字
  16. windows2003 升级sp1,sp2 产品密匙无效
  17. 37岁被裁员,出来再找工作,大公司不愿要,无奈去小公司面试,HR的话扎心了
  18. mysql从数据库查询的时间与实际时间相差8小时(时区问题)
  19. Android闹钟制作过程图,小学闹钟手工制作步骤详解(配图)
  20. Windows命令-解压缩文件-tar

热门文章

  1. 【阮一峰博客学习记录1】追踪用户点击
  2. HMI智能座舱自动化测试内容
  3. 安卓-碎片的使用入门
  4. 软件测试工程师真实经历,一个软件测试工程师的加班经历
  5. Google Earth Engine(GEE) ——土著土地地图数据集
  6. 2023年十大热门专业和它们的就业前景
  7. PLS系列003 单因变量线性PLS
  8. html注册失败,javascript – 服务工作者注册失败
  9. java 时区 edt_时区EST与EST5EDT有何不同?
  10. hdu4160 Dolls