lab 4:内存管理
Overview
在本实验中,我们将编写操作系统的内存管理代码。 内存管理有两个组成部分。
第一个部分是内核的物理内存分配器,以致于内核可以分配和释放内存。 分配器将以4096字节为操作单位,称为一个页面。 我们的任务是维护一个数据结构,去记录哪些物理页面是空闲的,哪些是已分配的,以及共享每个已分配页面的进程数。 我们还要编写例程来分配和释放内存页面。
内存管理的第二个组件是虚拟内存,它将内核和用户软件使用的虚拟地址映射到物理内存中的地址。 当指令使用内存时,x86硬件的内存管理单元(MMU)
执行映射,查询一组页表。 我们根据任务提供的规范修改JOS以设置MMU的页面表。
Part Ⅰ:Physical Page Management
物理内存页管理
操作系统必须跟踪物理RAM(Random Access Memory,一般是主存)的哪些部分是空闲的,哪些部分正在使用,而这部分的实现是通过物理页面分配器来进行的,它通过struct PageInfo的链表来查询,每个PageInfo对应着一个物理内存页面
Exercise 1
boot_alloc()函数
该函数维护一个static的指针nextfree,初始值是end,end是定义在/kern/kernel.ld中定义的符号,位于bss段的末尾。也就是说从内核的末尾开始分配物理内存。需要添加如下代码
// 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.
result = nextfree;
nextfree = ROUNDUP((char *)result + n, PGSIZE);
cprintf("boot memory at %x, next memory allocate at %x\n",result, nextfree);
return result;
之后来看mem_init()函数
mem_init()函数
可以看到是将boot_alloc()返回的result的值(当前的页)给了kern_pgdir,kern_pgdir保存的是内核页目录的物理地址
之后根据注释完成代码
// 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对应一个物理页的信息
page_init()函数
这个函数的主要作用是初始化之前分配的pages数组,并且构建一个PageInfo链表,保存空闲的物理页,表头是全局变量page_free_list。
// 1)第一个物理页是IDT所在,需要标识为已用
// 2)[IOPHYSMEM, EXTPHYSMEM)称为IO hole的区域,需要标识为已用。
// 3)EXTPHYSMEM是内核加载的起始位置,终止位置可以由boot_alloc(0)给出(理由是boot_alloc()分配的内存是内核的最尾部),这块区域也要标识
size_t i;
size_t io_hole_start_page = (size_t)IOPHYSMEM / PGSIZE;
size_t kernel_end_page = PADDR(boot_alloc(0)) / PGSIZE;
for ( i = 0; i < npages; i++)
{
if (i == 0)
{
pages[i].pp_ref = 1;
pages[i].pp_link = NULL;
}
else if (i >= io_hole_start_page && i < kernel_end_page)
{
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];
}
}
page_alloc()函数
函数具体作用:
从page_free_list指向的链表中取出一个PageInfo结构,之后根据形参决定是否将对应的内存初始化为0
具体实现:
// 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)
{
struct PageInfo *ret = page_free_list;
if (ret = NULL)
{
cprintf("page_alloc:out of free memory now\n");
return NULL;
}
page_free_list = ret->pp_link;
ret->pp_link = NULL;
if (alloc_flags && ALLOC_ZERO)
{
memset(page2kva(ret), 0, PGSIZE);
}
// Fill this function in
return ret;
}
page_free()函数
这个函数就是将对应的物理页设置为空闲状态并且将对应的PageInfo连接到空闲链表中
//
// 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_link != NULL && pp->pp_ref != 0)
{
panic("page_free:pp->pp_link is not NULL or pp->pp_ref is nonzero");
}
pp->pp_link = page_free_list;
page_free_list = pp;
}
Exercise复盘
就是进行了对于内存物理页面的管理,将4096个字节分割成为一个页,然后通过数据结构PageInfo来进行管理,存储对应的数组,并且初始化一些和物理页面相关的操作,比如page_alloc和page_free,完成Part Ⅰ之后的内存情况如下
Part Ⅱ:Virtual Memory
Exercise 4
pagedir_walk()函数
这个函数是给定pdir参数,来指向一个页目录,返回指针指向虚拟地址va对应的页表条目
pte_t *
pgdir_walk(pde_t *pgdir, const void *va, int create)
{
// Fill this function in
pde_t *pde_ptr = pgdir + PDX(va);
if (!(*pde_ptr && PTE_P))
{
if (create)
{
//分配一个页来作为页表
struct PageInfo *pp = page_alloc(1);
if (pp == NULL)
{
return NULL;
}
pp->pp_ref++;
*pde_ptr = (page2pa(pp)) | PTE_P | PTE_U | PTE_W;
}
else
{
return NULL;
}
}
return (pte_t *)KADDR(PTE_ADDR(*pde_ptr)) + PTX(va);;
}
boot_map_region()函数
该函数的作用是通过修改pgdir指向的树结构,将va,va+size对应的虚拟地址映射到pa,pa+size对应的物理地址空间
static void
boot_map_region(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm)
{
// Fill this function in
size_t pgs = size / PGSIZE;
if (size % PGSIZE != 0 )
{
pgs++; //计算有多少页
}
for (int i = 0; i < pgs; i++)
{
pte_t *pte = pgdir_walk(pgdir, (void *)va, 1);
if (pte == NULL)
{
panic("boot_map_region:out of physical memory");
}
*pte = pa | PTE_P | perm;
pa = pa + PGSIZE;
va = va + PGSIZE;
}
}
page_lookup()函数
查找pgdir指向的树结构,返回va对应的PTE对应的物理地址对应的PageInfo结构
struct PageInfo *
page_lookup(pde_t *pgdir, void *va, pte_t **pte_store)
{
// Fill this function in
struct PageInfo *pp;
pte_t *pte = pgdir_walk(pgdir, va, 0);
if (pte == NULL)
{
return NULL;
}
if (!(*pte) & PTE_P)
{
return NULL;
}
physaddr_t pa = PTE_ADDR(*pte); //va对应的物理
pp = pa2page(pa);
if (pte_store != NULL)
{
*pte_store = pte;
}
return pp;
}
page_remove()函数
修改pgdir对用的树结构,解除映射关系
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); //获取va对应的PTE的地址以及pp结构
if (pp == NULL)
{ //va可能还没有映射,什么都不用做
return;
}
page_decref(pp); //将pp->pp_ref减1,如果pp->pp_ref为0,需要释放该PageInfo结构(将其放入page_free_list链表中)
*pte_store = 0; //将PTE清空
tlb_invalidate(pgdir, va); //失效化TLB缓存
}
page_insert()函数
修改pgdir对应的树结构,建立va与pp对应的内存物理页之间的链接
int
page_insert(pde_t *pgdir, struct PageInfo *pp, void *va, int perm)
{
// Fill this function in
// Fill this function in
pte_t *pte = pgdir_walk(pgdir, va, 1); //拿到va对应的PTE地址,如果va对应的页表还没有分配,则分配一个物理页作为页表
if (pte == NULL) {
return -E_NO_MEM;
}
pp->pp_ref++;
if ((*pte) & PTE_P)
{ //当前虚拟地址va已经被映射过,需要先释放
page_remove(pgdir, va);
}
physaddr_t pa = page2pa(pp); //将PageInfo结构转换为对应物理页的首地址
*pte = pa | perm | PTE_P; //修改PTE
pgdir[PDX(va)] |= perm;
return 0;
}
Exercise 4复盘
实现的是对于虚拟内存和物理页面之间的建立联系/树结构的过程,并且实现了一些相关操作
Part Ⅲ:kernel address space
Exercise 5
//////////////////////////////////////////////////////////////////////
// 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映射到物理地址pags起始位置
boot_map_region(kern_pgdir, UPAGES, PTSIZE, PADDR(pages), 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:
boot_map_region(kern_pgdir, KSTACKTOP-KSTKSIZE, KSTKSIZE, PADDR(bootstack), PTE_W);
//////////////////////////////////////////////////////////////////////
// Map all of physical memory at KERNBASE.
// Ie. the VA range [KERNBASE, 2^32) should map to
// the PA range [0, 2^32 - KERNBASE)
// We might not have 2^32 - KERNBASE bytes of physical memory, but
// we just set up the mapping anyway.
// Permissions: kernel RW, user NONE
// Your code goes here:
boot_map_region(kern_pgdir, KERNBASE, 0xffffffff - KERNBASE, 0, PTE_W);
make grade结果
展示地址
首先是需要声明函数并且在command结构里面加入对应的命令
之后就是对应的hanshudaima
int
mon_showva2pa(int argc, char **argv, struct Trapframe *tf)
{
if (argc != 3 && argc != 2)
{
cprintf("mon_showva2pa:Command error!!!");
}
else if(argc == 3)
{
char *str;
uint32_t start = (uint32_t)strtol(argv[1], &str, 16);
uint32_t end = (uint32_t)strtol(argv[2], &str, 16);
uint16_t ref;
uint32_t pa;
int u, w;
pde_t *pte;
struct PageInfo *po;
for ( ; start <= end; start += PGSIZE)
{
po = page_lookup(kern_pgdir, (void *)start, &pte);
pte = pgdir_walk(kern_pgdir, (void *)start, 0);
if (pte == NULL)
{
cprintf("the va does not have a corresponding physical page");
break;
}
ref = po->pp_ref;
u = ((* pte&PTE_U) == PTE_U);
w = ((* pte & PTE_W) == PTE_W);
pa = PTE_ADDR(* pte) | (start & 0xff);
cprintf("VA = %x, PA = %x, pp_ref = %d, PTE_U = %d, PTE_W = %d\n",start, pa, ref, u, w);
}
}
else if (argc == 2)
{
char *str;
uint32_t va = (uint32_t)strtol(argv[1], &str, 16);
uint16_t ref;
uint32_t pa;
int u, w;
pde_t *pte;
struct PageInfo *po;
po = page_lookup(kern_pgdir, (void *)va, &pte);
pte = pgdir_walk(kern_pgdir, (void *)va, 0);
if (pte == NULL)
{
cprintf("the va does not have a corresponding physical page");
return 0;
}
ref = po->pp_ref;
u = ((* pte&PTE_U) == PTE_U);
w = ((* pte & PTE_W) == PTE_W);
pa = PTE_ADDR(* pte) | (va & 0xff);
cprintf("VA = %x, PA = %x, pp_ref = %d, PTE_U = %d, PTE_W = %d\n",va, pa, ref, u, w);
}
return 0;
}
运行截图
- 范围查询
- 单页查询
问题与回答
Q1
程序中的地址从什么时候开始都是虚拟地址了,请找到那几行代码。
A1
- 分段机制:是 boot.S 中加载 GDT 并启用 cr0 保护模式后启用的
# Switch from real to protected mode, using a bootstrap GDT # and segment translation that makes virtual addresses # identical to their physical addresses, so that the # effective memory map does not change during the switch. lgdt gdtdesc movl %cr0, %eax orl $CR0_PE_ON, %eax movl %eax, %cr0
- 分页机制:是 entry.S 中加载了 entry_pgdir 后启用的
# 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
- 分页分段都启用后,CPU 便能处理虚拟地址
Q2
mem_init()函数中 kern_pgdir 的虚拟地址是多少?物理地址呢?在我们还未完成本 次 lab 之前,为什么我们已经可以使用虚拟地址了?
A2
cprintf("%x\n", kern_pgdir);
得到结果 0xf011b000,对应物理地址 0x0011b000
因为在程序中已经完成了内核部分 4MB 大小的虚拟地址到物理地址的简单映射,在 kern/entry.S 与 kern/entrypgdir.c 中。
Q3
哪一行代码使得本次 lab 所构建的虚拟内存系统真正被使用?请指出它的位置。
A3
mem_init() 中的lcr3(PADDR(kern_pgdir));
Q4
此操作系统可支持的最大物理内存是多少?为什么?
A4
2GB的最大物理内存,所有空闲的物理页面最开始都放在了pages数组中,数组中每个struct大小为8B,UPAGES大小为PTSIZE,所以最多可存储512K个pageInfo,而每个结构对应页面大小4KB,所以最多可以管理2^19 *2^12 = 2^31 = 2GB
Q5
请详细描述在 JOS 中虚拟地址到物理地址的转换过程。
A5
- 在 inc/mmu.h 中的 PDX、PTX、PGOFF 三个宏将虚拟地址分为 31 - 22 位、21 - 12 位、11 - 0位的三段。
- 首先通过 cr3 寄存器找到页表目录的物理地址 kern_pgdir,以虚拟地址的高10位
PDX(va)
作为索引在页表目录中找对应的页表项,该表项储存次级页表的起始地址和标志位。 - 然后通过
PTE_ADDR()
取出将页表项的高 20 位得到次级页表的物理地址,以虚拟地址的中间 10 位PTX(va)
为索引找到对应的页表项,该表项储存对应物理页框的地址。 - 最后将线性地址的低 12 位
PGOFF(va)
与物理页框起始地址相加就得到了虚拟地址 va 对应的物理地址 pa。
Q6
在函数 pgdir_walk() 的上下文中,请说明以下地址的含义,并指出他们是虚拟地址还是物理地址:
A6
- pgdir 页目录地址,是虚拟地址。
- pgtab = PTE_ADDR(pgdir[PDX(va)]) pgdir[PDX(va)] 是页表目录中以 PDX(va)为索引找到的页表项, 页表项中储存的都是物理地址。而宏 PTE_ADDR 则是去除这个地址的高 20 位,因此也是物理地址。
- pg = PTE_ADDR(KADDR(pgtab)[PTX(va)]) pg 就是二级页表中对应这个地址的页表项,因此也是物理地址。
Q7
画出本次 Lab 结束后虚拟地址空间与物理地址空间的映射关系,地址空间表示图中应至 少包含 kern_pgdir 与 pages,展示越多的细节越好。(提示:地址空间的表示方式可以 参考 Lab 1-“The PC’s Physical Address Space”小节)
A7
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!