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 的硬件总线上的地址。
具体的转换过程参见该PDF:
https://pdos.csail.mit.edu/6.828/2016/lec/x86_translation_and_registers.pdf
在 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
编译运行,出现:
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>